授权码 + PKCE 模式|OIDC & OAuth2.0 认证协议最佳实践系列【03】


在上一篇文章中,我们介绍了 OIDC 授权码模式(点击下方图片查看),本次我们将重点围绕 授权码 + PKCE 模式(Authorization Code With PKCE)进行介绍 ,从而让你的系统快速具备接入用户认证的标准体系。
为什么会有 PKCE 模式:
PKCE 是 Proof Key for Code Exchange 的缩写,PKCE 是一种用于增强授权码模式安全性的方法,它可以防止恶意应用程序通过截获授权码和重定向 URI 来获得访问令牌。PKCE 通过将随机字符串(code_verifier)和其 SHA-256 哈希值(code_challenge)与授权请求一起发送,确保访问令牌只能由具有相应 code_verifier 的应用程序使用,保障用户的安全性。
【OAuth 2.0 协议扩展】PKCE 扩展协议:为了解决公开客户端的授权安全问题
「面向对象」public 客户端,其本身没有能力保存密钥信息(恶意攻击者可以通过反编译等手段查看到客户端的密钥 client_secret, 也就可以通过授权码 code 换取 access_token, 到这一步,恶意应用就可以拿着 token 请求资源服务器了)
「原理」PKCE 协议本身是对 OAuth 2.0 的扩展, 它和之前的授权码流程大体上是一致的, 区别在于在向授权服务器的 authorize endpoint 请求时,需要额外的 code_challenge 和 code_challenge_method 参数;向 token endpoint 请求时, 需要额外的 code_verifier 参数。最后授权服务器会对这三个参数进行对比验证, 通过后颁发令牌。
01.授权码 + PKCE 模式(Authorization Code With PKCE)
如果你的应用是一个 SPA 前端应用或移动端 App,建议使用授权码 + PKCE 模式来完成用户的认证和授权。授权码 + PKCE 模式适合不能安全存储密钥的场景(例如前端浏览器)
我们解释下 code_verifier 和 code_challenge
对于每一个 OAuth/OIDC 请求,客户端会先创建一个代码验证器 code_verifier
code_verifier:在 [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" 范围内,生成 43-128 位的随机字符串。
code_challenge:则是对 code_verifier 通过 code_challenge_method 例如 sha256 转换得来的。
用大白话讲下就是在认证是用户携带的是加密后的 code_challenge ,在用户认证成功通过 code 获取 Token 时,客户端证明自己的方式则是把 code_verifier 原文发送,认证中心收到获取 Token 请求时通过 code_verifier + code_challenge_method 进行转换,发现最终结果与 code_challenge 匹配则返回 Token ,否则拒绝。
1.1 整体流程
整体上,有以下流程:
1.用户点击登录。
2.在你的应用中,生成 code_verifier 和 code_challenge。
3. 拼接登录链接(包含 code_challenge ) 跳转到 Authing 请求认证。
4. Authing 发现用户没有登录,重定向到认证页面,要求用户完成认证。
5. 用户在浏览器完成认证。
6. Authing 服务器通过浏览器通过重定向将授权码(code)发送到你的应用前端。
7. 你的应用将授权码 (code) 和 code_verifier 发送到 Authing 请求获取 Token.
8. Authing 校验 code、code_verifier 和 code_challenge。
9. 校验通过,Authing 则返回 AccessToken 和 IdToken 以及可选的 RefreshToken。
10. 你的应用现在知道了用户的身份,后续使用 AccessToken 换取用户信息,调用资源方的 API 等

1.2 准备接入
1.2.1 整体流程
需要先在 Authing 创建应用:

配置登录回调和登出回调,配置为你实际项目的地址,我们在这里配置 localhost 用于测试。
若你想匹配多个登录/登出回调
可以使用 ‘*’ 号进行通配,登录/登出回调可以是如下格式


在协议配置中,我们勾选 authorization_code 并且使用 code 作为返回类型,如下图所示:
PKCE 模式使用的是 code_verifier 来换取 Token ,所以需要配置获取 Token 的方式为 null

1.3 接入测试
1.3.1 所需调用接口列表
GET ${host}/oidc/auth 发起登录(拼接你的发起登录地址)
POST ${host}/oidc/token 获取 Token
GET ${host}/oidc/me 获取用户信息
POST ${host}/oidc/token/introspection 校验 Token
POST ${host}/oidc/token 刷新 Token
POST ${host}/oidc/revocation 吊销 Token
GET ${host}/session/end 登出
1.3.2 Run in Postman所需调用接口列表
1.3.3 发起登录
GET ${host}/oidc/auth
这是基于浏览器的 OIDC 的起点,请求对用户进行身份验证,并会在验证成功后返回授权码到您所指定的 redirect_uri。
生成 code_challenge 和 code_verifier
在线生成
离线生成
首先,我们要生成一个 code_challenge 和 code_verifier,以下是使用 JavaScript 语言生成 PKCE 所需要的 code_verifier 和 code_challenge 的脚本:
以上代码使用 jsSHA 库计算 SHA-256 哈希值,使用 base64url 编码将哈希值转换为 code_challenge。你可以将以上代码复制到你的 JavaScript 代码中,并使用 pkce.codeVerifier 和 pkce.codeChallenge 调用 OAuth 2.0 授权请求。
举例
code_verifier 的长度为 43 ~ 128 ,我们生成的是 128 位
发起登录地址(浏览器中打开)
体验地址
参数说明

1.3.4 获取 Token
用户在 Authing 侧完成登录操作后, Authing 会将生成的 code 作为参数回调到 redirect_uri 地址,此时通过 code 换 token 接口即可拿到对应的访问令牌 access_token
请求参数

请求示例
响应示例(成功)
响应示例(失败)
1.3.5 所需调用接口列表
此端点是 OIDC 获取用户端点,可以通过 AccessToken 获取有关当前登录用户的信息。
请求参数

请求示例
响应示例(成功)
响应示例(失败)
1.3.6 校验 Token
此端点接受 access_token、id_token、refresh_token ,并返回一个布尔值,指示它是否处于活动状态。如果令牌处于活动状态,还将返回有关令牌的其他数据。如果 token 无效、过期或被吊销,则认为它处于非活动状态。
access_token 可以使用 RS256 签名算法或 HS256 签名算法进行签名。下面是这两种签名算法的区别:
RS256 是使用 RSA 算法的一种数字签名算法,它使用公钥/私钥对来加密和验证信息。RS256 签名生成的令牌比 HS256 签名生成的令牌更加安全,因为使用 RSA 密钥对进行签名可以提供更高的保护级别。使用 RS256 签名算法的令牌可以使用公钥进行验证,公钥可以通过 JWK 端点获取。
HS256 是使用对称密钥的一种数字签名算法。它使用同一个密钥进行签名和验证。HS256 签名算法在性能方面比 RS256 签名算法更快,因为它使用的是对称密钥,而不是使用 RSA 公钥/私钥对来签名和验证。使用 HS256 签名算法的令牌可以通过 shared secret (应用密钥)进行验证。
在实际应用中,RS256 算法更加安全,但同时也更加消耗资源,如果系统需要高性能,可以选择 HS256 签名算法。
验证 Token 分为两种方式
本地验证与使用 Authing 在线验证。我们建议在本地验证 JWT Token,因为可以节省你的服务器带宽并加快验证速度。你也可以选择将 Token 发送到 Authing 的验证接口由 Authing 进行验证并返回结果,但这样会造成网络延迟,而且在网络拥塞时可能会有慢速请求。
以下是本地验证和在线验证的优劣对比:

在线校验
需要注意的是,id_token 目前无法在线校验,因为 id_token 只是一个标识,若需要校验 id_token 则需要您在离线自行校验
请求参数

请求示例
校验 access_token 响应示例(校验通过)
校验 access_token 响应示例(校验未通过)
校验 refresh_token 响应示例 (校验通过)
校验 refresh_token 响应示例(校验未通过)
离线校验
可参考文档(Authing 开发者文档):
我们简单说下,若您使用离线校验应该对 token 进行如下规则的校验
1.格式校验 - 校验 token 格式是否是 JWT 格式
2.类型校验 - 校验 token 是否是目标 token 类型,比如 access_token 、id_token、refresh_token
3.issuer 校验 - 校验 token 是否为信赖的 issuer 颁发
4.签名校验 - 校验 token 签名是否由 issuer 签发,防止伪造
5.有效期校验 - 校验 token 是否在有效期内
6.claims 校验 - 是否符合与预期的一致
示例代码
下面是一个示例 Java 代码,可以用于在本地校验 OIDC RS256 和 HS256 签发的 access_token
这段代码使用 Nimbus JOSE+JWT 库来解析和验证 JWT token。它使用指定的 issuer 和 audience 值对 access_token 进行验证,并验证 JWT 中 claims 的格式、类型、签名、有效期和 issuer。如果发生任何验证错误,则将抛出 RuntimeException。使用时需要传入对应的 JWK URL 和 access_token 进行调用,例如:
这个示例只校验了 RS256 和 HS256 签名算法。
1.3.7 刷新 Token
此功能用于用户 token 的刷新操作,在 token 获取阶段需要先获取到 refresh_token 。
请求参数

请求参数
响应示例(成功)
响应示例(失败)
1.3.8 撤回 Token
撤销 access_token / refresh_token 。
请求参数

请求示例
响应示例(成功)
响应示例(失败)
1.3.9 用户登出
使用此操作通过删除用户的浏览器会话来注销用户。
post_logout_redirect_uri 可以指定在执行注销后重定向的地址。否则,浏览器将重定向到默认页面
请求参数

请求示例(浏览器访问)
02.本章总结
本章我们介绍了 OIDC 授权码模式的接入流程以及相关接口的调用方式,对于小白来说可能需要整体跑一遍流程才能熟悉,我们也建议你 fork 我们的 postman collection 跑一遍流程,对 PKCE 模式你就基本掌握啦。