跳到正文

OpenID Connect 流程图:Authorization Code Flow、ID Token 与 Discovery

OpenID Connect 如何为 OAuth 2.0 加上登录:带 PKCE 的授权码流程、ID Token 及其校验、Discovery、JWKS、UserInfo 和登出。

· 创始人兼开发者

发布于

包含用户、浏览器、Relying Party 和 OpenID 提供方的时序图。13 条带编号的消息涵盖重定向、登录、code 交换和令牌响应;critical 片段校验 ID Token,opt 片段调用 UserInfo。

一个移动应用通过身份提供方让用户登录,并把 access token 交给你的后端。后端调用提供方的用户资料端点,拿到电子邮件地址,然后创建会话。所有测试都能通过。但这样一来,用户用同一个提供方登录过的任何其他应用,也能用自己的 access token 冒充该用户登录你的应用,因为没有任何环节检查这个令牌是发给哪个应用的。

OpenID Connect(OIDC)补上了这个漏洞。它在 OAuth 2.0 之上增加了 ID Token:一个带签名的 JWT,提供方在其中声明用户是谁、令牌是发给哪个应用的,以及用户何时登录。

本文把带 PKCE 的授权码流程画成时序图,并介绍 OIDC 新增的内容:openid scope、nonce、ID Token 校验、Discovery、密钥轮换、UserInfo 和登出。

OAuth 2.0 与 OpenID Connect 的区别

OAuth 2.0 关注访问:用一个令牌代表用户调用 API。OpenID Connect 关注身份:谁登录了。

OAuth 2.0 OpenID Connect
它回答的问题 这个应用可以调用这个 API 吗? 用户是谁,什么时候登录的?
令牌 Access token ID Token
令牌的接收方 API 你的应用
令牌格式 未定义:不透明字符串或 JWT 带签名的 JWT
如何请求 由 API 定义的 scope scope 包含 openid
定义的内容 Grant 和端点 Claim、UserInfo、Discovery、登出

在网络上,OIDC 登录就是 scope 中带有 openid 的 OAuth 授权请求,令牌响应里除了 access token 还有一个 id_token。OAuth 的部分在 OAuth 2.0 流程图中有详细介绍。

逐步看流程

OpenID Connect Core 1.0 把你的应用称为 Relying Party(RP),把授权服务器称为 OpenID 提供方(OP)。经过浏览器的内容,即前端通道,可能泄露到 URL、历史记录和日志里;而你的后端到提供方的调用,即后端通道,不会。编号与图中一致。

  1. 开始(1–3)。 用户点击 Sign in。你的应用把新的 state、nonce 和 PKCE 的 code_verifier 存进会话,然后把浏览器重定向到提供方。
  2. 认证请求(4)。 浏览器跟随重定向:
GET /authorize?response_type=code
    &client_id=orders-web
    &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
    &scope=openid%20profile%20email
    &state=7fJq2LbX9aQe
    &nonce=n-0S6_WzA2Mj
    &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
    &code_challenge_method=S256 HTTP/1.1
Host: op.example.com
  1. 登录与授权同意(5)。 提供方对用户进行认证(密码、通行密钥、MFA 或已有会话),并征求同意。你的应用从不接触凭据。
  2. 返回 code(6–7)。 提供方带着一个短期有效的 code 和同一个 state 重定向到已注册的 redirect_uri,你的应用把它与会话中的值比较。
  3. 令牌请求(8–9)。 你的后端用客户端凭据 POST code、redirect_uri 和 code_verifier。提供方用 challenge 校验 verifier,并返回 id_token、access_token,通常还有 refresh_token。
  4. 校验(10)。 你的应用校验 ID Token,只有当令牌指明的密钥不在缓存里时才去获取签名密钥。
  5. UserInfo(11–12),可选。 仅当 ID Token 缺少你需要的 claim 时才用。
  6. 会话(13)。 你的应用创建自己的会话并设置 Cookie。

state、nonce 和 PKCE 分别防什么

值 出现在 由谁检查 防止
state 第 4 步和返回的重定向 你的应用 用户从未发起的登录响应(CSRF)
nonce 第 4 步,之后在 ID Token 内部 你的应用 ID Token 重放,以及针对机密客户端的 code 注入
code_verifier 第 4 步是它的 SHA-256 哈希,第 8 步是它本身 提供方 兑换被盗的 code

OAuth 安全最佳实践 RFC 9700 允许在某些情况下用其中一个代替另一个,但每个只多花一个随机值,所以三个都发送。单页应用和移动应用等公共客户端必须使用带 S256 的 PKCE(RFC 7636)。

ID Token 内部

第 9 步的 ID Token,解码后如下:

{
  "iss": "https://op.example.com",
  "sub": "248289761001",
  "aud": "orders-web",
  "nonce": "n-0S6_WzA2Mj",
  "iat": 1793736400,
  "exp": 1793740000,
  "auth_time": 1793736390
}
Claim 含义 你的应用
iss 签发者,一个 https URL 必须与 Discovery 得到的 issuer 完全相等
sub 用户 ID,在签发者范围内永不重新分配,最长 255 个 ASCII 字符 用 iss 加 sub 作为账号键
aud 受众,字符串或数组 必须能在其中找到自己的 client_id
exp、iat 过期时间和签发时间,Unix 秒 拒绝已过期或太旧的令牌
nonce 第 4 步发送的值 必须与会话中的值一致
auth_time 用户实际完成认证的时间 发送了 max_age 时要检查
acr、amr 保证级别和认证方式,例如 pwd 或 mfa 在敏感操作之前检查

Core 说得很明确:只有 iss 加 sub 才能标识一个用户,因为电子邮件地址会变,也可能被重新分配。

校验 ID Token

Core 的 3.1.3.7 节列出了这些规则:

  1. 固定算法(默认 RS256),不要信任 alg 头,也绝不接受 none。
  2. 验证签名,使用 kid 指明的 JWKS 密钥。因为令牌直接来自令牌端点,Core 允许授权码流程的客户端改为依赖 TLS,但验证的成本很低。
  3. 检查 iss,与 Discovery 得到的签发者逐字符比较。
  4. 检查 aud,确认包含你的 client_id,并拒绝你不信任的受众。
  5. 检查 exp 和 iat,允许几秒到几分钟的时钟偏差。
  6. 检查 nonce,与会话中的值比较,然后删除它。
  7. 检查 auth_time 和 acr,如果你请求过它们。

做完这些才创建会话。在 Node.js 中使用 jose 库:

import { createRemoteJWKSet, jwtVerify } from 'jose'

// issuer and jwks_uri come from the discovery document.
// The key set is cached; an unknown kid triggers a refetch
// (at most once every 30 seconds by default).
const JWKS = createRemoteJWKSet(new URL(oidc.jwks_uri))

const { payload } = await jwtVerify(tokens.id_token, JWKS, {
  issuer: oidc.issuer,     // exact match
  audience: CLIENT_ID,     // must appear in aud
  algorithms: ['RS256'],   // never trust the header alone
  clockTolerance: 60,      // seconds of clock skew
  maxTokenAge: 600,        // rejects an old iat
})
if (payload.nonce !== session.nonce) throw new Error('nonce mismatch')

Discovery 与密钥轮换

OpenID Connect Discovery 1.0 把提供方的设置放在一个 JSON 文档里,地址是签发者 URL 加上 /.well-known/openid-configuration。下面是 Google 的,只保留了这个流程用到的字段,截至撰写时(2026 年 11 月):

{
  "issuer": "https://accounts.google.com",
  "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
  "token_endpoint": "https://oauth2.googleapis.com/token",
  "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
  "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
  "id_token_signing_alg_values_supported": ["RS256"],
  "code_challenge_methods_supported": ["plain", "S256"]
}

issuer 必须与你获取它所用的签发者 URL 以及每个 ID Token 的 iss 完全相同。预期的签发者要自己配置,绝不要从未经验证的令牌里取。

密钥通过 jwks_uri 轮换:提供方添加一把新密钥并用它签名,同时让旧密钥继续公布一段时间(Core 的 10.1.1 节)。要缓存密钥集,遵守 Cache-Control,遇到不认识的 kid 时重新获取,并对这些重新获取做限流。JWT 认证流程图讲的是 access token 上同样的机制。

UserInfo

用 access token 调用 UserInfo 时,它会返回已授予 scope(profile、email、address、phone)对应的 claim。响应中始终包含 sub,它必须与 ID Token 的 sub 完全一致,否则就丢弃这个响应。

登出方式

有三份最终版的 OpenID 规范涉及登出:

规范 工作方式 注意事项
RP-Initiated Logout 1.0 你的应用带着 id_token_hint 重定向到提供方的 end_session_endpoint 你的应用还必须结束自己的会话
Front-Channel Logout 1.0 提供方在 iframe 中加载每个应用的登出 URL 第三方 Cookie 被屏蔽时可能失效
Back-Channel Logout 1.0 提供方向每个应用 POST 一个带签名的登出令牌 你的应用必须在服务端找到并结束会话

Core 还指出,ID Token 的 exp 与会话的存续时间无关。

评审中常见的错误

  • 把 access token 当作身份证明。 就是开头那个场景。只用你的后端从令牌端点拿到并校验过的 ID Token 让用户登录。
  • 跳过 aud 或 nonce。 在同一个提供方注册的每个应用,都会拿到签名有效、iss 相同的 ID Token;只有 aud 说明令牌是发给你的,也只有 nonce 把它绑定到这次登录。
  • 隐式流程。 response_type=id_token token 会把令牌放进 URL 片段;RFC 9700 指出,客户端不应使用会在授权响应中返回 access token 的 response type。请使用带 PKCE 的 code,单页应用也一样。
  • 把 ID Token 发给 API。 API 应当只接受发给自己的 access token;API 网关架构图展示了这个检查通常放在哪里。

OpenID Connect 流程图

包含用户、浏览器、Relying Party 和 OpenID 提供方的时序图。13 条带编号的消息涵盖重定向、登录、code 交换和令牌响应;critical 片段校验 ID Token,opt 片段调用 UserInfo。

16 个组件 · 13 个连接

在 Archboard 中打开
图表内容
  • User
  • Browser
  • Relying party (your app)
  • OpenID provider
  • all checks must pass
  • claims missing from the ID token
  • ID token: for your app (aud = client_id)
  • Access token: for UserInfo and APIs
  • 1. Click Sign in
  • 2. GET /login
  • 3. 302 to the provider's /authorize
  • 4. GET /authorize: scope=openid, state, nonce, code_challenge
  • 5. Log in (password, passkey, MFA) and consent
  • 6. 302 to redirect_uri with code, state
  • 7. GET /callback?code&state
  • 8. POST /token: code, code_verifier, client auth
  • 9. id_token, access_token
  • 10. GET jwks_uri (when kid is not cached)
  • 11. GET /userinfo, Bearer access_token
  • 12. Claims; sub must match the ID token
  • 13. 302 to the app, Set-Cookie: session
带 PKCE 的 OpenID Connect 授权码流程。你的应用带着 scope=openid、state、nonce 和 code challenge 把浏览器送到提供方,通过后端通道交换 code,并在创建会话之前校验 ID Token。

这张图的读法如下:

  • 四个参与者: 用户、浏览器、你的应用(Relying Party)和 OpenID 提供方。时间向下流动。
  • 颜色表示通道: 蓝色经过浏览器,青色是从你的后端到提供方,紫色是用户的操作。虚线箭头是响应和重定向。
  • 第 3 步和第 4 步携带 scope=openid、state、nonce 和 PKCE 的 challenge;第 8 步携带 verifier。
  • critical 片段包含获取密钥,以及会话建立之前必须通过的检查。opt 片段是 UserInfo 调用。
  • 右侧一栏把 Discovery 字段对应到各步骤,列出 PKCE 参数,解码一个 ID Token,并显示哪个令牌是给谁的。

如何把 OpenID Connect 图画清楚

  • 给浏览器单独一条生命线。 前端通道和后端通道的区分,正是这个流程的安全要点。
  • 在箭头上写出被检查的参数: state、nonce、code_challenge、code_verifier。
  • 把校验写具体。 「校验令牌」没法评审,「签名、iss、aud、exp、iat、nonce」可以。
  • 说明哪个令牌送到哪里,并带上它的受众。很多 OIDC 的 bug 都是把令牌发给了错误的一方。
  • 一张图只画一个流程。 登录、刷新和登出分开画,并使用占位的 ID 和令牌。

时序图示例介绍了这种记法;SAML SSO 流程图展示了企业客户至今仍会要求的基于 XML 的协议。

在 Archboard 中构建

  1. 用 Google 或 GitHub 登录并创建一个图表。
  2. 在时序包中,为用户添加一个参与者,再添加三个等高的生命线节点:Browser、Relying party(你的应用)和 OpenID provider。
  3. 把 13 条消息画成带编号的直线边,自上而下。让响应用虚线,并给前端通道和后端通道设置不同的颜色。
  4. 从时序包添加一个组合片段,运算符为 critical,围住获取密钥和各项检查;再添加第二个组合片段,运算符为 opt,围住 UserInfo 调用。
  5. 从基础包添加文本,放在 Relying party 旁边,写上会话值和各项检查,再加一个颜色图例。
  6. 从基础包添加一个 Markdown 块,写 Discovery、PKCE 和解码后的 ID Token,再从安全包添加两个令牌节点,分别代表 ID Token 和 access token。

这个示例共用 9 个图表节点,在 Free 套餐的限制之内。你的 AI 智能体也可以通过 MCP 来画。

常见问题

OAuth 2.0 和 OpenID Connect 有什么区别? OAuth 2.0 给应用一个 access token 来调用 API。OpenID Connect 在其上加了登录:openid scope、说明用户是谁的 ID Token,以及标准的 Discovery 和 UserInfo 端点。

ID Token 和 access token 是一回事吗? 不是。ID Token 是发给你的应用的带签名 JWT,说明谁登录了。Access token 是给 API 用的,包括 UserInfo。绝不要把 ID Token 发给 API,也不要把 access token 当作登录证明。

已经发送了 nonce,还需要 PKCE 吗? 两个都用。RFC 9700 要求公共客户端使用 PKCE。机密客户端可以在采取额外预防措施的前提下依赖 nonce,但 PKCE 能阻止被盗 code 被兑换,而 nonce 只能在令牌响应之后发现被注入的 code。

如何校验 ID Token? 用与其 kid 匹配的 JWKS 密钥验证签名,然后检查 iss 等于签发者、aud 包含你的 client ID、exp 尚未过期、iat 足够新,并且 nonce 与会话一致。

/.well-known/openid-configuration 是什么? 它是 OpenID Connect 的 Discovery 文档,列出提供方的签发者、端点、算法和 jwks_uri,让你的应用只靠一个 URL 就能完成配置。

免费开始 — 1 个席位,无需信用卡。

相关文章

← 返回博客

一分钟打开画布

使用 Google 或 GitHub 登录。单席位免费,团队加入后再升级。