在客户端的哪里存储刷新令牌?

Posted

技术标签:

【中文标题】在客户端的哪里存储刷新令牌?【英文标题】:Where to store the refresh token on the Client? 【发布时间】:2019-12-30 05:15:37 【问题描述】:

我的 SPA 应用程序使用以下架构 (source):

这假设我的客户端应用程序知道刷新令牌,因为如果不存在用户凭据(例如电子邮件/密码),我需要它来请求新的访问令牌。

我的问题:我在哪里存储刷新令牌在我的客户端应用程序中?关于这个主题有很多关于 SO 的问题/答案,但关于刷新令牌的答案并不清楚.

访问令牌和刷新令牌不应存储在本地/会话存储中,因为它们不是任何敏感数据的地方。因此,我会将 访问令牌 存储在 httpOnly cookie 中(即使有 CSRF),无论如何我对资源服务器的大部分请求都需要它。

但是刷新令牌呢?我不能将它存储在 cookie 中,因为 (1) 它会随每个请求一起发送到我的资源服务器,这使得它也容易受到 CSRF 的攻击,并且(2) 它会发送具有相同攻击向量的公开访问/刷新令牌。

我能想到三种解决方案:


1) 将刷新令牌存储在内存中的 javascript 变量中,这有两个缺点:

a) 它容易受到 XSS 攻击(但可能不像本地/会话存储那么明显 b) 如果用户关闭浏览器选项卡,它会丢失“会话”

尤其是后一个缺点会导致用户体验不佳。


2) 将访问令牌存储在会话存储中并通过Bearer access_token 授权标头将其发送到我的资源服务器。然后我可以使用httpOnly cookie 作为刷新令牌。这有一个我能想到的缺点:

a) 向资源服务器发出的每个请求都会向 CSRF 公开刷新令牌。

3) 将两个令牌都保存在 httpOnly cookie 中,这有提到的缺点是两个令牌都暴露在相同的攻击向量中。


也许除了我提到的缺点之外,还有其他方法或更多(请告诉我),但最终一切都归结为我在客户端的哪里保存刷新令牌?是httpOnly cookie 还是内存中的 JS 变量?如果是前者,那我应该把我的访问令牌放在哪里?

非常高兴从熟悉该主题的人那里获得有关如何以最佳方式做到这一点的任何线索。

【问题讨论】:

提供更多信息。这是 SPA 还是基于服务器的应用程序? 在这种情况下,它是一个 SPA。 想知道您实施的解决方案是什么?我目前正在使用选项 2,而不用担心 CSRF。刷新令牌在每个 s-s-r 上都使用并失效。但是,当 SPA 在多个选项卡中打开时,这会导致访问令牌不同步。 【参考方案1】:

如果您的身份验证提供程序实现了刷新令牌轮换,您可以将它们存储在本地存储中。

但这意味着每次客户端刷新 JWT 时,您的身份验证提供程序都应该返回一个新的刷新令牌。如果再次尝试使用一个刷新令牌,它还应该有一种方法使后代刷新令牌无效。

https://auth0.com/docs/tokens/refresh-tokens/refresh-token-rotation

【讨论】:

【参考方案2】:

将访问令牌存储在会话存储中并通过 承载access_token 授权标头到我的资源服务器。然后 我可以将 httpOnly cookie 用于刷新令牌。这有一个 我能想到的缺点: a) 每次向资源服务器发出请求时,刷新令牌都会暴露给 CSRF。

您可以正确设置CORS policy,以便仅接受来自授权服务器的对/refresh_token 的请求。

如果客户端和服务器由同一台机器提供服务,您可以在Cookie 中将标志sameSite 设置为true,并包含anti-CSRF 令牌。

【讨论】:

【参考方案3】:

您可以将令牌、访问和刷新都存储为 cookie。但刷新令牌必须具有特殊路径(例如 /refresh)。所以刷新令牌只会发送给 /refresh url 的请求,而不是像访问令牌这样的每个请求。

【讨论】:

这是为什么呢?你不能在每个请求中发送刷新令牌,类似于基于会话的身份验证中的 remember.me cookie? @BartoszPopiela,因为刷新令牌应该在刷新时轮换。因此,如果您将它包含在每个请求中,您也会在每个请求中获得一个新令牌。如果您不在刷新时轮换令牌,则意味着长期令牌被盗的机会增加。 @s.meijer 您可以在每个请求中发送刷新令牌,但只有在访问令牌过期时才使用它,然后才创建新的访问令牌并轮换刷新令牌。 好的,根据this 的回答,刷新令牌应该只与授权服务器交换以降低泄漏的风险,因此只将它发送到 /refresh 端点是有意义的,即使资源服务器和授权服务器是一样的。 @Chano cookie 仍然被设置,但是当您关闭所有隐身窗口时它们会被清除,因此它不会影响用户的登录能力。【参考方案4】:

OAuth 定义了四种授权类型:授权码、隐式、 资源所有者密码凭据和客户端凭据。它也是 提供了一种扩展机制来定义额外的授权类型。

__RFC 6749 - The OAuth 2.0 Authorization Framework


Authorization Code 进程本质上是为与安全客户端一起使用而设计的,例如。一个服务器,它被保护得足以容纳Client Secret。如果您的客户端足够安全以保存该秘密,只需将 Refresh Token 与您的 Client Secret 放在同一安全存储中。

User-Agent (UA) 中托管的应用程序并非如此。对于那些,规范建议使用Implicit 授权类型,它在Redirection URI 之后的Redirection URI 之后的片段中呈现# 符号之后的Access Token。鉴于您直接在 User-Agent 中接收令牌,它本质上是一种不安全的方法,除了遵循 User-Agent 的安全规则外,您无能为力。

您可以将应用程序的使用限制为特定的用户代理,但很容易被篡改。您可以将您的令牌存储在 cookie 中,但如果 UA 不遵守通用安全规范,也可以访问该 cookie。如果代币由 UA 实施和提供,您可以将代币存储在本地存储中,但如果它遵守规范,则同样如此。

这些隐式间接授权的关键是对 UA 的信任;否则,最安全的授权类型是授权码,因为它需要在受控环境(应用程序的服务器)上安全可靠地存储秘密。

如果您别无选择,只能使用隐式调用,请大胆尝试并相信用户使用遵循安全协议的安全 UA;任何方式您都不会对用户对 UA 的错误选择负责。

【讨论】:

这曾经是最佳做法,但在 2019 年初更改了建议。此页面解释了为什么 developer.okta.com/blog/2019/05/01/…【参考方案5】:

您可以将加密令牌安全地存储在HttpOnly cookie 中。

https://medium.com/@sadnub/simple-and-secure-api-authentication-for-spas-e46bcea592ad

如果您担心长期存在的刷新令牌。您可以跳过存储它并且根本不使用它。只需将 Access Token 保存在内存中,并在 Access Token 过期时进行静默登录。

不要使用Implicit 流,因为它是obsolete。

SPA 最安全的身份验证方式是Authorization Code with PKCE。

一般来说,最好使用基于oidc-client 的现有库,而不是自己构建一些东西。

【讨论】:

1) 如果我采用内存方式,如果用户关闭浏览器选项卡,他/她的会话就会丢失,对吗? 2)内存中意味着只是将其存储为变量,是吗?那么它就不能通过浏览器的开发工具访问了,是吗? 1) 令牌不是会话 ID。它只是一个访问令牌。因此,如果他们丢失了令牌,他们只会重新获得它。 2)是的,内存中的意思是“变量”。我认为某些库还将access_token 存储在 sessionStorage 中,因此您也可以这样做。但是,它增加了对任何恶意脚本的可见性。 3)我相信,解决原始问题没有通用且安全的解决方案。如果您在客户端上有东西,那么任何浏览器扩展程序或注入的脚本都可以访问该数据。 do silent sign-in when Access Token expires 是什么意思? 当用户在页面上停留很长时间没有交互时,“访问令牌”过期。然后用户执行一些操作,API 以401 响应。通常,客户端上会保存一个“刷新令牌”。在得到 401 作为响应后,UI 应该使用“刷新令牌”刷新“访问令牌”。如果没有“刷新令牌”,则 UI 可以简单地重新验证用户并获取新的“访问令牌”。 取决于 AS,AS 可以根据与 AS 的会话 cookie 返回访问令牌而不要求提供凭据。因此,您将被重定向到 AS,它会立即使用代码重定向回来,并且应用程序会检索访问代码。浏览器中的用户不会看到它,但会注意到轻微的延迟,但您可以称之为“静默”。我想这就是弗拉基米尔的意思。【参考方案6】:

您没有使用最好的身份验证架构。 SPA 是一个公共客户端,它无法安全地存储诸如客户端密码或刷新令牌之类的信息。您应该切换到Implicit Flow,其中refresh tokens are not used。但是可以使用Silent Authentication(静默续订)代替。

我建议使用OIDC certified library,其中已为 SPA 应用程序排序。我最喜欢的一个:https://github.com/damienbod/angular-auth-oidc-client

【讨论】:

是的,我尝试走那条路,但是如果我询问如何使用获得的访问令牌来访问我自己的 REST 端点,我会以同样的 session 与 jwt 问题结束,所以它有点像在我看来毫无意义。一方面它作为灵丹妙药出售,另一方面他们说它只有在你没有后端的情况下才有效。现在告诉我一个除了待办事项列表之外没有后端的应用程序,即便如此...... 这也非常非常令人困惑。新的代码流并没有为我提供给定问题的真正解决方案(即如何访问我自己的 REST 资源)。不建议使用刷新令牌,尽管它确实适用于保护权衡。我敢打赌,大多数开发人员也不知道并将返回服务器会话,或者实施最简单的流程,无论他们提供的保护级别如何。 第三件事是他们将访问令牌存储在会话存储中,那么这对给定的论点有何影响……我真的很想理解,但那里的大多数论点只有一半意义.另一半只是巧妙地避开。但它并没有完全点击。 隐式流现在被认为是遗留的,它们推荐的替换是带有​​ PKCE 的 auth 代码,这让我们再次回到这个问题 甚至 autho 也要求 SPA 和公共客户端在客户端存储刷新令牌和 access_token 并启用 PKCE 和 refresh_token 轮换。 auth0.com/docs/libraries/auth0-react#getting-started 所以我认为现在如果您使用 PKCE 身份验证,将令牌存储在客户端上可能是一种公认​​的策略。

以上是关于在客户端的哪里存储刷新令牌?的主要内容,如果未能解决你的问题,请参考以下文章

在基于浏览器的应用程序中,我在哪里存储 OAuth 刷新令牌

为命令存储访问和刷新令牌

Google Api Php 客户端的刷新令牌

Symfony 在哪里存储和获取不记名令牌并做出反应?

jwt访问令牌存储在spring boot中的哪里?

在哪里存储以及如何在客户端维护来自 cosmos db 的延续令牌