跳转到内容

OIDC 流程

查看 Markdown

如果应用已经支持 OpenID Connect,或者你希望直接使用现有 OIDC 库,可以将 Sudomimus 作为标准 OIDC 提供方接入。Sudomimus 的 OIDC provider 部署在 oidc.sudomimus.com,支持带 PKCE 的授权码流程。你的应用在该流程中是接入方(Relying Party,RP)。

适用场景:

  • 你的框架或平台原生支持 OIDC(Next-Auth、Spring Security、Keycloak adapter 等),希望直接把 Sudomimus 接入做 IdP。
  • 你要和某个已经只接 OIDC 的伙伴系统对接。
  • 你更熟悉 OIDC 的 client、scope 和 ID token 概念,不希望使用 Connect 协议。

如果你从零开始、只想做最小的自定义集成,Connect 协议 通常更短。

官方 SDK 主要覆盖 Connect、Session、Device、Native 与 token 验证辅助。如果你的 OIDC 应用还需要验证 Sudomimus 应用 access token,或直接管理 ApplicationSession,请参考 SDK

Sudomimus 发布标准 OIDC discovery 文档。将库的 issuer 设置为 https://oidc.sudomimus.com 后,客户端库可以自动读取其余配置:

Terminal window
curl https://oidc.sudomimus.com/.well-known/openid-configuration

Sudomimus 不另外发布 OIDC OpenAPI schema。请通过 discovery 获取当前部署的 端点 URL 与已声明能力,通过 OpenID Connect 和 OAuth 标准理解协议语义,并以 本指南确认 Sudomimus 支持的 profile 与平台约束。生成的 OpenAPI 参考用于 Connect、Session、Native 和 Device 产品 API。

discovery 文档声明:

  • response_types_supported["code"](仅 authorization code flow)。
  • authorization_response_iss_parameter_supportedtrue —— 授权回调按照 RFC 9207 标识提供方。
  • grant_types_supported["authorization_code", "refresh_token"]
  • scopes_supported["openid", "email", "profile", "offline_access"]
  • claims_supported 包含 subemailemail_verifiednamegiven_namefamily_namepicturepicture_animated
  • claim_state_endpoint 指向 Sudomimus 提供方专属的实时 claim 策略与同意状态端点。
  • id_token_signing_alg_values_supported["RS256"]
  • code_challenge_methods_supported["S256"] —— 强制 PKCE,不支持 plain。
  • token_endpoint_auth_methods_supported["private_key_jwt", "client_secret_basic", "client_secret_post", "none"]
  • ui_locales_supported["en-US", "zh-CN"]

with.sudomimus.com 中,为需要启用 OIDC 的应用完成以下配置:

  1. 添加一条 Layer 3 OIDC 返回规则:

    {
    "returnMethod": "OIDC",
    "payload": {
    "redirectUris": ["https://app.example.com/oidc/callback"],
    "postLogoutRedirectUris": ["https://app.example.com/"],
    "allowedScopes": ["openid", "email", "profile", "offline_access"],
    "tokenEndpointAuthMethod": "private_key_jwt"
    }
    }
  2. 与其他应用一样配置 Layer 1 和 Layer 2:至少添加一条认证规则(例如 PASSKEY_USERNAMELESSPASSKEY_REASONED)和一条身份准入规则(例如允许特定邮箱的 EMAIL 规则)。OIDC 流程仍使用平台的同一套用户认证挑战。

  3. 选择客户端认证方式

    • private_key_jwt(推荐用于机密客户端)—— RP 持有私钥,在 /token 上用 JWT assertion 做客户端认证。签名密钥就是应用的 client-auth 私钥;机密 Connect 集成调用 /establish 时也使用这把密钥。
    • client_secret_basic(机密客户端)—— RP 在 /token 的 HTTP Authorization: Basic 头中携带共享密钥。
    • client_secret_post(机密客户端)—— RP 在 /token 的表单体中发送共享密钥(client_id + client_secret 参数)。
    • none —— 仅限公开客户端(SPA、无后端的移动应用)。必须使用 PKCE。

client_secret_basicclient_secret_post 共用同一把应用密钥。请在 With 门户的应用页面生成或轮换它,并妥善保存。

应用的 applicationAnchor 就是你的 client_id

把用户浏览器跳转到 /authorize,附带标准 OIDC 参数:

https://oidc.sudomimus.com/authorize
?client_id=my-app
&redirect_uri=https%3A%2F%2Fapp.example.com%2Foidc%2Fcallback
&response_type=code
&scope=openid%20email%20profile
&ui_locales=zh-CN%20en-US
&state=<csrf-token>
&nonce=<random-nonce>
&code_challenge=<S256-of-verifier>
&code_challenge_method=S256

必填:client_idredirect_uriresponse_type=codescope(必须包含 openid)、code_challengecode_challenge_method=S256。 选填(建议带上):statenonce

如果应用已知用户偏好的界面语言,可以传入可选的 ui_locales。请按偏好顺序填写以空格分隔的 BCP 47 语言标签。Sudomimus 会采用第一个支持的值(en-USzh-CN);不支持的值会被忽略,不会阻止登录。这个提示只作用于本次登录,用户仍可在页面中切换语言。

建议让 OIDC 客户端库生成 PKCE 参数。每次发起授权都要生成一组新参数,并把 verifier 与待完成的登录状态一起保存到回调完成为止。

如果需要自行生成:

  • code_verifier 长度必须为 43–128 个字符,只能使用英文字母、数字、-._~
  • code_challenge 是 verifier 的 SHA-256 摘要经过无填充 base64url 编码后的结果。S256 challenge 固定为 43 个字符,只能使用英文字母、数字、-_
  • /authorize 只发送 code_challenge;在 /token 兑换授权码时,发送原始且未经修改的 code_verifier

不要重复使用 verifier,也不要把文档里的固定示例用于生产环境。Sudomimus 会拒绝格式不正确的参数和不匹配的 verifier/challenge。

多数应用可以不传 promptmax_age,直接使用普通的交互式登录。只有在应用确实需要指定行为时才添加这些参数:

  • prompt=login 要求重新认证。Sudomimus 的每次交互式 OIDC 请求本来都会执行一次全新认证。
  • prompt=consent 会再次展示授权确认,即使用户已经保存了长期的声明共享选择。
  • prompt=none 用于非交互式检查。Sudomimus 不保留可复用的 provider 浏览器会话,因此会返回 login_required;应用应捕获该错误,再发起交互式授权请求。
  • max_age 是非负的秒数。由于每次交互式请求都会重新认证,任何有效的 max_age 都会得到满足。

只有当应用需要在用户离开或关闭应用后继续刷新令牌时,才请求 offline_access。Sudomimus 会向用户展示一个默认未选中的独立离线会话选项。如果用户拒绝,登录仍会成功,但返回的 scope 不包含 offline_access,也不会签发 refresh token。应用应以返回的 scope 和是否存在 refresh_token 为准。

Sudomimus 会将用户跳转到 via.sudomimus.com,完成 Layer 1 允许的认证方式。认证和身份准入检查成功后,浏览器会携带 code、原始 stateiss=https://oidc.sudomimus.com 返回 redirect_uri

Sudomimus 验证注册回调地址后发生的授权错误,会携带 errorerror_description、原始 state 和同一个 iss 返回该回调。OIDC 库必须把 iss 与本次登录保存的 issuer 做精确比较,并在缺失或不一致时于兑换授权码之前拒绝响应。回调地址验证之前的错误则直接以 JSON 返回。请把 server_error 视为暂时性的服务端故障,让用户稍后重试,并且不要缓存协议错误响应。

/token 提交授权码。按照 OIDC 规范,请求体必须使用 application/x-www-form-urlencoded

Terminal window
curl -X POST https://oidc.sudomimus.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$AUTH_CODE" \
--data-urlencode "redirect_uri=https://app.example.com/oidc/callback" \
--data-urlencode "code_verifier=$PKCE_VERIFIER" \
--data-urlencode "client_id=my-app" \
--data-urlencode "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
--data-urlencode "client_assertion=$CLIENT_ASSERTION_JWT"

client_assertion 是使用应用 client-auth 私钥签名的 JWT。必需声明包括:iss = client_idsub = client_idaud = 完整且精确的 token endpoint URL(生产环境为 https://oidc.sudomimus.com/token)、全新的 jtiiatexp(与 iat 相差不超过 300 秒)。签名算法为 RS256。

成功响应(JSON):

{
"access_token": "<JWT>",
"token_type": "Bearer",
"expires_in": 10800,
"id_token": "<JWT>",
"scope": "openid email profile"
}
  • id_token —— 由 Sudomimus 的平台级 OIDC 密钥签发,通过 https://oidc.sudomimus.com/.well-known/jwks.json 验证。它包含最小协议声明(isssubaudexpiatat_hash,以及可选的 nonceauth_time)和认证上下文(amracr)。个人资料从 /userinfo 获取。
  • access_token —— 由应用的 token-signing 密钥签发,结构与 Connect 协议的访问令牌相同。
  • refresh_token —— 仅在请求了 offline_access 且用户允许离线会话时返回。
Terminal window
curl https://oidc.sudomimus.com/userinfo \
-H "Authorization: Bearer $ACCESS_TOKEN"

返回 scope 允许的声明:

{
"sub": "<sector subject>",
"email": "<只在授予 'email' scope 时返回>",
"email_verified": true,
"name": "<只在授予 'profile' scope 时返回>",
"given_name": "<只在授予 'profile' scope 时返回>",
"family_name": "<只在授予 'profile' scope 时返回>",
"picture": "<只在授予 'profile' scope 时返回>",
"picture_animated": "<只在授予 'profile' scope 时返回>"
}

sub扇区主体(sector subject) —— 按扇区维度、应用可见的标识符,与 id_tokensub 相同。请用它作为用户键。/userinfo 同时支持 GETPOST

picture 与私有 claim picture_animated 遵循扇区头像交付约定;见头像声明与交付

如果应用需要当前策略与同意状态,请携带同一个 Bearer access token 调用 discovery 中的 claim_state_endpoint。它同时支持 GET 与 POST,并且不会返回资料值:

{
"sub": "<sector subject>",
"claims": {
"email": { "requirement": "OPTIONAL", "state": "GRANTED" },
"given_name": { "requirement": "OPTIONAL", "state": "GRANTED" },
"family_name": { "requirement": "OFF", "state": "UNKNOWN" },
"picture": { "requirement": "OFF", "state": "UNKNOWN" },
"picture_animated": { "requirement": "OFF", "state": "UNKNOWN" }
}
}

只返回 email scope 与 profile scope 覆盖的条目。只有 openid scope 时, claims 是空对象。

每个 id_token 都包含:

声明含义
amr标准 Authentication Methods References 值。例如 passkey 是 ["hwk", "user"],邮箱验证码是 ["otp"]
acrSudomimus 的认证上下文字符串。例如 urn:sudomimus:acr:passkeyurn:sudomimus:acr:email-otp

如果你的 RP 在敏感操作前需要确认用户刚才用了抗钓鱼方式登录,请检查 acr 是否为 urn:sudomimus:acr:passkey。Google、GitHub、Discord、Battle.net、X、Steam 和企业联合登录这类上游身份源都会映射到 amr: ["pwd"];需要区分具体来源时,请看 acr

如果请求了 offline_access、用户允许离线会话,并且响应中包含 refresh token,再到 /token 兑换:

Terminal window
curl -X POST https://oidc.sudomimus.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "refresh_token=$REFRESH_TOKEN" \
--data-urlencode "client_id=my-app"
# 机密客户端再加 client_assertion

刷新时应继续使用授权码兑换时选择的客户端认证方式。使用 client_secret_basic 时,通过 -u "my-app:$CLIENT_SECRET" 发送凭据,并省略 表单中的 client_id;使用 client_secret_post 时,在表单中同时发送 client_idclient_secret

可以传入可选的 scope,请求原始授权范围的子集。根据 OIDC §12.1,刷新得到的 ID token 不会包含新的 nonce

https://oidc.sudomimus.com/end-session
?id_token_hint=<id_token>
&client_id=my-app
&post_logout_redirect_uri=https%3A%2F%2Fapp.example.com%2F
&state=<可选>

/end-session 通知 OIDC 会话结束,并把用户跳转到 post_logout_redirect_uri(必须与注册过的某个 URI 完全匹配)。如果 id_token_hint 验证通过,并且其中的 aud 与当前 client 匹配,Sudomimus 会推进账户/应用会话权威,并吊销该账户在这个应用下的旧 ApplicationSession。

伪造、无效或已经无法对应到账户的 hint 不会阻止跳转,只是不会触发应用会话吊销。hint 可以是过期的,这样 RP 即使本地会话已经超时,也仍然可以发起一次干净的 logout。

如果你手上有某一个 refresh token,只想吊销这一条会话,请调用 Session API /logout。如果后端需要在不依赖 id_token_hint 的情况下吊销某个账户在应用内的所有会话,请调用 /revoke-all。详见 管理会话

绝大多数现代 OIDC 库(Node 的 openid-client、Python 的 authlib、Java 的 nimbus-jose-jwt 等)都能从 issuer URL 自动发现,并自动处理 PKCE、JWKS、令牌验证。这个 Node 示例支持 openid-client 6.x(使用 6.8.4 验证),采用注册为 token_endpoint_auth_method: "none" 的公共 PKCE 客户端:

import * as oidc from "openid-client";
const redirectUri = "https://app.example.com/oidc/callback";
const configuration = await oidc.discovery(
new URL("https://oidc.sudomimus.com"),
"my-app",
{
redirect_uris: [redirectUri],
response_types: ["code"],
token_endpoint_auth_method: "none",
},
oidc.None(),
{
execute: [oidc.enableNonRepudiationChecks],
},
);
export const startSignIn = async (savePendingSignIn) => {
const codeVerifier = oidc.randomPKCECodeVerifier();
const codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier);
const state = oidc.randomState();
const nonce = oidc.randomNonce();
await savePendingSignIn({
codeVerifier,
state,
nonce,
});
return oidc.buildAuthorizationUrl(configuration, {
redirect_uri: redirectUri,
scope: "openid email profile",
code_challenge: codeChallenge,
code_challenge_method: "S256",
state,
nonce,
});
};
export const finishSignIn = async (
callbackUrl,
consumePendingSignIn,
createServerSideApplicationSession,
) => {
const pendingSignIn = await consumePendingSignIn();
if (pendingSignIn === undefined) {
throw new Error("OIDC sign-in state is missing or expired");
}
const tokens = await oidc.authorizationCodeGrant(
configuration,
callbackUrl,
{
pkceCodeVerifier: pendingSignIn.codeVerifier,
expectedState: pendingSignIn.state,
expectedNonce: pendingSignIn.nonce,
},
);
const claims = tokens.claims();
if (claims?.sub === undefined) {
throw new Error("The validated ID token did not contain sub");
}
const userinfo = await oidc.fetchUserInfo(
configuration,
tokens.access_token,
claims.sub,
);
await createServerSideApplicationSession({
userKey: claims.sub,
refreshToken: tokens.refresh_token,
});
return userinfo;
};

savePendingSignInconsumePendingSignIn 表示与浏览器本次登录绑定的短期服务端存储。回调时只能消费一次 verifier、state 和 nonce。authorizationCodeGrant 会验证授权响应中的 discovery RFC 9207 issuer 与预期 state,并验证 ID-token nonce;配置的 non-repudiation check 还会通过 discovery 获得的 JWKS 验证 ID-token 签名。全部验证成功后,claims() 才会提供这些 claims。

createServerSideApplicationSession 表示应用自己的会话存储。请使用验证后的、限定于应用的 sub 作为用户键,把 refresh token 留在服务端,并且只向浏览器发送应用自己的 session cookie。不要把返回的 token response 序列化进 cookie。

OIDC ID token 通过 oidc.sudomimus.com/.well-known/jwks.json 上的 JWKS 验证 —— 这是 OIDC 标准机制,OIDC 库会自动做。

/token 返回的 access_token 不是使用平台级 OIDC JWKS 验证的令牌。它是结构与 Connect 相同的应用专属访问令牌:payload sub 是配对用户键,sid 是 ApplicationSession,jti 是这个 token 实例。需要按 kid 使用 https://session-api.sudomimus.com/applications/{applicationAnchor}/jwks.json 验证。如果 OIDC 库无法为 access token 配置独立 JWKS,也可以将它视为不透明字符串,只通过 /userinfo 获取声明。详见令牌与验证