一、id_token 是什么以及它解决的问题OIDCOpenID Connect在 OAuth 2.0 之上构建了一层身份认证能力。OAuth 2.0 本身只解决授权authorization——它告诉资源服务器这个 access_token 持有者被允许访问某些资源但它从不保证这个 token 的持有者是谁。把 access_token 当成身份凭证使用是一个经典且危险的误用。id_token正是为填补这个空缺而生它是一个由 OPOpenID Provider身份提供方签发、面向RPRelying Party依赖方/客户端的、携带用户身份断言的 JWTJSON Web Token。核心区别可以用一句话概括Token受众audience用途access_tokenResource Server资源服务器授权——“能访问什么”id_tokenClient / RP依赖方认证——“用户是谁”id_token的格式必须是 JWT且必须签名JWSJSON Web Signature。这是 OIDC 与 OAuth 2.0 的硬性差异之一。二、id_token 的结构id_token是一个标准 JWT三段式Header.Payload.Signature各段用 Base64URL 编码后以.连接。2.1 Header头部{alg:RS256,typ:JWT,kid:2024-key-01}alg签名算法。OIDC 默认且最常见为RS256RSA SHA-256。kidKey ID标识本次签名使用的密钥是 RP 在校验时从 JWKSJSON Web Key Set中精确定位公钥的关键。2.2 Payload声明集ClaimsOIDC 规定了一组标准声明下面是签发与校验中最关键的几个Claim全称/含义必需说明issIssuer签发者是OP 的标识 URL校验时必须精确字符串匹配subSubject主体是用户在该 OP 下的唯一且不可变标识符audAudience受众是接收方通常为 RP 的client_idexpExpiration Time过期时间是Unix 时间戳过期后必须拒绝iatIssued At签发时间是签发时刻nonce随机串条件防重放绑定到一次认证请求auth_time认证发生时间条件配合max_age使用azpAuthorized Party被授权方条件当aud含多个值时标识实际使用方一个典型的 payload{iss:https://op.example.com,sub:248289761001,aud:s6BhdRkqt3,exp:1735689600,iat:1735686000,nonce:n-0S6_WzA2Mj,auth_time:1735685990,email:aliceexample.com,email_verified:true}关于sub它在同一个 issuer 内唯一标识一个用户。RP 判断是否同一用户的正确依据是(iss, sub)组合而非email、phone等可变信息。把email当主键是一个常见的安全/正确性漏洞。2.3 Signature签名签名覆盖Base64URL(Header) . Base64URL(Payload)使用 OP 私钥生成RP 用对应公钥校验。三、签发流程OP 视角3.1 何时签发id_token在 OIDC 各种 flow 的Token Endpoint响应中签发Authorization Code Flow或在 Authorization Endpoint 直接返回Implicit / Hybrid Flow。当前最佳实践是Authorization Code Flow PKCEProof Key for Code ExchangeImplicit Flow 已被 OAuth 2.1 弃用。Authorization Code Flow 下token 响应形如{access_token:...,token_type:Bearer,expires_in:3600,id_token:eyJhbGciOiJSUzI1Ni␣...,refresh_token:...}3.2 签名算法选择算法族代表说明RSARS256最通用互操作性最好默认首选ECDSAES256密钥更短、性能更好逐渐普及HMACHS256对称密钥RP 与 OP 共享密钥仅适合机密客户端的特定场景非对称算法RS256/ES256是主流OP 持有私钥签名公钥通过 JWKS 公开分发RP 无需持有任何密钥即可校验。绝对不要接受alg: none。这是 JWT 历史上最著名的漏洞类别——攻击者把算法改成none并去掉签名若校验方盲信 header 中的alg即可伪造任意身份。3.3 密钥管理与轮换OP 通过JWKS 端点通常是/.well-known/jwks.json发布公钥集合每个密钥带唯一kid。密钥轮换时OP 应先发布新公钥与旧公钥并存于 JWKS 中再切换为用新私钥签名。这给了 RP 缓存刷新的时间窗口避免轮换瞬间校验失败。签名时把kid写入 headerRP 据此选用正确公钥。3.4 nonce 的回填如果授权请求中 RP 传了nonceOP必须把它原样写入id_token的nonce声明。这是防重放链条的 OP 端职责。四、校验流程RP 视角——核心中的核心校验是整个机制安全性的落脚点。签发由 OP 负责但绝大多数攻击面在 RP 的校验实现里。以下步骤缺一不可。4.1 完整校验清单否是否是失败成功是否是否收到 id_token解析三段读取 Header 的 alg / kidalg 是否在白名单内?(拒绝 none、拒绝意外的 HS256)拒绝按 kid 从缓存的 JWKS 取公钥缓存命中?刷新 JWKS 再取验证签名校验 iss 精确匹配校验 aud 含本 client_id校验 exp 未过期 (含时钟偏移容忍)校验 iat 合理本次发过 nonce?校验 nonce 与会话中存的一致跳过用了 max_age?校验 auth_time max_age now通过建立用户会话认证失败4.2 逐项详解(1) 算法校验——先于一切不要信任 token header 里的alg来决定校验逻辑。RP 应基于自己的配置维护一个算法白名单例如只接受RS256并拒绝一切不在白名单内的算法。尤其要防范alg: noneRS256 → HS256 混淆攻击攻击者把算法从 RS256 改成 HS256用 OP 的公钥公开可得当作 HMAC 密钥重新签名。若 RP 校验代码根据 header 的 alg 自动选择算法且把公钥当对称密钥喂进去签名就会通过。根治办法是按配置固定算法而非读 header。(2) 签名验证根据kid从 JWKS 取对应公钥。JWKS 应带缓存按Cache-Control或固定 TTL但遇到未知kid时应触发一次刷新以适配密钥轮换。注意防 JWKS 刷新被滥用为 DoS限频。(3)iss校验与 RP 配置的 issuer 做精确字符串比较不做模糊/子串匹配。多租户场景尤其要锁定到具体租户的 issuer。(4)aud校验aud可能是字符串或数组。RP 必须确认其中包含自己的client_id。若aud含多个值且存在azp还应校验azp等于本client_id。这一步防止为别的客户端签发的 token 被拿来冒用。(5)exp/iat时间校验exp当前时间必须早于exp。允许一个小的时钟偏移容忍clock skew通常 ≤ 60 秒避免分布式系统轻微时间差导致误判。容忍值不宜过大。iat可选地校验签发时间不在未来、不过分陈旧。(6)nonce校验——防重放完整链条RP 在发起授权请求时生成随机nonce存入用户会话如 server-side session 或 HttpOnly Cookie 绑定的状态OP 回填到id_tokenRP 收到后比对二者是否一致。这能防止旧的id_token被重放并把 token 绑定到本次具体的认证会话。注意区分nonce与statestate防 CSRFCross-Site Request Forgery、维持请求上下文作用在授权请求/回调环节nonce防 id_token 重放作用在 token 本身。两者职责不同都应使用。(7)auth_time/max_age若 RP 在授权请求中带了max_ageOP 必须返回auth_time。RP 校验auth_time max_age是否仍在有效期内用于对敏感操作强制近期重新认证。4.3 一段示意性校验伪代码// 概念示意省略异常处理细节publicClaimsPrincipalValidateIdToken(stringidToken){varparametersnewTokenValidationParameters{// 1) 固定算法白名单绝不读 header 的 alg 自动决定ValidAlgorithmsnew[]{RS256},// 2) 签名公钥来自带缓存、自动刷新的 JWKSIssuerSigningKeyResolver(token,securityToken,kid,p)_jwksCache.ResolveByKid(kid),ValidateIssuerSigningKeytrue,// 3) iss 精确匹配ValidateIssuertrue,ValidIssuer_config.Issuer,// 4) aud 必须包含本 client_idValidateAudiencetrue,ValidAudience_config.ClientId,// 5) 过期校验 有限时钟偏移ValidateLifetimetrue,ClockSkewTimeSpan.FromSeconds(60),};varprincipal_handler.ValidateToken(idToken,parameters,out_);// 6) nonce 单独比对库一般不自动做varnonceprincipal.FindFirst(nonce)?.Value;if(nonce!_session.ExpectedNonce)thrownewSecurityException(nonce mismatch);returnprincipal;}五、工程最佳实践小结优先用成熟库JWT/OIDC 校验细节极多自己手写极易出错。用经过审计的库如 .NET 的Microsoft.IdentityModel.Tokens、各语言的官方 OIDC 中间件只在配置层做正确决策。算法按配置固定建立白名单杜绝none与 RS256/HS256 混淆。iss 精确匹配、aud 必含 client_id这两步拦截绝大多数借用 token攻击。nonce state 都要用分别防重放与 CSRF。JWKS 带缓存且能按未知 kid 触发刷新平滑支持密钥轮换。id_token 不是 access_token它用于在 RP 建立本地会话/确认身份不应被当作调用资源服务器的凭证反之 access_token 也不应被 RP 解析当身份。id_token 设短有效期它的使命是认证完成那一刻确认身份建立会话后就不再需要长期存活。时钟偏移容忍要小建议 ≤ 60s过大会变相延长过期 token 的可用窗口。
OIDC id_token 的签发与校验:从规范到工程实践
发布时间:2026/6/6 9:05:18
一、id_token 是什么以及它解决的问题OIDCOpenID Connect在 OAuth 2.0 之上构建了一层身份认证能力。OAuth 2.0 本身只解决授权authorization——它告诉资源服务器这个 access_token 持有者被允许访问某些资源但它从不保证这个 token 的持有者是谁。把 access_token 当成身份凭证使用是一个经典且危险的误用。id_token正是为填补这个空缺而生它是一个由 OPOpenID Provider身份提供方签发、面向RPRelying Party依赖方/客户端的、携带用户身份断言的 JWTJSON Web Token。核心区别可以用一句话概括Token受众audience用途access_tokenResource Server资源服务器授权——“能访问什么”id_tokenClient / RP依赖方认证——“用户是谁”id_token的格式必须是 JWT且必须签名JWSJSON Web Signature。这是 OIDC 与 OAuth 2.0 的硬性差异之一。二、id_token 的结构id_token是一个标准 JWT三段式Header.Payload.Signature各段用 Base64URL 编码后以.连接。2.1 Header头部{alg:RS256,typ:JWT,kid:2024-key-01}alg签名算法。OIDC 默认且最常见为RS256RSA SHA-256。kidKey ID标识本次签名使用的密钥是 RP 在校验时从 JWKSJSON Web Key Set中精确定位公钥的关键。2.2 Payload声明集ClaimsOIDC 规定了一组标准声明下面是签发与校验中最关键的几个Claim全称/含义必需说明issIssuer签发者是OP 的标识 URL校验时必须精确字符串匹配subSubject主体是用户在该 OP 下的唯一且不可变标识符audAudience受众是接收方通常为 RP 的client_idexpExpiration Time过期时间是Unix 时间戳过期后必须拒绝iatIssued At签发时间是签发时刻nonce随机串条件防重放绑定到一次认证请求auth_time认证发生时间条件配合max_age使用azpAuthorized Party被授权方条件当aud含多个值时标识实际使用方一个典型的 payload{iss:https://op.example.com,sub:248289761001,aud:s6BhdRkqt3,exp:1735689600,iat:1735686000,nonce:n-0S6_WzA2Mj,auth_time:1735685990,email:aliceexample.com,email_verified:true}关于sub它在同一个 issuer 内唯一标识一个用户。RP 判断是否同一用户的正确依据是(iss, sub)组合而非email、phone等可变信息。把email当主键是一个常见的安全/正确性漏洞。2.3 Signature签名签名覆盖Base64URL(Header) . Base64URL(Payload)使用 OP 私钥生成RP 用对应公钥校验。三、签发流程OP 视角3.1 何时签发id_token在 OIDC 各种 flow 的Token Endpoint响应中签发Authorization Code Flow或在 Authorization Endpoint 直接返回Implicit / Hybrid Flow。当前最佳实践是Authorization Code Flow PKCEProof Key for Code ExchangeImplicit Flow 已被 OAuth 2.1 弃用。Authorization Code Flow 下token 响应形如{access_token:...,token_type:Bearer,expires_in:3600,id_token:eyJhbGciOiJSUzI1Ni␣...,refresh_token:...}3.2 签名算法选择算法族代表说明RSARS256最通用互操作性最好默认首选ECDSAES256密钥更短、性能更好逐渐普及HMACHS256对称密钥RP 与 OP 共享密钥仅适合机密客户端的特定场景非对称算法RS256/ES256是主流OP 持有私钥签名公钥通过 JWKS 公开分发RP 无需持有任何密钥即可校验。绝对不要接受alg: none。这是 JWT 历史上最著名的漏洞类别——攻击者把算法改成none并去掉签名若校验方盲信 header 中的alg即可伪造任意身份。3.3 密钥管理与轮换OP 通过JWKS 端点通常是/.well-known/jwks.json发布公钥集合每个密钥带唯一kid。密钥轮换时OP 应先发布新公钥与旧公钥并存于 JWKS 中再切换为用新私钥签名。这给了 RP 缓存刷新的时间窗口避免轮换瞬间校验失败。签名时把kid写入 headerRP 据此选用正确公钥。3.4 nonce 的回填如果授权请求中 RP 传了nonceOP必须把它原样写入id_token的nonce声明。这是防重放链条的 OP 端职责。四、校验流程RP 视角——核心中的核心校验是整个机制安全性的落脚点。签发由 OP 负责但绝大多数攻击面在 RP 的校验实现里。以下步骤缺一不可。4.1 完整校验清单否是否是失败成功是否是否收到 id_token解析三段读取 Header 的 alg / kidalg 是否在白名单内?(拒绝 none、拒绝意外的 HS256)拒绝按 kid 从缓存的 JWKS 取公钥缓存命中?刷新 JWKS 再取验证签名校验 iss 精确匹配校验 aud 含本 client_id校验 exp 未过期 (含时钟偏移容忍)校验 iat 合理本次发过 nonce?校验 nonce 与会话中存的一致跳过用了 max_age?校验 auth_time max_age now通过建立用户会话认证失败4.2 逐项详解(1) 算法校验——先于一切不要信任 token header 里的alg来决定校验逻辑。RP 应基于自己的配置维护一个算法白名单例如只接受RS256并拒绝一切不在白名单内的算法。尤其要防范alg: noneRS256 → HS256 混淆攻击攻击者把算法从 RS256 改成 HS256用 OP 的公钥公开可得当作 HMAC 密钥重新签名。若 RP 校验代码根据 header 的 alg 自动选择算法且把公钥当对称密钥喂进去签名就会通过。根治办法是按配置固定算法而非读 header。(2) 签名验证根据kid从 JWKS 取对应公钥。JWKS 应带缓存按Cache-Control或固定 TTL但遇到未知kid时应触发一次刷新以适配密钥轮换。注意防 JWKS 刷新被滥用为 DoS限频。(3)iss校验与 RP 配置的 issuer 做精确字符串比较不做模糊/子串匹配。多租户场景尤其要锁定到具体租户的 issuer。(4)aud校验aud可能是字符串或数组。RP 必须确认其中包含自己的client_id。若aud含多个值且存在azp还应校验azp等于本client_id。这一步防止为别的客户端签发的 token 被拿来冒用。(5)exp/iat时间校验exp当前时间必须早于exp。允许一个小的时钟偏移容忍clock skew通常 ≤ 60 秒避免分布式系统轻微时间差导致误判。容忍值不宜过大。iat可选地校验签发时间不在未来、不过分陈旧。(6)nonce校验——防重放完整链条RP 在发起授权请求时生成随机nonce存入用户会话如 server-side session 或 HttpOnly Cookie 绑定的状态OP 回填到id_tokenRP 收到后比对二者是否一致。这能防止旧的id_token被重放并把 token 绑定到本次具体的认证会话。注意区分nonce与statestate防 CSRFCross-Site Request Forgery、维持请求上下文作用在授权请求/回调环节nonce防 id_token 重放作用在 token 本身。两者职责不同都应使用。(7)auth_time/max_age若 RP 在授权请求中带了max_ageOP 必须返回auth_time。RP 校验auth_time max_age是否仍在有效期内用于对敏感操作强制近期重新认证。4.3 一段示意性校验伪代码// 概念示意省略异常处理细节publicClaimsPrincipalValidateIdToken(stringidToken){varparametersnewTokenValidationParameters{// 1) 固定算法白名单绝不读 header 的 alg 自动决定ValidAlgorithmsnew[]{RS256},// 2) 签名公钥来自带缓存、自动刷新的 JWKSIssuerSigningKeyResolver(token,securityToken,kid,p)_jwksCache.ResolveByKid(kid),ValidateIssuerSigningKeytrue,// 3) iss 精确匹配ValidateIssuertrue,ValidIssuer_config.Issuer,// 4) aud 必须包含本 client_idValidateAudiencetrue,ValidAudience_config.ClientId,// 5) 过期校验 有限时钟偏移ValidateLifetimetrue,ClockSkewTimeSpan.FromSeconds(60),};varprincipal_handler.ValidateToken(idToken,parameters,out_);// 6) nonce 单独比对库一般不自动做varnonceprincipal.FindFirst(nonce)?.Value;if(nonce!_session.ExpectedNonce)thrownewSecurityException(nonce mismatch);returnprincipal;}五、工程最佳实践小结优先用成熟库JWT/OIDC 校验细节极多自己手写极易出错。用经过审计的库如 .NET 的Microsoft.IdentityModel.Tokens、各语言的官方 OIDC 中间件只在配置层做正确决策。算法按配置固定建立白名单杜绝none与 RS256/HS256 混淆。iss 精确匹配、aud 必含 client_id这两步拦截绝大多数借用 token攻击。nonce state 都要用分别防重放与 CSRF。JWKS 带缓存且能按未知 kid 触发刷新平滑支持密钥轮换。id_token 不是 access_token它用于在 RP 建立本地会话/确认身份不应被当作调用资源服务器的凭证反之 access_token 也不应被 RP 解析当身份。id_token 设短有效期它的使命是认证完成那一刻确认身份建立会话后就不再需要长期存活。时钟偏移容忍要小建议 ≤ 60s过大会变相延长过期 token 的可用窗口。