【发布时间】:2019-06-27 01:55:10
【问题描述】:
我正在努力为 SSO 身份验证设置 OpenID Connect 服务器。我认为我的基本设置/要求非常标准,但我很难将它们放在一起。
广泛的设置是单页应用程序、Web API 和身份服务器。 SPA 由与 Web API 相同的域名提供服务,而 ID 服务器位于不同的域中,因此我可能有多个 SPA/Web API 组合,但当然每种情况都是相同的设置(具有静态内容的单个主机和API)。目前我正在与IdentityServer4 合作创建身份服务器;如果该提供商有某种问题,我可以灵活地尝试其他提供商,但到目前为止一切都很好。
我认为我的登录要求也很标准;我想拥有短期访问令牌,并且我还想使用刷新令牌来实现滑动到期,这样用户就不必被重定向离开我的 SPA,直到他们“一段时间”处于非活动状态(但是我结束向上定义)。
经过一番研究,我想我想要的是使用授权码流。所以一般来说,我认为这会起作用的方式是:
- 用户访问应用程序主机(服务于 Web API 和 SPA);提供静态 SPA
- SPA 加载并确定本地存储中没有访问令牌。 SPA 通过生成随机标识符并将其存储在会话存储中来启动登录过程,然后将浏览器导航到 ID 服务器主机
- 用户通过 ID 服务器主机进行身份验证
- ID 服务器主机重定向到客户端,并在重定向中包含 SPA 最初生成的随机标识符以及授权代码
- 在加载并检测到它获得访问代码后,SPA 会检查会话存储中存储在步骤 2 中的标识符。找到它后,SPA 调用 Web API 以将授权代码交换为访问令牌
- Web API 使用带有 ID 服务器的反向通道来生成访问令牌和刷新令牌
- Web API 存储刷新令牌和访问令牌,然后向客户端发出访问令牌
- 在所有未来的请求中,客户端将访问令牌与 Web API 一起使用。当 SPA 确定它拥有的访问令牌已过期或即将过期时,它会以某种方式请求刷新(我现在要手动刷新一下)
所以我浏览了tutorial on the IdentityServer4 site,令我惊讶的是,我最终进入了一个有点不同的状态。我花了一段时间才完成它。如果有人想继续,我正在谈论的步骤是“添加 JavaScript 客户端”,但我愿意结果在实现 OpenID Connect 的人中很常见。由此产生的流程与我从第 5 步开始的预期不同; SPA 不是使用授权代码调用 Web API 并请求访问令牌,而是使用 CORS 并向 ID 服务器发出跨域请求以请求访问令牌。本教程并没有真正涵盖所有刷新令牌(文档的其他部分会这样做,但只是简要介绍),但我认为这意味着如果我想使用刷新令牌,它们将被发布给客户端和它将使用本地存储来存储它们;然后为了将来的刷新,它还会向 ID 服务器发出跨域请求。附带说明一下,另一个令人惊讶的是本教程让您使用 PKCE,这在研究中似乎对于 Web 应用程序是不必要的;这有点重要,因为在客户端包含一个 SHA-2 实现会大大增加我的应用程序的大小。
我认为向 Web 客户端发出刷新令牌并要求它存储它是一种不好的做法;我对打开的特定漏洞有些模糊,但总体思路是,如果有人以某种方式颠覆您的客户端,刷新令牌比短期访问令牌强大得多。
因此,考虑到这一点,我相信我最初认为这可行的方式是,Web API 是 OAuth 2 用语中的“依赖方”,并且教程将其设置为客户端是“依赖方”。这让我觉得,如果我想获得一个滑动到期,我必须超越教程的内容,并将令牌交换的功能从客户端移动到我最初设想的 Web API 中。它最终看起来有点像 Web API 在功能上充当 SPA 的代理,以将授权代码交换为访问令牌。
最后,我的问题是:我做对了吗?看起来确实有两种不同的模型可以为 SPA/API Web 应用程序实现 OpenID Connect;一个 API 是 RP,另一个 SPA 是 RP。如果您想使用刷新令牌,我认为您应该使用选项 1,但也许如果您关心 API 可以模拟客户端,您会使用选项 2?这对我来说似乎仍然无关紧要。该授权代码/访问令牌交换只能用于特定应用程序,因此在该设置中,一个 API 不会突然作为不同的后端进行身份验证。我只是担心自己要从结构上改变教程的设置,因为这是与安全相关的。
更新
尽管答案已被接受,但我还是使用了授权代码流程而不是隐式流程,因为这是 IETF 的最新建议(请参阅 https://datatracker.ietf.org/doc/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 并在任何 API 调用之前调用 getAccessToken。我仍然需要最终添加从 API 自动重定向 401 的功能,但这很容易。
为了使静默重定向工作,我根据此处的说明创建了silent-redirect.html:https://www.scottbrady91.com/OpenID-Connect/Silent-Refresh-Refreshing-Access-Tokens-when-using-the-Implicit-Flow。我还将 Google 身份验证集成为外部提供程序,并验证它也适用于静默刷新,因此无需权衡。
为了完善它,对我来说,我最初问题的答案基本上是“否”,我不想将交换步骤移至后端。我也确实决定使用 PKCE,尽管在我看来它不应该是必要的,它在我提到的 IETF 建议中,所以我会坚持下去。
【问题讨论】:
-
你熟悉这篇文章吗:leastprivilege.com/2019/01/18/…
-
您的第 5 点是错误的——身份令牌和访问令牌只是两种不同类型的令牌——一种用于纯粹封装用户身份信息(姓名、电子邮件)——另一种用于封装用户的访问信息授予您的应用程序(api1、api2)。访问令牌也可以有一些身份信息。第 5 点之后的几乎所有内容也不正确。我个人不会打扰刷新令牌,您可以在身份提供者(ids4)上使用cookie auth,当您的短期令牌过期时,spa 将调用 api,api 返回 401,spa 调用 ids4,ids4 cookie auths 用户并返回
-
@RuardvanElburg 我已经通读了一点;那里肯定有一些很好的建议。我将开始对其进行分类,看看是否是我要走的路;我仍然对 OpenID Connect 似乎没有提供一种安全且标准的方式来实现 SSO 感到不安。它似乎只是一种标准,对于 SPA 来说通常不安全;我希望不必自己编写太多与身份验证相关的代码。那篇文章中还有一个指向我正在阅读的 IETF 文档的链接 (tools.ietf.org/html/draft-parecki-oauth-browser-based-apps-02)。
-
@VidmantasBlazevicius 你是对的;我将授权代码称为 ID 令牌,将交换的令牌称为访问令牌,这是不正确的;我已经编辑了这个问题,所以我认为它现在是正确的。至于刷新令牌;我认为您提出的解决方案行不通。当您说“spa 调用 ids4,ids4 cookie 验证用户并返回”时,那一定是浏览器位置更改,对吧?我不能让应用随机重定向用户;他们会失去工作。我使用过这样做的网站,这真的很烦人。
-
将是浏览器位置更改是的。它在几个项目中对我有用,因为谁在 2019 年在 SPA 应用程序中停留在同一个浏览器窗口会话中超过一个小时。我不知道您的项目上下文,因此它可能对您不起作用(这就是我没有发布答案的原因)。您的第 5 点直到最后仍然看起来不正确,SPA 应该存储刷新令牌然后交换访问令牌的代码。 API 是无状态的,因此存储任何与会话相关的内容都无法正常工作。
标签: oauth-2.0 identityserver4 openid-connect