惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
D
Darknet – Hacking Tools, Hacker News & Cyber Security
S
Schneier on Security
The Last Watchdog
The Last Watchdog
Cyberwarzone
Cyberwarzone
S
Securelist
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
Cyber Attacks, Cyber Crime and Cyber Security
L
Lohrmann on Cybersecurity
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
The Cloudflare Blog
V
V2EX
博客园_首页
博客园 - 聂微东
Vercel News
Vercel News
人人都是产品经理
人人都是产品经理
G
GRAHAM CLULEY
T
Tenable Blog
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
L
LINUX DO - 最新话题
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
SecWiki News
SecWiki News
博客园 - 三生石上(FineUI控件)
S
Secure Thoughts
N
News | PayPal Newsroom
T
The Blog of Author Tim Ferriss
The GitHub Blog
The GitHub Blog
T
Troy Hunt's Blog
博客园 - 【当耐特】
Forbes - Security
Forbes - Security
H
Hacker News: Front Page
A
About on SuperTechFans
B
Blog RSS Feed
Engineering at Meta
Engineering at Meta
MongoDB | Blog
MongoDB | Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
罗磊的独立博客
D
DataBreaches.Net
P
Privacy & Cybersecurity Law Blog
Schneier on Security
Schneier on Security
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Jina AI
Jina AI
D
Docker
P
Proofpoint News Feed

OAuth

OIDC & OAuth2.0 协议及其授权模式详解|认证协议最佳实践系列 [1] - V2EX OIDC & OAuth2.0 认证协议最佳实践系列 02 - 授权码模式(Authorization Code)接入 Authing - V2EX Java 小小写个开源 OAuth 客户端工具 - V2EX 腾讯的网页授权真难申请,微博秒搞定! - V2EX 关于 native 应用程序在使用 OAuth 2.0 的一些问题 如何创建一个 OAuth 服务 - V2EX 在oauth1.0中签名的话必须有consumer_key 与consumer_secret ,这样的话在桌面应用中岂不是这两个都给暴漏了 OAuth.io - V2EX OAuth 2.0 - V2EX weibo的OAuth问题 - V2EX 腾讯微博的OAuth问题...... - V2EX 搞定 OAuth 的感觉实在是太爽了 - V2EX img.ly API
授权码 + PKCE 模式| OIDC & OAuth2.0 认证协议最佳实践系列 [03] - V2EX
Authing · 2023-05-04 · via OAuth

图片 在上一篇文章中,我们介绍了 OIDC 授权码模式,本次我们将重点围绕 授权码 + PKCE 模式( Authorization Code With PKCE )进行介绍 ,从而让你的系统快速具备接入用户认证的标准体系。OIDC & OAuth2.0 认证协议最佳实践系列 02 - 授权码模式( Authorization Code )接入 Authing 图片

为什么会有 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 获取 
TokenGET${host}/oidc/me 获取用户信息
POST${host}/oidc/token/introspection 校验 
TokenPOST${host}/oidc/token 刷新 
TokenPOST${host}/oidc/revocation 吊销 
TokenGET${host}/session/end 登出

1.3.2 Run in Postman 所需调用接口列表

https://app.getpostman.com/run-collection/24730905-5d29e488-719e-4ffe-af21-a7c18298d328?action=collection%2Ffork&collection-url=entityId%3D24730905-5d29e488-719e-4ffe-af21-a7c18298d328%26entityType%3Dcollection%26workspaceId%3D13ff793c-024c-459d-b1f6-87e91c4769ed#env%5BAuthing%20OIDC%5D=W3sia2V5IjoiaG9zdCIsInZhbHVlIjoiaHR0cHM6Ly9kZWVwbGFuZy5hdXRoaW5nLmNuIiwiZW5hYmxlZCI6dHJ1ZSwidHlwZSI6ImRlZmF1bHQifSx7ImtleSI6ImNsaWVudF9pZCIsInZhbHVlIjoiNjM4MmNmNDg2ZTVhNjk0NDNhZjI5NzFiIiwiZW5hYmxlZCI6dHJ1ZSwidHlwZSI6ImRlZmF1bHQifSx7ImtleSI6ImNsaWVudF9zZWNyZXQiLCJ2YWx1ZSI6Ijc3NWMyM2NlMjkwYzkwZDQwNDUxNGU3MDgyMDkzZWIzIiwiZW5hYmxlZCI6dHJ1ZSwidHlwZSI6ImRlZmF1bHQifSx7ImtleSI6ImFjY2Vzc190b2tlbiIsInZhbHVlIjoiIiwiZW5hYmxlZCI6dHJ1ZSwidHlwZSI6ImRlZmF1bHQifSx7ImtleSI6ImlkX3Rva2VuIiwidmFsdWUiOiIiLCJlbmFibGVkIjp0cnVlLCJ0eXBlIjoiZGVmYXVsdCJ9LHsia2V5IjoicmVmcmVzaF90b2tlbiIsInZhbHVlIjoiIiwiZW5hYmxlZCI6dHJ1ZSwidHlwZSI6ImRlZmF1bHQifV0=

1.3.3 发起登录

GET${host}/oidc/auth

这是基于浏览器的 OIDC 的起点,请求对用户进行身份验证,并会在验证成功后返回授权码到您所指定的 redirect_uri 。

生成 code_challenge 和 code_verifier

在线生成 https://tonyxu-io.github.io/pkce-generator/

离线生成 首先,我们要生成一个 code_challenge 和 code_verifier,以下是使用 JavaScript 语言生成 PKCE 所需要的 code_verifier 和 code_challenge 的脚本:

// 生成随机字符串
function generateRandomString(length) {
  var result = '';
  var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  var charactersLength = characters.length;
  for (var i = 0; i < length; i++) {
    result += characters.charAt(Math.floor(Math.random() * charactersLength));
  }
  return result;
}
// 生成 code_verifier
var codeVerifier = generateRandomString(128);
// 对 code_verifier 进行 SHA-256 编码,并将其转换为 base64url 格式的 code_challenge
var sha256 = new jsSHA("SHA-256", "TEXT");
sha256.update(codeVerifier);
var codeChallenge = btoa(String.fromCharCode.apply(null, new Uint8Array(sha256.getHash("ARRAYBUFFER"))))
  .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
// 将 code_verifier 和 code_challenge 用对象形式返回
var pkce = {
  codeVerifier: codeVerifier,
  codeChallenge: codeChallenge
};

以上代码使用 jsSHA 库计算 SHA-256 哈希值,使用 base64url 编码将哈希值转换为 code_challenge 。你可以将以上代码复制到你的 JavaScript 代码中,并使用 pkce.codeVerifier 和 pkce.codeChallenge 调用 OAuth 2.0 授权请求。

举例

code_verifier:4aHg5fN1AGdbnBAfVKMf9ZMK4PUOBTwQSKKk9V8wYXOFYDZklMl7dzDUhnQi4sYhzGb6PWCkNQqLP70K1DNOneEDq8iyASepAdGjGBBmCs4BGCDDJNwLrGpnJEfmrI66

code_verifier 的长度为 43 ~ 128 ,我们生成的是 128 位

code_challenge:OhMk95M9qWkKd06--utVtRzQh8Y0Qtqo4cPqqzMJyMw

发起登录地址(浏览器中打开) https://{host}/oidc/auth?scope=openid+profile+offline_access+username+email+phone&redirect_uri=http://localhost:8080/&response_type=code&prompt=consent&nonce=6e187def-1a19-4067-8875-653f024d5a9f&client_id={client_id}&state=1676881862&code_challenge={code_challenge}&code_challenge_method=S256

体验地址 https://oidc-authorizationcode-withpkce.authing.cn/oidc/auth?scope=openid+profile+offline_access+username+email+phone&redirect_uri=http://localhost:8080/&response_type=code&prompt=consent&nonce=6e187def-1a19-4067-8875-653f024d5a9f&client_id=63f30f5bf629268cc27d93c6&state=1676881862&code_challenge=OhMk95M9qWkKd06--utVtRzQh8Y0Qtqo4cPqqzMJyMw&code_challenge_method=S256

参数说明 图片

1.3.4 获取 Token

POST${host}/oidc/token 用户在 Authing 侧完成登录操作后,Authing 会将生成的 code 作为参数回调到 redirect_uri 地址,此时通过 code 换 token 接口即可拿到对应的访问令牌 access_token

请求参数 图片 请求示例

curl --location --request POST 'https://{host}/oidc/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id={应用 ID}' \
--data-urlencode 'client_secret={应用密钥}' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'redirect_uri={发起登录时指定的 redirect_uri}' \
--data-urlencode 'code={/oidc/auth 返回的 code}' \
--data-urlencode 'code_verifier={code_verifier}' 

响应示例(成功)

{
"scope": "openid username email phone offline_access profile",
"token_type": "Bearer",
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImVtSzBGbVRIa0xlQWFjeS1YWEpVT3J6SzkxV243TkdoNGFlYUVlSjVQOUUifQ.eyJzdWIiOiI2M2ViNTNjNDQxYTVjMmYwNWYyNGJiMDMiLCJhdWQiOiI2M2ViNDU4NTE1NmQ5NzcxMDFkZDM3NTAiLCJzY29wZSI6Im9wZW5pZCB1c2VybmFtZSBlbWFpbCBwaG9uZSBvZmZsaW5lX2FjY2VzcyBwcm9maWxlIiwiaWF0IjoxNjc2MzY2OTE0LCJleHAiOjE2Nzc1NzY1MTQsImp0aSI6ImVmVU04enNrbl92LXYzeXZfbDVHRV9fQ2JEY0NNZDhEVDFnYVI0bHRqcHAiLCJpc3MiOiJodHRwczovL29pZGMtYXV0aG9yaXphdGlvbi1jb2RlLmF1dGhpbmcuY24vb2lkYyJ9.E3gAYzCQbJmrtM5zl91OPHm2YPnDxzRejw75oVMF1tLqCS0trj6CSBxyxP3Z9t6Eb_oAu1f_3I6XC3KC-l0DTM6q7_R2rnW4LWlik2rDCLuGpG0FqFScLZhwafmrPsVn93yaBQfEEoaLviqKhj3DgOymKqHZzFG3taaz2k_pWsxt4z97DtKjRTiqyMvcSfHsVrjSKELaC-5S_PHPWcQ70iX85IwUb6i5ldZGxYmODCvChNC9p4D4IOT3atvyEHgBTmjA9ZKI-T7hCVHSO91WZY3l1p4iWdi6KdP1oMGTy8WbmUHG9SiWO1Efh_9I5ZpRzVNWXINLv-lZ0d2aZKjg2w",
"expires_in": 1209600,
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2M2ViNTNjNDQxYTVjMmYwNWYyNGJiMDMiLCJhdWQiOiI2M2ViNDU4NTE1NmQ5NzcxMDFkZDM3NTAiLCJpYXQiOjE2NzYzNjY5MTQsImV4cCI6MTY3NzU3NjUxNCwiaXNzIjoiaHR0cHM6Ly9vaWRjLWF1dGhvcml6YXRpb24tY29kZS5hdXRoaW5nLmNuL29pZGMiLCJub25jZSI6IjhiYjg3MjdhLWU1MGUtNDUzOC05ZmZmLWZhOTFlNWQ0Y2MwYSIsIm5hbWUiOm51bGwsImdpdmVuX25hbWUiOm51bGwsIm1pZGRsZV9uYW1lIjpudWxsLCJmYW1pbHlfbmFtZSI6bnVsbCwibmlja25hbWUiOm51bGwsInByZWZlcnJlZF91c2VybmFtZSI6bnVsbCwicHJvZmlsZSI6bnVsbCwicGljdHVyZSI6Imh0dHBzOi8vZmlsZXMuYXV0aGluZy5jby9hdXRoaW5nLWNvbnNvbGUvZGVmYXVsdC11c2VyLWF2YXRhci5wbmciLCJ3ZWJzaXRlIjpudWxsLCJiaXJ0aGRhdGUiOm51bGwsImdlbmRlciI6IlUiLCJ6b25laW5mbyI6bnVsbCwibG9jYWxlIjpudWxsLCJ1cGRhdGVkX2F0IjoiMjAyMy0wMi0xNFQwOToyNjoyOC4wNjhaIiwiZW1haWwiOm51bGwsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicGhvbmVfbnVtYmVyIjoiMTg1MTY4Mjk5OTUiLCJwaG9uZV9udW1iZXJfdmVyaWZpZWQiOnRydWUsInVzZXJuYW1lIjpudWxsfQ.GweoWBCEyHQGP6G9ohbfBMUMALlbZMM9hRAes1De7BM",
"refresh_token": "KanvCEmonS_FgCRdFftOCwka2f8Qjj4tcsIfJF-VC1W"
} 

响应示例(失败)

{
 "error": "invalid_grant",
 "error_description": "授权码无效或已过期"
}

1.3.5 所需调用接口列表

GET${host}/oidc/me 获取用户信息 此端点是 OIDC 获取用户端点,可以通过 AccessToken 获取有关当前登录用户的信息。

请求参数 图片 请求示例

curl --location --request GET 'https://{host}/oidc/me?access_token={access_token}' 

响应示例(成功)

{
 "name": null,
 "given_name": null,
 "middle_name": null,
 "family_name": null,
 "nickname": null,
 "preferred_username": null,
 "profile": null,
 "picture": "https://files.authing.co/authing-console/default-user-avatar.png",
 "website": null,
 "birthdate": null,
 "gender": "U",
 "zoneinfo": null,
 "locale": null,
 "updated_at": "2023-02-14T09:26:28.068Z",
 "email": "[email protected]",
 "email_verified": true,
 "phone_number": "185xxxx9995",
 "phone_number_verified": true,
 "username": "neo",
 "sub": "63eb53c441a5c2f05f24bb03"
}

响应示例(失败)

{
 "error": "invalid_grant",
 "error_description": "Access Token 无效"
}

1.3.6 校验 Token

POST${host}/oidc/auth 此端点接受 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 则需要您在离线自行校验

请求参数 图片

请求示例

curl --location --request POST 'https://{host}/oidc/token/introspection' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id={应用 ID}' \
--data-urlencode 'client_secret={应用密钥}' \
--data-urlencode 'token={ token }' \
--data-urlencode 'token_type_hint={token_type_hint}'

校验 access_token 响应示例(校验通过)

{
    "active": true,
    "sub": "63eb53c441a5c2f05f24bb03",
    "client_id": "63eb4585156d977101dd3750",
    "exp": 1677648467,
    "iat": 1676438867,
    "iss": "https://oidc-authorization-code.authing.cn/oidc",
    "jti": "ObgavGBUocr1wsrUvtDLHmuFSgoebxsiOY4JNRqIhaQ",
    "scope": "offline_access username profile openid phone email",
    "token_type": "Bearer"
}

校验 access_token 响应示例(校验未通过)

{
"active": false
}

校验 refresh_token 响应示例 (校验通过)

{
    "active": true,
    "sub": "63eb53c441a5c2f05f24bb03",
    "client_id": "63eb4585156d977101dd3750",
    "exp": 1679030867,
    "iat": 1676438867,
    "iss": "https://oidc-authorization-code.authing.cn/oidc",
    "jti": "6F2TO1v1YZ1_N7I3jXYHjK-vZzKtlD0IiP5KPoUFUCQ",
    "scope": "offline_access username profile openid phone email"
}

校验 refresh_token 响应示例(校验未通过)

{
    "active": false
}

离线校验 可参考文档( Authing 开发者文档): https://docs.authing.cn/v2/guides/faqs/how-to-validate-user-token.html#%E6%9C%AC%E5%9C%B0%E9%AA%8C%E8%AF%81

我们简单说下,若您使用离线校验应该对 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

import com.nimbusds.jose.JWSObject;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import java.net.URL;
import java.text.ParseException;
import java.util.Date;
public class OIDCValidator {
    private static final String ISSUER = "https://your-issuer.com";
    private static final String AUDIENCE = "your-client-id";
    private final URL jwkUrl;
    public OIDCValidator(final URL jwkUrl) {
        this.jwkUrl = jwkUrl;
    }
    public JWTClaimsSet validateToken(final String accessToken) throws ParseException {
        final SignedJWT signedJWT = SignedJWT.parse(accessToken);
        if (signedJWT == null) {
            throw new RuntimeException("Access token is null or empty");
        }
        final JWTClaimsSet claims = signedJWT.getJWTClaimsSet();
        if (claims == null) {
            throw new RuntimeException("No claims present in the access token");
        }
        if (!claims.getIssuer().equals(ISSUER)) {
            throw new RuntimeException("Invalid issuer in access token");
        }
        if (!claims.getAudience().contains(AUDIENCE)) {
            throw new RuntimeException("Invalid audience in access token");
        }
        final JWSObject jwsObject = signedJWT.getJWSObject();
        if (jwsObject == null) {
            throw new RuntimeException("No JWS object found in the access token");
        }
        // Fetch the JWKs from the JWK set URL
        final JWKSet jwkSet = JWKSet.load(jwkUrl);
        final JWK jwk = jwkSet.getKeyByKeyId(jwsObject.getHeader().getKeyID());
        if (jwk == null) {
            throw new RuntimeException("No JWK found for the access token");
        }
        if (!jwsObject.verify(jwk.getKey())) {
            throw new RuntimeException("Invalid signature in access token");
        }
        if (claims.getExpirationTime() == null || claims.getExpirationTime().before(new Date())) {
            throw new RuntimeException("Expired access token");
        }
        return claims;
    }
}

这段代码使用 Nimbus JOSE+JWT 库来解析和验证 JWT token 。它使用指定的 issuer 和 audience 值对 access_token 进行验证,并验证 JWT 中 claims 的格式、类型、签名、有效期和 issuer 。如果发生任何验证错误,则将抛出 RuntimeException 。使用时需要传入对应的 JWK URL 和 access_token 进行调用,例如:

final URL jwkUrl = new URL("https://your-issuer.com/.well-known/jwks.json");
final OIDCValidator validator = new OIDCValidator(jwkUrl);
final String accessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
final JWTClaimsSet claims = validator.validateToken(accessToken);

这个示例只校验了 RS256 和 HS256 签名算法。

1.3.7 刷新 Token

POST${host}/oidc/token 此功能用于用户 token 的刷新操作,在 token 获取阶段需要先获取到 refresh_token 。

请求参数 图片 请求参数

curl --location --request POST 'https://{host}/oidc/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id={应用 ID}' \
--data-urlencode 'client_secret={应用密钥}' \
--data-urlencode 'refresh_token={刷新令牌}' \
--data-urlencode 'grant_type=refresh_token'

响应示例(成功)

{
    "refresh_token": "6F2TO1v1YZ1_N7I3jXYHjK-vZzKtlD0IiP5KPoUFUCQ",
    "scope": "offline_access username profile openid phone email",
    "token_type": "Bearer",
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImVtSzBGbVRIa0xlQWFjeS1YWEpVT3J6SzkxV243TkdoNGFlYUVlSjVQOUUifQ.eyJzdWIiOiI2M2ViNTNjNDQxYTVjMmYwNWYyNGJiMDMiLCJhdWQiOiI2M2ViNDU4NTE1NmQ5NzcxMDFkZDM3NTAiLCJzY29wZSI6Im9mZmxpbmVfYWNjZXNzIHVzZXJuYW1lIHByb2ZpbGUgb3BlbmlkIHBob25lIGVtYWlsIiwiaWF0IjoxNjc2NDQ0MjY4LCJleHAiOjE2Nzc2NTM4NjgsImp0aSI6IkEtZUlQYkJ5N3lJLTliUmp1RnJHeXNCSXdjbWtOUl9WalpYODB2aU05VFkiLCJpc3MiOiJodHRwczovL29pZGMtYXV0aG9yaXphdGlvbi1jb2RlLmF1dGhpbmcuY24vb2lkYyJ9.Kk3jSK5BSUEDVTQMdMAdG5cBCxZt31vQiD-XYHNA84Gd3Mo8eDLcQpjMEzQ8HJ4_b9IgMOz5ydXz0zAQ6AjLMW3Rl49qhTGDB7Kq7tHTFmDO8itoO2LQTCLPCPtP3TkoOgptlFD_sd32nefH-HojNhuqwKw469Byw3xnW5xEs3wSuOoUdHwR2n9j1T1Zgp3e90xmBjbtbofQE1z0IWtCnrfJ9ujWsKXoN_7OAXbCTa-Ak_DhgLHU7xutQaaBOgD28lLLT5xclgBWfv7Leyx_kBnVGT5Jvo1tfA6AUEp6wJO4GUBzsijLefI04VDzBGypNuFJlw_jOhSp-SWxJjQSwQ",
    "expires_in": 1209600,
    "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2M2ViNTNjNDQxYTVjMmYwNWYyNGJiMDMiLCJhdWQiOiI2M2ViNDU4NTE1NmQ5NzcxMDFkZDM3NTAiLCJpYXQiOjE2NzY0NDQyNjgsImV4cCI6MTY3NzY1Mzg2OCwiaXNzIjoiaHR0cHM6Ly9vaWRjLWF1dGhvcml6YXRpb24tY29kZS5hdXRoaW5nLmNuL29pZGMiLCJuYW1lIjpudWxsLCJnaXZlbl9uYW1lIjpudWxsLCJtaWRkbGVfbmFtZSI6bnVsbCwiZmFtaWx5X25hbWUiOm51bGwsIm5pY2tuYW1lIjpudWxsLCJwcmVmZXJyZWRfdXNlcm5hbWUiOm51bGwsInByb2ZpbGUiOm51bGwsInBpY3R1cmUiOiJodHRwczovL2ZpbGVzLmF1dGhpbmcuY28vYXV0aGluZy1jb25zb2xlL2RlZmF1bHQtdXNlci1hdmF0YXIucG5nIiwid2Vic2l0ZSI6bnVsbCwiYmlydGhkYXRlIjpudWxsLCJnZW5kZXIiOiJVIiwiem9uZWluZm8iOm51bGwsImxvY2FsZSI6bnVsbCwidXBkYXRlZF9hdCI6IjIwMjMtMDItMTRUMDk6MjY6MjguMDY4WiIsImVtYWlsIjpudWxsLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInBob25lX251bWJlciI6IjE4NTE2ODI5OTk1IiwicGhvbmVfbnVtYmVyX3ZlcmlmaWVkIjp0cnVlLCJ1c2VybmFtZSI6bnVsbH0.DGoJrzkgti44zw-MotVM1KpLxbJTzc5pfh-xYun_xDQ"
}

响应示例(失败)

{
    "error": "invalid_grant",
    "error_description": "Refresh Token 无效或已过期"
}

1.3.8 撤回 Token

POST${host}/oidc/auth

撤销 access_token / refresh_token 。

请求参数 图片 请求示例

curl --location --request POST 'https://{host}/oidc/token/revocation' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id={应用 ID}' \
--data-urlencode 'client_secret={应用密钥}' \
--data-urlencode 'token= {token}' \
--data-urlencode 'token_type_hint={token_type_hint}'

响应示例(成功)

HTTP 200 OK

响应示例(失败)

{
    "error": "xxxx",
    "error_description": "xxxx"
}

1.3.9 用户登出

GET${host}/oidc/session/end 使用此操作通过删除用户的浏览器会话来注销用户。 post_logout_redirect_uri 可以指定在执行注销后重定向的地址。否则,浏览器将重定向到默认页面 请求参数 图片 请求示例(浏览器访问)

GET https://oidc-authorization-code.authing.cn/oidc/session/end?id_token_hint={id_token}&post_logout_redirect_uri=http://localhost:8080/&state=1676452381

02.本章总结

本章我们介绍了 OIDC 授权码模式的接入流程以及相关接口的调用方式,对于小白来说可能需要整体跑一遍流程才能熟悉,我们也建议你 fork 我们的 postman collection 跑一遍流程,对 PKCE 模式你就基本掌握啦。 接下来我们还会介绍 OIDC 的授权码+PKCE 流程,以及接入 Authing 的方式,需要你对授权码模式的流程有一定了解哦。

往期精彩内容 什么是事件驱动( EDA )?为什么它是技术领域的主要驱动力? 图片