【问题标题】:Should I move the auth code to Access Token exchange to the API for a Web API/SPA OpenID Connect implementation?我应该将身份验证代码移动到访问令牌交换到 API 以实现 Web API/SPA OpenID Connect 吗?
【发布时间】:2019-06-27 01:55:10
【问题描述】:

我正在努力为 SSO 身份验证设置 OpenID Connect 服务器。我认为我的基本设置/要求非常标准,但我很难将它们放在一起。

广泛的设置是单页应用程序、Web API 和身份服务器。 SPA 由与 Web API 相同的域名提供服务,而 ID 服务器位于不同的域中,因此我可能有多个 SPA/Web API 组合,但当然每种情况都是相同的设置(具有静态内容的单个主机和API)。目前我正在与IdentityServer4 合作创建身份服务器;如果该提供商有某种问题,我可以灵活地尝试其他提供商,但到目前为止一切都很好。

我认为我的登录要求也很标准;我想拥有短期访问令牌,并且我还想使用刷新令牌来实现滑动到期,这样用户就不必被重定向离开我的 SPA,直到他们“一段时间”处于非活动状态(但是我结束向上定义)。

经过一番研究,我想我想要的是使用授权码流。所以一般来说,我认为这会起作用的方式是:

  1. 用户访问应用程序主机(服务于 Web API 和 SPA);提供静态 SPA
  2. SPA 加载并确定本地存储中没有访问令牌。 SPA 通过生成随机标识符并将其存储在会话存储中来启动登录过程,然后将浏览器导航到 ID 服务器主机
  3. 用户通过 ID 服务器主机进行身份验证
  4. ID 服务器主机重定向到客户端,并在重定向中包含 SPA 最初生成的随机标识符以及授权代码
  5. 在加载并检测到它获得访问代码后,SPA 会检查会话存储中存储在步骤 2 中的标识符。找到它后,SPA 调用 Web API 以将授权代码交换为访问令牌
  6. Web API 使用带有 ID 服务器的反向通道来生成访问令牌和刷新令牌
  7. Web API 存储刷新令牌和访问令牌,然后向客户端发出访问令牌
  8. 在所有未来的请求中,客户端将访问令牌与 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


【解决方案1】:

SPA 有一个特殊的 OAuth2 流程 - Implicit grant。如果您只需要访问令牌,请在访问 /auth 端点时指定 &response_type=token。或者,您也可以使用&response_type=token id_token&scope=openid 请求 ID 令牌。 SPA 从自动化提供程序(在哈希部分#access_token=...)获取重定向 URL 中的令牌及其生命周期expires_in=...。所以令牌保留在您的浏览器中 - 哈希部分不会发送到托管 SPA 文件的服务器。

您的 SPA 应该验证并保留这两个值,并且在令牌到期之前,它应该使用 &prompt=none 参数在 iframe 中调用 /auth 端点。如果您的授权提供程序支持单点登录 (SSO),那么您应该获得一个新的访问令牌,而用户不会注意到它。因此,它的工作原理类似于刷新令牌,不需要 CORS、PKCE 或客户端密码。

如果您想实现一些更复杂的 SSO 管理,请查看OpenID Connect Session management RFC。

【讨论】:

  • 我认为隐式授权基本上已被弃用。 IETF 文档在这里讨论它:tools.ietf.org/html/…,这篇文章也是:brockallen.com/2019/01/03/…。但是我认为您关于使用 iframe 的建议仍然适用于代码授权;对我来说,这似乎是一种黑客行为,它基本上就是刷新令牌的作用。我最终可能会使用 iframe 技术并接受这个答案;我还在调查。
  • 这听起来不错,但是您需要同时拥有 auth 和 web 应用程序域,否则您将永远无法通过 csps 来 iframe 您的身份提供者 /code/authorize 页面。如果您想要外部提供商登录(facebook、google)或与他们链接的帐户,它也将不起作用。
  • 是的,隐式授权存在安全注意事项,但其中许多通常适用于任何安全的 SPA。您选择的架构很大程度上取决于您的要求——您是否想要一个无状态的后端,您是想使用访问令牌进行授权(范围)还是仅仅作为后端的一些标识符。如果您使用授权码授权,您将向您的 SPA 发送访问令牌或发布您自己的令牌或 cookie,这意味着您自己的安全协议具有未知风险。
  • 使用iframeprompt=none 获取令牌并不是一种技巧——请查看 Oidc 会话管理 RFC 以了解推荐用途。 iframes/windows 可以使用message 事件进行通信。 Google supportsprompt=none,我不知道 Facebook 和其他提供商。
猜你喜欢
  • 1970-01-01
  • 2023-02-24
  • 2022-01-06
  • 2023-03-27
  • 1970-01-01
  • 1970-01-01
  • 2018-10-22
  • 1970-01-01
  • 2018-11-23
相关资源
最近更新 更多