我正在努力建立用于SSO身份验证的OpenID Connect服务器。我想我的基本设置/要求是非常标准的,但我有一点点困难,把他们放在一起。
广泛的设置是一个单页应用程序,一个Web API和一个身份服务器。 SPA使用与Web API相同的域名提供服务,并且ID服务器位于不同的域中,因此我可能有几种SPA / Web API组合,但是当然每种情况都是相同的设置(具有静态内容的单个主机和API)。目前,我正在与IdentityServer4合作创建身份服务器;如果该提供商存在某种问题,我很乐意尝试其他提供商,但到目前为止效果很好。
我认为我的登录要求也很标准;我想拥有短暂的访问令牌,并且我还想使用刷新令牌来实现滑动到期,因此不必将用户重定向到我的SPA之外,直到他们在一段时间内处于非活动状态为止(但是我结束了向上限定)。
经过一些研究,我想我想要使用授权码流程。因此,总的来说,我认为可行的方式是:
所以我经历了tutorial on the IdentityServer4 site,令我惊讶的是,我最终处于一种不同的状态。我花了一些时间来解决它;我要说的是,要有人遵循的步骤是“添加JavaScript客户端”,但我愿意成为在实现OpenID Connect的人们中常见的结果。最终的流程与我从步骤5开始的预期有所不同;代替SPA调用带有授权代码的网络API并请求访问令牌,则SPA用途CORS并使得跨域请求回ID服务器以请求令牌接入。本教程并没有真正涵盖所有刷新令牌(文档的其他部分都做了,但只是简短地介绍了),但是我认为这意味着,如果我想使用刷新令牌,则会将它们发行给客户端,并且它会使用本地存储来存储他们。那么对于未来的刷新,也愿意做一个跨域请求回ID服务器。附带说明一下,另一个令人惊讶的地方是该教程使用了PKCE,但对于Web应用程序而言,在研究中似乎没有必要使用PKCE。这有点重要,因为在客户端中包含SHA-2实施会增加我的应用程序的大小。
我相信这是一个不好的做法,发出令牌网络客户端刷新,并要求它来存储它;对于可能出现的特定漏洞,我有些含糊,但总体思路是,如果有人以某种方式破坏了您的客户端,则刷新令牌要比短期访问令牌强大得多。
因此,我想办法解决这个问题,我相信本来可以起作用的方式是,Web API是OAuth 2的“依赖方”,而本教程将其设置为使客户端是“依赖方”。这让我认为,如果我想让到期日滑落,我必须走过教程的去处,然后像我最初设想的那样,将用于令牌交换的功能从客户端转移到Web API中。最终看起来就像Web API在功能上是SPA的代理,以交换访问令牌的授权代码。
最终,我的问题是:我说对了吗?看起来,实际上有两种不同的模型可以为SPA / API Web应用程序实现OpenID Connect。一个是API是RP,另一个是SPA是RP。如果你想使用刷新令牌,我想你应该选择1走,但也许如果你关心的是,API可以模拟你与选项2去客户端?对我来说,这似乎仍然无关紧要。授权代码/访问令牌交换只能用于特定的应用程序,因此这并不像一个API可以突然将其身份验证为该设置中的另一个后端。我很担心自己不能从结构上改变教程的设置,因为这与安全性有关。
更新
尽管答案被接受,但我还是使用授权代码流而不是隐式流,因为这是IETF的最新建议(请参阅https://tools.ietf.org/html/draft-parecki-oauth-browser-based-apps-02#section-4,并在https://brockallen.com/2019/01/03/the-state-of-the-implicit-flow-in-oauth2/上有很多文章)。我接受了这个答案,因为通过iframe使用无提示刷新而不是刷新令牌似乎是我尝试执行的最标准的方法。使用它,我能够构建一个类似于本教程的工作系统。事实上,客户端库它建议(oidc-client)有一个内置的函数来处理细节。为了完整起见,我要从头开始使用这个服务:
import oidc from "oidc-client";
import Url from "url-parse";
let baseUrl = new Url(window.location.href).set("pathname", "").set("query", "").set("hash", "");
let redirectUrl = (new Url(baseUrl)).set("query", "redirect=fromIdentityProvider");
let silentRedirectUrl = (new Url(baseUrl)).set("pathname", "silent-refresh.html");
let identitySettings = {
authority: "[my application's id server domain]",
client_id: "[my client's id]",
redirect_uri: redirectUrl.toString(),
response_type: "code",
scope: "openid profile [my application's resource name]",
post_logout_redirect_uri: baseUrl,
automaticSilentRenew: true,
silent_redirect_uri: silentRedirectUrl.toString()
};
let userManager = new oidc.UserManager(identitySettings);
let user = null;
export default {
async logIn() {
await userManager.signinRedirect();
},
async isLoggedIn() {
return !!(await this.getAccessToken());
},
async logOut() {
await userManager.signoutRedirect();
},
async getAccessToken() {
user = await userManager.getUser();
return user ? user.access_token : null;
},
async initializeApp() {
let url = new Url(window.location.href, true);
if (url.query && url.query.redirect === "fromIdentityProvider") {
await new oidc.UserManager({
response_mode: "query"
}).signinRedirectCallback();
window.location = "/";
return false;
}
user = await userManager.getUser();
return true;
}
};
然后在我的应用程序调用initializeApp应用程序启动和getAccessToken时之前的任何API调用。我还需要最终添加从API上401自动重定向的能力,但是这是很容易的。
要进行无声重定向工作,我创建基于这里的说明无声-将redirect.html:https://www.scottbrady91.com/OpenID-Connect/Silent-Refresh-Refreshing-Access-Tokens-when-using-the-Implicit-Flow。我还集成了Google身份验证作为外部提供程序,并验证了它也可用于无提示刷新,因此没有折衷办法。
为解决这个问题,对我来说,我原来的问题的答案基本上是“否”,我不想将交换步骤移到后端。我确实也决定使用PKCE,即使在我看来它不是必需的,这是我提到的IETF建议中的内容,因此我会坚持下去。
答案 0 :(得分:4)
SPA有一个特殊的OAuth2流-Implicit grant。如果只需要访问令牌,则在访问&response_type=token
端点时指定/auth
。或者,你可以要求使用令牌和一个ID &response_type=token id_token&scope=openid
。 SPA从自动化提供者(在哈希部分#access_token=...
中)在重定向URL中获取令牌及其生命周期expires_in=...
。因此令牌保留在您的浏览器中-哈希部分不会发送到托管SPA文件的服务器。
您的SPA应该验证并保留这两个值,并且在令牌到期之前,应使用&prompt=none
参数在/auth
中调用iframe
端点。如果您授权提供支持单点登录(SSO),那么你应该得到,而用户注意到它令牌的新鲜的访问。因此,它的工作原理类似于刷新令牌,而无需CORS,PKCE或客户机密。
如果你想实现一些更复杂的SSO管理,看看在OpenID Connect Session management RFC。