我会尝试以通用的方式回答这个问题,以便该答案适用于各种框架、实现和语言,因为所有问题的答案都可以从一般协议或算法规范中得出。
应使用哪种OAuth 2.0授权类型?
这是要决定的第一件事。当涉及到SPA时,有两种可能的选择:
1.授权码授予(推荐,前提是客户端秘钥存储在服务器端)
2.资源所有者密码凭据授予
我没有提到隐式授权类型作为选项的原因是:
1.未通过提供客户端秘钥和授权码进行客户端认证步骤。所以安全性较低。
2.访问令牌作为URL片段发送回来(以便令牌不会发送到服务器),这将继续留在浏览器历史记录中。
3.如果发生XSS攻击,恶意脚本可以将令牌发送到攻击者控制下的远程服务器。
(此讨论排除了客户端凭据授权类型,因为它用于客户端非代表用户操作的情况。例如,批量作业)
在授权码授予类型的情况下,授权服务器通常是不同于资源服务器的服务器。最好将授权服务器保持分离,并将其用作组织内所有SPA的常见授权服务器。这始终是推荐的解决方案。
在授权码授予类型中,流程如下所示:
- 用户在SPA登录页面上点击登录按钮
- 用户将被重定向到授权服务器登录页面。客户端ID提供在URL查询参数中
- 用户输入他/她的凭据并单击登录按钮。用户名和密码将通过HTTP POST发送到授权服务器。凭证应该在请求正文或头中发送,而不是在URL中(因为URL在浏览器历史记录和应用程序服务器中被记录)。此外,应设置适当的缓存HTTP标头,以便不缓存凭据:
Cache-Control: no-cache, no-store
,Pragma: no-cache
,Expires: 0
- 授权服务器根据存储有随机盐的用户名和用户密码哈希(哈希算法如Argon2、PBKDF2、Bcrypt或Scrypt)对用户进行身份验证
- 在成功验证后,授权服务器将从其数据库检索针对URL查询参数中提供的客户端ID的重定向URL。重定向URL是资源服务器URL
- 然后,用户将被重定向到具有URL查询参数中的授权代码的资源服务器端点
- 资源服务器将然后对授权服务器进行HTTP POST请求以获取访问令牌。授权代码、客户端ID、客户端密钥应该在请求正文中。 (应使用适当的缓存标头,如上所述)
- 授权服务器将在响应正文或头中返回访问令牌和刷新令牌(带有上述适当的缓存标头)
- 资源服务器现在将通过设置适当的Cookie(将在下面详细解释)将用户重定向(HTTP响应代码302)到SPA URL
另一方面,对于资源所有者密码凭证授权类型,授权服务器和资源服务器是相同的。它更容易实现,并且如果符合要求和实施时间表,也可以使用。
此外,请参考我在
这里的答案,以获取有关资源所有者授权类型的进一步详细信息。
在SPA中,重要的是要注意,只有在调用适当的服务以确保请求中存在有效令牌后,才应启用所有受保护的路由。同样,受保护的API也应具有适当的过滤器来验证访问令牌。
为什么不应该将令牌存储在浏览器本地存储或会话存储中?
许多SPA确实将访问令牌和/或刷新令牌存储在浏览器本地存储或会话存储中。我认为我们不应该将令牌存储在这些浏览器存储中的原因是:
1. 如果发生XSS攻击,则恶意脚本可以轻松地从那里读取令牌并将其发送到远程服务器。从那时起,远程服务器或攻击者将没有问题模拟受害用户。
2. localstorage和sessionstorage不在子域之间共享。因此,如果我们在不同子域上运行两个SPA,则不会获得SSO功能,因为一个应用程序存储的令牌将无法在组织内的另一个应用程序中使用。
然而,如果令牌仍存储在这些浏览器存储之一中,则必须包含适当的指纹。指纹是一个具有密码学强度的随机字节字符串。原始字符串的Base64字符串将存储在以名称前缀__Secure-命名的HttpOnly、Secure、SameSite cookie中。需要适当的Domain和Path属性值。字符串的SHA256哈希值也将在JWT的声明中传递。因此,即使XSS攻击将JWT访问令牌发送到攻击者控制的远程服务器,它也无法发送cookie中的原始字符串,因此服务器可以根据cookie的缺失拒绝请求。此外,可以通过使用适当的content-security-policy响应头来进一步减轻XSS和脚本注入的影响。
注意:
SameSite=strict
确保给定的 cookie 不会随着来自其他站点(通过 AJAX 或跟踪超链接)的请求一起发送。简而言之,任何来自具有与目标站点相同的“注册域”名称的站点的请求都将被允许。例如,“http://www.example.com” 是站点名称,则注册域是“example.com”。有关详细信息,请参见下面最后一节中的参考资料 3。因此,它提供了一些防止 CSRF 的保护。但是,这也意味着如果 URL 所给出的是论坛,则经过身份验证的用户不能跟随该链接。如果这对应用程序来说是一个严重的限制,则可以使用 SameSite=lax
,它将允许跨站点请求,只要 HTTP 方法是安全的,即 GET、HEAD、OPTIONS 和 TRACE。由于 CSRF 基于不安全的方法,如 POST、PUT、DELETE,因此 lax
仍提供保护措施,以防止 CSRF。
为了允许在 "example.com" 的任何子域中通过所有请求传递 cookie,cookie 的 domain 属性应设置为 "example.com"
为什么我应该将访问令牌和/或刷新令牌存储在 cookie 中?
- 将令牌存储在cookie中时,我们可以将cookie设置为
secure
和httpOnly
。因此,如果发生XSS攻击,恶意脚本无法读取并将其发送到远程服务器。 XSS仍然可以从用户的浏览器冒充用户,但是如果关闭浏览器,则脚本无法造成更多损害。 secure
标志确保令牌不能通过不安全的连接发送- SSL / TLS是强制性的
- 在cookie中设置根域为
domain=example.com
,例如,可以确保所有子域都可以访问cookie。因此,组织内的不同应用程序和服务器可以使用相同的令牌。只需要登录一次
如何验证令牌?
令牌通常是JWT令牌。通常,令牌的内容不是机密的。因此,它们通常不加密。如果需要加密(也许因为令牌中还传递了一些敏感信息),则有一个单独的规范JWE。即使不需要加密,我们也需要确保令牌的完整性。没有人(用户或攻击者)应该能够修改令牌。如果他们这样做,服务器应该能够检测到并拒绝带有伪造令牌的所有请求。为了确保这种完整性,使用像HmacSHA256这样的算法对JWT令牌进行数字签名。为了生成此签名,需要一个秘密密钥。授权服务器将拥有和保护该秘密。每当调用授权服务器API以验证令牌时,授权服务器都会重新计算传递的令牌上的HMAC。如果它与输入的HMAC不匹配,则返回负面响应。 JWT令牌以Base64编码格式返回或存储。
然而,对于资源服务器上的每个API调用,授权服务器不参与验证令牌。资源服务器可以缓存授权服务器发出的令牌。资源服务器可以使用内存数据网格(如Redis)或者如果无法全部存储在RAM中,则使用基于LSM的数据库(如带有Level DB的Riak)来存储令牌。
对于每个API调用,资源服务器将检查其缓存。
- 如果访问令牌不在缓存中,则API应返回适当的响应消息和401响应代码,以便SPA可以将用户重定向到适当的页面,在该页面上要求用户重新登录。
- 如果访问令牌有效但已过期(请注意,JWT令牌通常包含用户名和到期日期等信息),则API应返回适当的响应消息和401响应代码,以便SPA可以调用适当的资源服务器API使用刷新令牌(具有适当的缓存标头)来更新访问令牌。然后,服务器将使用访问令牌、刷新令牌和客户端密钥调用授权服务器,授权服务器可以返回新的访问和刷新令牌,最终流向SPA(具有适当的缓存标头)。然后客户端需要重试原始请求。所有这些都将由系统处理,无需用户干预。可以创建一个单独的cookie来存储刷新令牌,类似于访问令牌,但具有适当的
Path
属性值,以便刷新令牌不随每个请求一起发送,而仅在更新请求中可用。
- 如果刷新令牌无效或已过期,则API应返回适当的响应消息和401响应代码,以便SPA可以将用户重定向到适当的页面,在该页面上要求用户重新登录。
为什么我们需要两个令牌-访问令牌和刷新令牌?
访问令牌通常具有短的有效期,例如30分钟。刷新令牌通常具有更长的有效期,例如6个月。如果访问令牌被攻击者获取,攻击者只能在访问令牌有效期内冒充受害用户。由于攻击者没有客户端密钥,因此无法请求授权服务器获取新的访问令牌。然而,攻击者可以请求资源服务器进行令牌更新(如上所述的设置中,更新请求通过资源服务器进行,以避免在浏览器中存储客户端密钥),但考虑到其他采取的步骤,这是不太可能的,而且服务器可以根据IP地址采取其他保护措施。
如果访问令牌的短暂有效期有助于授权服务器根据需要撤销已发出的令牌。授权服务器还可以维护已发出令牌的缓存。系统管理员可以标记某些用户的令牌已撤销,如果需要的话。在访问令牌过期时,当资源服务器访问授权服务器时,用户将被强制重新登录。
CSRF呢?
为了保护用户免受CSRF攻击,我们可以采用类似Angular框架的方法(在Angular HttpClient
documentation中有解释),即服务器必须发送一个非HttpOnly cookie(也就是可读取的cookie),其中包含该特定会话的唯一不可预测值。它应该是一个具有密码强度的随机值。然后客户端将始终读取cookie并在自定义HTTP头中发送该值(除了不应具有任何状态更改逻辑的GET和HEAD请求之外。注意,由于同源策略,CSRF无法从目标Web应用程序读取任何内容),以便服务器可以验证来自头和cookie的值。由于跨域表单无法读取cookie或设置自定义头,在CSRF请求的情况下,自定义头值将缺失,服务器将能够检测到攻击。
为了保护应用程序免受登录CSRF攻击,请始终检查
referer
头,并仅在
referer
是受信任的域时接受请求。如果
referer
头不存在或者是非白名单域,则简单地拒绝该请求。当使用SSL/TLS时,
referrer
通常存在。着陆页面(主要是信息性的,不包含登录表单或任何安全内容)可能会放松,允许缺少
referer
头的请求。
服务器应该阻止
TRACE
HTTP方法,因为它可以用来读取
httpOnly
cookie。此外,还应设置头文件
Strict-Transport-Security: max-age=<expire-time>; includeSubDomains
,以仅允许安全连接,以防止中间人覆盖子域名的CSRF cookies。另外,应使用上述提到的
SameSite
设置。
状态变量(Auth0使用它)-客户端将生成并传递一个具有密码强度的随机nonce,服务器将在其响应中回显该nonce,以允许客户端验证nonce。这在
Auth0 doc中有解释。
最后,SSL/TLS对于所有通信都是强制性的 - 截至今天,PCI/DSS合规性不接受低于1.1版本的TLS。应使用适当的密码套件以确保前向保密和身份验证加密。此外,访问和刷新令牌应在用户明确单击“注销”时立即列入黑名单,以防止任何令牌误用的可能性。
参考资料
- RFC 6749 - OAuth2.0
- OWASP JWT Cheat Sheet
- SameSite Cookie IETF Draft
- Cookie Prefixes
- RFC 6265 - Cookie
CreateFreshApiToken
对我无效。无法撤销这些令牌...正在寻找一个涵盖所有上述点并能良好、协调地一起工作的解决方案。 - Wonka