Voocii博客
首页博客AI 热榜作品集读书友链工具关于

© 2026 Voocii. Built with Next.js & tRPC.

GitHubXEmailRSS


实战:基于 OAuth2/OIDC 的联合认证服务器(Identity Broker)实现教程

工作总结rick-hayekrick-hayek2025年5月18日

本文以已有 Azure AD B2C 租户为例。新项目应同时评估 Microsoft Entra External ID,因为 Azure AD B2C 已不再向新客户服务。

适用场景:企业对外开放 API,自身不维护用户账号密码,而是代理客户企业的 Microsoft Entra ID (原Azure AD) / Azure AD B2C 完成认证,再向下游客户端(第三方应用)签发标准 OAuth2/OIDC token。

文档中将出现三个角色: 认证服务器 - 企业自己实现的 OAuth/OIDC 认证服务;为方便理解,文中的 我们 或者 Broker即指代这里的企业;
Microsoft Entra ID / Azure AD B2C - 用户实际认证的服务;
客户端 - 最终用户使用的 web/ app;


一、背景与需求说明

这其实是我之前工作中独立完成的一个小项目,本文作为一个工作总结。

1.1 业务背景

某公司在特定工业技术领域对外开放一组 API,供客户调用以完成工作流/流水线的定义与设计。客户企业的员工(最终用户)需要:

  1. 登录公司自己的账号体系;
  2. 由客户企业预先申请一个 client_id(app id),用以标识其应用。

公司希望员工可以直接使用自己企业已有的 Microsoft Entra ID 账号登录,而不需要公司另外维护一套账号密码体系。

1.2 核心需求

需求说明
不维护用户密码不存储、不校验任何用户的账号/密码
联合登录用户使用自身企业的 Microsoft Entra ID(通过 Azure AD B2C)账号登录
标准协议对外对下游客户端,提供标准 OAuth2 Authorization Code + PKCE 流程
多租户/多 client支持多个客户企业各自申请 client_id,数据与权限相互隔离
SSO(单点登录)同一用户在同一浏览器内访问不同已登录应用时无需重复登录
Token 全生命周期管理签发、校验、续期(refresh)、吊销 access_token / refresh_token

1.3 为什么不能直接转发 Azure B2C 的 id_token?

下游客户端的 OIDC SDK 在校验 id_token 时,会检查 iss(签发者)是否与其在 Discovery 文档中配置的一致。如果直接把 B2C 签发的 id_token 转发给客户端,iss 会指向 B2C 的地址,而客户端配置的是我们自己的 issuer, 校验必然失败。

因此必须由自己的 OAuth 服务器重新签发一套 id_token / access_token,这是本架构的核心设计决策。


二、原理介绍与技术选型

2.1 架构角色定位

这是一种成熟的架构模式,业界称为 Identity Broker(身份代理) 或 Federation Proxy(联合认证代理),Keycloak 的 "Identity Brokering"、Auth0 的 "Enterprise Connections" 都是同一模式的商业实现。

认证服务器同时扮演两个角色:

  • 对下游客户端(第三方应用)而言:是 OP / Authorization Server(OIDC Provider)
  • 对 Azure AD B2C 而言:是 RP / Relying Party(标准 OIDC 客户端)

两段 OAuth/OIDC 会话是完全独立的两次协议交互,由我们的服务器在中间做身份桥接与状态维护。

2.2 技术需求清单

必须实现的 OAuth2/OIDC 端点:

  1. Discovery endpoint(/.well-known/openid-configuration)
  2. Authorization endpoint(/authorize)—— 仅支持 Authorization Code + PKCE 模式
  3. Token endpoint(/token)—— 处理 authorization_code 和 refresh_token 两种 grant_type
  4. JWKS endpoint(/.well-known/jwks.json)—— 供客户端校验 token 签名
  5. UserInfo endpoint(/userinfo)—— 标准 OIDC SDK 常规调用
  6. Token revocation endpoint(/revoke,RFC 7009)
  7. End Session endpoint(/logout)—— 处理登出,含与 B2C 的联动登出

必需的基础设施:

  • 签名密钥对(非对称,如 RS256)+ JWKS 轮换机制
  • 服务端 session store(Redis 等)—— 存放登录会话与临时 transaction 上下文
  • Authorization code 的一次性存储(短 TTL,通常 60~120 秒)
  • Client 注册表(client_id、client_secret、redirect_uri 白名单、允许的 scope)
  • Refresh token 存储与轮换记录(用于检测重放/被盗用)

2.3 多租户支持

这里只支持 Microsoft Entra ID 的登陆,对于我们 Broker 来说,客户自己的账号系统必须是 Microsoft Entra ID。对于有其他系统或者自建 AD FS 的客户,如果要支持授权,则需要确定识别用户来自哪个客户的策略。通常有以下方法:

  • 客户专属登录入口;
  • tenant_hint;
  • 邮箱域名 Home Realm Discovery;
  • 每个 Client 绑定固定上游 IdP;

三、认证流程图

3.1 整体角色关系

flowchart LR
    subgraph 客户企业
        EU[最终用户/员工]
        APP[客户端应用<br/>client_id]
    end
    subgraph 我们的OAuth服务器
        AUTH[/authorize/]
        TOKEN[/token/]
        SESSION[(登录 Session Store)]
        TXN[(临时 Transaction 存储)]
    end
    subgraph 微软侧
        B2C[Azure AD B2C]
        AAD[客户企业 Microsoft Entra ID]
    end

    EU -->|1.点击登录| APP
    APP -->|2.跳转 /authorize| AUTH
    AUTH -->|3.无session,跳转| B2C
    B2C -->|4.联合到| AAD
    AAD -->|5.用户完成认证| B2C
    B2C -->|6.回调携带 authorization code| AUTH
    AUTH -->|7.后端使用 code 请求 B2C token endpoint| B2C
    B2C -->|8.返回 id_token/access_token| AUTH
    AUTH -->|9.校验上游 id_token| AUTH
    AUTH -->|10.建立登录session| SESSION
    AUTH -->|11.重定向携带code| APP
    APP -->|12.code+code_verifier换token| TOKEN
    TOKEN -->|13.签发自有id_token/access_token| APP

查看大图:👉 点击此处在新页面中打开 整体角色关系 图

3.2 详细时序图(含 state/nonce 桥接)

sequenceDiagram
    participant U as 用户浏览器
    participant C as "客户端应用(client_id=A1)"
    participant OP as 我们的OAuth服务器
    participant B2C as Azure AD B2C

    C->>OP: GET /authorize?client_id=A1&redirect_uri=..&state=S1&code_challenge=C1&nonce=N1
    OP->>OP: 校验client_id/redirect_uri白名单
    OP->>OP: 检查本地登录session cookie

    alt 无登录session
        OP->>OP: 保存transaction上下文(S1,C1,N1,client_id,redirect_uri) => txn_id
        OP->>OP: 生成内部state=S2, nonce=N2
        OP->>B2C: 重定向 /authorize?state=S2&nonce=N2&response_type=code&code_challenge=C2
        B2C->>U: 展示登录页(联合到客户企业AAD)
        U->>B2C: 完成账号密码/MFA认证
        B2C->>OP: 回调携带code及state=S2
        OP->>B2C: code + code_verifier=C2,用code换取B2C的id_token
        OP->>OP: 校验id_token(签名/iss/aud/exp/nonce=N2)
        OP->>OP: 用txn_id取回原始上下文(S1,C1,N1)
        OP->>OP: 建立登录session,写HttpOnly Cookie
    else 已有登录session - SSO命中
        OP->>OP: 直接使用session中的用户身份
    end

    OP->>OP: 生成authorization code(与C1绑定),TTL短
    OP->>C: 302重定向携带code + 原始state=S1
    C->>OP: POST /token (code, code_verifier, client_id)
    OP->>OP: 校验code_verifier与C1匹配(PKCE)
    OP->>OP: 校验code未被使用过(一次性)
    OP->>C: 签发自有id_token(iss=我们自己,nonce=N1)+access_token+refresh_token

其中:

C1:下游客户端 → Broker (我们的OAuth服务器);
C2:Broker → 上游 IdP。

查看大图:👉 点击此处在新页面中打开时序图


四、Step-by-Step 可执行实施步骤

步骤 1:搭建基础设施

  1. 生成 RS256 非对称密钥对,建立 JWKS 端点,预留密钥轮换的 kid 机制。
  2. 部署 Redis(或等效 KV 存储),用于:
    • 登录 session(用户已认证标记)
    • 临时 transaction 上下文(桥接 B2C 跳转期间的状态)
    • 一次性 authorization code
    • refresh_token 记录(含轮换链)
  3. 建立 Client 注册表(数据库表),字段至少包括:client_id、client_secret_hash、redirect_uris(白名单,精确匹配)、allowed_scopes。

步骤 2:实现 Discovery + JWKS 端点

  • /.well-known/openid-configuration 返回 issuer、各端点 URL、支持的签名算法、支持的 scope 等。
  • /.well-known/jwks.json 返回当前及上一版本的公钥(轮换过渡期两把 key 共存)。

步骤 3:实现 /authorize 端点(核心)

  1. 校验请求参数:client_id、redirect_uri(必须精确字符串匹配白名单,禁止前缀匹配)、response_type=code、code_challenge、code_challenge_method=S256。
  2. 检查登录 session cookie 是否存在且有效:
    • 存在且有效 → 跳到步骤 5(SSO 命中)。
    • 不存在 → 继续步骤 4。
  3. 保存本次客户端请求上下文(client_id, redirect_uri, state=S1, code_challenge, nonce=N1, scope)到 transaction 存储,生成内部 txn_id。
  4. 用我们自己新生成的 state=S2、nonce=N2 重定向到 Azure AD B2C 的 /authorize(带上 txn_id 用于回调时找回上下文,可放在自定义的 state 里签名保护,或直接作为 session cookie 关联)。
  5. B2C 完成认证后回调我们的 /callback 端点:
    • 用返回的 code 向 B2C 的 token endpoint 换取 B2C 的 id_token。
    • 校验 B2C id_token 的签名、iss (精确匹配,不能只检查前缀或域名)、aud(我们,即Broker,在B2C注册的客户端ID)、exp、nonce=N2(必须做,防重放)。

    azp:存在时校验 Authorized Party; iat:必要时检查签发时间; nbf:存在时检查生效时间;

    • 取回 txn_id 对应的原始上下文。
    • 建立我们自己的登录 session:写 HttpOnly + Secure + SameSite=Lax 的 session cookie,session 内容映射到服务端 store 中的用户身份。
  6. 生成一次性 authorization code(与 client_id、code_challenge、nonce=N1、用户身份绑定),TTL 60~120 秒。
  7. 302 重定向回客户端 redirect_uri,携带 code 和原始的 state=S1(必须原样带回,否则客户端会因 state 不匹配而报 CSRF 错误)。

步骤 4:实现 /token 端点

grant_type=authorization_code 分支:

  1. 校验 client_id / client_secret(如为机密客户端)。
  2. 校验 code 是否存在、未过期、未被使用过(用完即删,防重放)。
  3. 用 code_verifier 重新计算并比对 code_challenge(PKCE 校验)。
  4. 签发:
    • id_token:iss=我们自己,aud=client_id,sub=稳定用户标识,nonce=N1(取自 authorization code 绑定的原始 nonce)。

    不建议直接使用 email 作为 sub,因为 email 可能改变,或者多租户之间存在相同 email 等问题。可以使用不变标识组合生成,比如 hash(上游issuer + 上游sub)

    • access_token:按 client_id 对应的 allowed_scopes 生成权限范围。

    实际应该根据业务需求生成 access_token,最终 scope 是“请求 scope、客户端允许 scope、用户/租户权限”三者经过授权后的结果

    • refresh_token:随机不透明字符串,存入 store,记录所属 client_id、用户、有效期。

grant_type=refresh_token 分支:

  1. 校验 refresh_token 是否存在、有效、未被吊销。
  2. 轮换:签发新的 refresh_token,旧的立即失效并记录到"轮换链"。

轮换的操作要保证原子的:通过 Redis Lua、数据库事务来保证

  1. 若检测到已失效的旧 refresh_token 被再次使用 → 判定为重放攻击,吊销整条轮换链上的所有 token。
  2. 重新签发 access_token(及可选的新 id_token)。

Token Claim 设计: access_token 通常需要以下字段:

iss
sub
aud
exp
iat
nbf
jti
scope
client_id 或 azp
tenant_id

不要把过多用户资料塞进 access_token;ID Token 给客户端,Access Token 给 API,两者不要混淆使用.

步骤 5:实现 /userinfo、/revoke、/logout(end_session)

  • /userinfo:凭 access_token 返回用户基本信息(标准 OIDC SDK 默认会调用,缺失会导致部分 SDK 报错或降级)。
  • /revoke:主动吊销指定的 refresh_token / access_token。
  • /logout:清除本地登录 session cookie;同时决定是否联动触发 B2C 侧的登出(见下方风险点)。

五、关键步骤详细说明

5.1 state/nonce 的"双层桥接"是全流程中最容易出错的地方

我们的服务器在这个流程里同时参与两段独立的 OAuth 交易:

  • 与下游客户端之间的一段(state=S1, nonce=N1, code_challenge=C1)
  • 与 Azure B2C 之间的一段(state=S2, nonce=N2)

绝对不能把 S1 直接透传给 B2C,也不能把 B2C 返回的 id_token 未经处理直接转发给客户端。正确做法是:

  • 用我们自己生成的 S2/N2 去和 B2C 交互;
  • 用 txn_id(内部关联标识)把 "B2C 那次交互" 和 "客户端原始那次请求(S1/N1/C1)" 关联起来;
  • B2C 认证完成、拿到并校验完 B2C 的 id_token 后,从 txn 存储里取回原始的 S1,原样带回给客户端;用原始的 N1 签发我们自己的 id_token。

5.2 登录 Session 与 client_id 是解耦的

这是关于 SSO 最容易误解的一点:

我们的登录 session(第 3 步建立的那个 cookie)只跟"用户身份"绑定,不跟 client_id 绑定。而 access_token 才是跟 client_id 绑定的。

也就是说,同一个已登录的浏览器 session,可以针对不同的 client_id 分别走 /authorize → /token,各自换取各自权限范围的 access_token,而不需要重新跳转 B2C、不需要重新输密码。这在以下场景下会被触发:

  • 同一客户企业下有多个不同的应用各自申请了不同的 client_id(如"工作流设计器"和"监控看板"两个网站);
  • 同一员工先后打开这两个应用,都点击了登录。

5.3 为什么必须重新签发 id_token,而不是转发 B2C 的

下游客户端的 OIDC SDK 校验 id_token 时会核对 iss 是否等于其在 Discovery 文档里读到的 issuer。转发 B2C 的 id_token 会导致 iss 不匹配而校验失败。因此,"替换 id_token" 不是可选项,而是协议正确性的硬性要求。


六、注意点 / 风险点

风险点说明建议
redirect_uri 校验方式前缀匹配容易被 open redirect 攻击利用必须精确字符串全匹配白名单
state 混用把客户端的 S1 直接传给 B2C,或把 B2C 的 S2 直接回传给客户端严格区分两段交易的 state/nonce,用内部 txn_id 桥接
nonce 重放不校验 nonce 或 nonce 可重复使用nonce 必须一次性,校验后立即失效
Authorization code 重放code 被使用后未失效code 必须一次性使用,用后立即从存储删除
PKCE 缺失或校验不严格仅记录 code_challenge 但 token 端点不做校验/token 必须重新计算 code_verifier 并比对
Refresh token 无轮换长期使用同一个 refresh_token,泄露后无法感知每次 refresh 轮换新 token,检测旧 token 重放并吊销整链
登出不彻底只清了我们自己的 session,但 B2C 侧 session 还在,导致"登出"后重新登录可"免密"通过 B2C明确产品策略:是否需要触发 B2C 的 single logout;至少在文档中说明该行为
JWKS 无轮换机制单一签名密钥,轮换时旧 token 全部失效采用 kid + 多把 key 共存的过渡期设计
多租户权限隔离access_token 的 scope 未按 client_id 严格隔离确保 A 客户的 token 无法访问 B 客户的数据/权限范围
Token endpoint 无限流容易被暴力枚举/爆破攻击增加速率限制与异常检测
错误响应格式不规范不遵循 RFC 6749 的 error / error_description 字段严格按标准格式返回错误,避免第三方 SDK 解析失败
client_secret 误用于纯前端 SPA若下游客户端是纯前端(浏览器直接发起 /token 请求,没有自己的后端中转),client_secret 会被打包进 JS 下发到浏览器,任何人都能看到,起不到保密作用,属于 public client;只有"有自己后端、secret 存于服务端"的 confidential client 才应该要求并校验 secret在 client 注册表增加 client_type: confidential | public 字段;confidential 类型要求并校验 client_secret_hash,public 类型不要求 secret 但强制要求 PKCE;务必确认"跳过 secret 校验"的逻辑只对显式标记为 public 的 client 生效,不能变成"没配置 secret 的 client 一律放行"的漏洞

七、生产环境必须补充的安全能力

前面的流程能把主链路跑通,但 Identity Broker 一旦进入生产环境,它就不只是一个"登录中转站",而是整个系统的认证边界。下面这些能力建议作为上线前的硬性检查项,否则主流程即使能跑通,也可能在边界场景里留下安全缺口。

7.1 HTTPS、回调地址与 URL 泄露控制

所有认证相关端点必须只允许 HTTPS,包括 /.well-known/openid-configuration、/authorize、/token、/userinfo、/revoke、/logout 以及 B2C 回调地址。redirect_uri 必须使用注册表里的精确字符串匹配,不要做前缀匹配、通配符匹配或"同域名即可"这类宽松判断。

授权码可以出现在回调 URL 里,但 access_token、refresh_token、id_token、code_verifier、cookie 等敏感值不应该进入 URL、日志、异常消息或前端埋点。生产日志里至少要对 code、state、nonce、Authorization header、Cookie、token 响应体做脱敏。

7.2 state、nonce、PKCE 与 Login CSRF

state 不只是"把用户带回原页面"的字段,它也是防 CSRF 的关键。Broker 应该为下游客户端请求和上游 B2C 请求分别生成独立的 state / nonce,并把它们绑定到一次性的服务端transaction记录中。transaction记录应有较短 TTL,校验成功后立即删除。

如果下游客户端是 public client,必须强制 PKCE。对 confidential client,也建议支持并优先使用 PKCE。/token 端点不能只检查 code 是否存在,还必须重新计算 code_verifier 与原始 code_challenge 是否匹配。

7.3 防 Authorization Server Mix-Up / Issuer Mix-Up

Broker 不能根据传入 token 里的 iss 动态决定去哪里拉 JWKS,也不能接受非注册上游返回的 issuer。每个租户或上游身份源都应该有明确的 allowlist 配置,包括 issuer、authorization endpoint、token endpoint、JWKS URI、client_id 等。

校验上游 id_token 时,至少要严格验证:

  • iss 必须等于预期 issuer;
  • aud 必须包含 Broker 在该上游注册的 client_id;
  • alg 必须是允许的签名算法,不能接受 none;
  • kid 必须能在预期上游的 JWKS 中找到;
  • exp、nbf、iat 要在合理时间窗口内;
  • 多租户场景下还要校验 tenant / policy / authority 是否匹配当前交易。

7.4 Cookie、登录 Session 与 Session Fixation

Broker 自己的登录 session cookie 应设置 HttpOnly、Secure、SameSite=Lax 或更严格策略。完成登录后要重新生成 session id,避免攻击者在登录前固定一个 session id,再诱导用户使用这个 session 登录。

登录 session 只应该绑定用户身份和必要的上下文,不要把下游 client_id、scope 或权限结果直接塞进登录 session。不同 client 的授权结果应由各自的 authorization code / access_token 表达,这样才能避免 SSO 和授权边界混在一起。

7.5 Client 类型、认证与错误信息

client 注册的表里应明确区分 public 和 confidential。confidential client 必须校验 client_secret 或其他已注册认证方式;public client 不校验 secret,但必须强制 PKCE。不要把"没有配置 secret"自动解释成 public client,否则容易把配置错误变成认证绕过。

错误响应应遵循 OAuth2 标准字段,例如 error 和 error_description,但不要在错误信息里泄露过多细节。比如"client_id 不存在"、"secret 错误"、"redirect_uri 不匹配"可以在服务端审计日志里记录清楚,对外响应则保持足够泛化,避免被用来枚举 client 或探测配置。

7.6 Token 存储、轮换、吊销与日志

refresh_token 不应明文存储。可以存储哈希值或加密后的值,并记录 token family、上一版本 token、过期时间、吊销状态和重放检测结果。每次 refresh 都应轮换 refresh_token;一旦检测到旧 refresh_token 被重放,应吊销同一 token family 下的后续 token。

access_token 建议短有效期。若使用 JWT access_token,需要设计吊销策略,例如缩短过期时间、配合版本号/会话版本检查,或在高风险 API 上做 introspection。若使用 opaque token,则必须保证 introspection / token lookup 的性能、缓存和吊销一致性。

7.7 签名密钥、JWKS 与上游元数据缓存

Broker 自己签发 token 的私钥应放在 Key Vault、HSM 或等价的密钥管理系统中,不要放在代码仓库、镜像或普通配置文件里。JWKS 只暴露公钥,并通过 kid 支持密钥轮换。

密钥轮换时应至少保持"新旧公钥并存"一段时间:新 token 用新 key 签发,旧 token 在最大有效期内仍能被旧公钥验证。等所有旧 token 过期后,再从 JWKS 中移除旧公钥。

上游 B2C 的 metadata 和 JWKS 可以缓存,但要设置合理 TTL。遇到未知 kid 时可以触发一次受控刷新,但不能根据未信任 token 里的任意 URL 去拉取配置。

7.8 限流、审计与运维监控

/authorize、/token、/userinfo、/revoke、/logout 和回调端点都应该有限流。限流维度可以包括 IP、client_id、用户账号、租户、失败次数和设备指纹等。尤其是 /token 端点,它直接处理 code、refresh_token 和 client 认证,不应允许无限尝试。

审计日志应覆盖 client 注册变更、redirect_uri 变更、密钥轮换、异常登录、refresh_token 重放、吊销操作、上游 issuer 变更等事件。生产环境还应配置告警,例如某个 client 的失败率突然升高、未知 redirect_uri 请求变多、旧 refresh_token 重放、JWKS 频繁刷新失败等。

7.9 登出语义与上游 SSO

本地 /logout 清除 Broker session,不一定等于清除了 B2C 的登录状态。产品上必须明确这个语义:用户点击退出后,是只退出当前 Broker,还是也要触发上游 B2C 的 single logout。

如果需要完整退出,应实现上游支持的 end session / RP-initiated logout 流程,并处理回跳、失败和多客户端会话清理。即使不做上游登出,也要在产品文案和测试用例里说明:清除 Broker session 后再次登录,仍可能因为 B2C session 存在而免密完成认证。

7.10 推荐上线检查清单

检查项最低要求
HTTPS所有认证端点和回调地址强制 HTTPS
redirect_uri精确匹配注册表,不允许通配符或前缀匹配
state / nonce上下游独立生成、一次性、短 TTL、校验后删除
PKCEpublic client 强制启用,token 端点严格校验
client 类型明确区分 public / confidential,不能用"缺 secret"隐式放行
token 存储refresh_token 不明文存储,支持轮换、重放检测和吊销
日志敏感字段脱敏,不记录 token、secret、cookie、code_verifier
签名密钥私钥进密钥管理系统,通过 kid 支持平滑轮换
issuer 校验上游 issuer / JWKS / endpoint 必须来自 allowlist
限流与审计认证关键端点有限流,安全事件可追踪、可告警

八、测试场景 / 测试用例清单

8.1 主流程测试

  1. 完整登录链路:客户端发起 → 跳转 B2C → 完成认证 → 返回 code → 换取 token → 携带 access_token 访问 API,全链路打通。
  2. Discovery 文档完整性:请求 /.well-known/openid-configuration,核对每个字段(尤其 jwks_uri、end_session_endpoint)都对应真实实现,而非占位符。

8.2 SSO 正确性测试

  1. 用同一浏览器,先登录 client_id=A1 成功后,不清 cookie 直接访问 /authorize?client_id=A2&...,验证是否:
    • 跳过 B2C 跳转;
    • 跳过登录页展示;
    • 直接重定向回 A2 的 redirect_uri 并带上 code。

8.3 安全类测试

  1. State 篡改:手动修改客户端请求中的 state 参数,验证是否被正确拒绝或检测到不一致。具体步骤:

    • 客户端发起授权请求并得到原始 state;
    • 回调返回时篡改或替换 state;
    • Broker 检查上游 state;
    • 客户端检查 Broker 回调中的原始 state 是否一致。
  2. Code 重放:用同一个 code + code_verifier 换两次 token,验证第二次是否被拒绝(code 一次性)。

  3. PKCE 失配:用错误的 code_verifier 换 token,验证是否被拒绝。

  4. Nonce 重放:在 B2C 回调环节重复提交同一个 nonce,验证是否被识别为重放。(Broker 端测试)

  5. Redirect_uri 篡改:提交一个非白名单内、但与白名单 URI 有相同前缀的 redirect_uri,验证是否被拒绝(防 open redirect)。

  6. Refresh Token 轮换检测:正常 refresh 一次后,再用已经失效的旧 refresh_token 重放,验证是否:

    • 被拒绝;
    • 同时触发对该轮换链上所有 token 的吊销。

8.4 多租户隔离测试

  1. 用 client_id=A1 签发的 access_token,尝试访问需要 client_id=B1 权限范围的 API,验证是否被拒绝。

8.5 登出测试

  1. 触发 /logout,验证:
    • 本地登录 session 是否清除;
    • 清除后重新访问 /authorize(同一 client_id 或不同 client_id)是否需要重新完成 B2C 认证,还是"免密"直接通过(需依据产品策略判断是否符合预期)。

8.6 异常/边界测试

  1. Token endpoint 返回的错误格式是否符合 RFC 6749(error + error_description)。
  2. Authorization code / refresh_token 过期后的行为是否返回标准错误码,而非服务端异常。
  3. 密钥轮换(JWKS 新增一把 key)期间,用旧 key 签发但仍在有效期内的 access_token 是否仍能被正确校验。

九、核心结论回顾

  • 这套 Identity Broker 架构在业界是成熟且被广泛验证的模式。
  • 必须重新签发 id_token,而不能转发 B2C 的 id_token —— 这是协议层面的硬性要求,不是可选的设计偏好。
  • 登录 session 与 client_id 解耦是实现正确 SSO 的关键,也是最容易被"跑通一次主流程"掩盖掉的细节。
  • 上线前的主要风险集中在:state/nonce 的双层桥接是否清晰隔离、code 与 refresh_token 的一次性/轮换机制是否严格、以及登出流程是否与 B2C 侧联动一致。

评论 (0)

暂无评论,快来抢沙发吧!

目录
  • 一、背景与需求说明
  • 二、原理介绍与技术选型
  • 三、认证流程图
  • 四、Step-by-Step 可执行实施步骤
  • 五、关键步骤详细说明
  • 六、注意点 / 风险点
  • 七、生产环境必须补充的安全能力
  • 八、测试场景 / 测试用例清单
  • 九、核心结论回顾