跳转到内容

OIDC 高级接入与能力参考

查看 Markdown

先按 Code + PKCE 接入。RP 需要其他响应类型、签名消息或原生回调时,再使用本页。配置库前,检查部署环境的 discovery。

response_type 授权响应 所需 grant 前通道 ID Token 哈希 刷新能力
code 授权码 authorization_code 无前通道 ID Token 允许 refresh_token 且用户同意 offline_access 时可用
id_token 含获准资料声明的 ID Token implicit 两者都没有 无
id_token token ID Token 和 access token implicit at_hash 无
code id_token 授权码和 ID Token authorization_code、implicit c_hash 允许 refresh_token 且用户同意 offline_access 时可用
code token 授权码和 access token authorization_code、implicit 无前通道 ID Token 无
code id_token token 授权码、ID Token 和 access token authorization_code、implicit c_hash、at_hash 无

每种所选类型都须显式启用。code 默认使用 query;Implicit 和 Hybrid 默认使用 fragment。六种类型都支持 form_post。Implicit 和 Hybrid 不接受 query。前通道返回 ID Token 时,请求必须提供非空 nonce。

含授权码的流程中,公开客户端必须使用 PKCE S256,机密客户端建议使用。纯 Implicit 忽略 PKCE。纯 Implicit 和携带 access token 的 Hybrid 在同意流程前移除 offline_access。Token 端点返回的 ID Token 包含对应 access token 的 at_hash,不含 c_hash。

参数 用途
client_id、response_type、scope 指定客户端和流程。scope 必须包含 openid。
redirect_uri 精确匹配已注册回调。
state 将回调关联到本次授权。
nonce 将 ID Token 关联到请求;前通道 ID Token 必须使用。
code_challenge、code_challenge_method 含授权码流程的 PKCE 关联,只支持 S256。
response_mode 按流程限制选择 query、fragment 或 form_post。
prompt、max_age 控制交互和认证时间要求。见交互行为。
acr_values 以空格分隔的认证上下文偏好。允许敏感操作前,检查返回的 acr。
id_token_hint 提供属于此客户端的有效 ID Token。它本身不能建立可复用登录。
ui_locales 偏好的界面语言,支持 en-US 和 zh-CN。
request、request_uri 选择一种签名 Request Object 传递方式。

请求没有提供对应参数时,使用 default_max_age 和 default_acr_values。require_auth_time 要求 ID Token 包含 auth_time。托管规则使用对应的 camelCase 字段,见返回规则。

动态注册配置 request_object_signing_alg="RS256",托管 OIDC 规则配置 requestObjectSigningAlg="RS256"。配置所选 RP 密钥来源。下例使用该指南生成的私钥。

保存为 signed-request.mjs。将 CLIENT_ID 和 REDIRECT_URI 设置为已注册的值。ISSUER 默认使用生产 issuer。

import { createHash, randomBytes, sign } from 'node:crypto';
import { readFileSync, writeFileSync } from 'node:fs';
const issuer = process.env.ISSUER ?? 'https://oidc.sudomimus.com';
const clientId = process.env.CLIENT_ID;
const redirectUri = process.env.REDIRECT_URI;
if (!clientId || !redirectUri) throw new Error('Set CLIENT_ID and REDIRECT_URI');
const now = Math.floor(Date.now() / 1000);
const verifier = randomBytes(32).toString('base64url');
const state = randomBytes(32).toString('base64url');
const nonce = randomBytes(32).toString('base64url');
const encode = value => Buffer.from(JSON.stringify(value)).toString('base64url');
const header = { alg: 'RS256', kid: 'rp-signing-1', typ: 'JWT' };
const payload = {
iss: clientId,
aud: issuer,
iat: now,
exp: now + 120,
client_id: clientId,
response_type: 'code',
scope: 'openid email profile',
redirect_uri: redirectUri,
state,
nonce,
code_challenge: createHash('sha256').update(verifier).digest('base64url'),
code_challenge_method: 'S256',
};
const input = `${encode(header)}.${encode(payload)}`;
const signature = sign('RSA-SHA256', Buffer.from(input),
readFileSync('rp-private.pem')).toString('base64url');
const request = `${input}.${signature}`;
writeFileSync('pending-authorization.json', JSON.stringify({
issuer, clientId, redirectUri, state, nonce, verifier,
}), { mode: 0o600, flag: 'wx' });
writeFileSync('request.jwt', request, { mode: 0o600, flag: 'wx' });
const url = new URL(`${issuer}/authorize`);
url.search = new URLSearchParams({
client_id: clientId,
response_type: 'code',
scope: 'openid email profile',
request,
}).toString();
console.log(url.toString());

将用户浏览器跳转到输出的 URL。待完成的请求信息保留在后端。回调时验证 state 和 iss,用保存的 verifier 兑换授权码,再验证 ID Token 的 nonce。示例文件只表示一次请求;实际 RP 需要能隔离并发请求、清理到期数据的存储。

使用 request_uri 时,通过公开 HTTPS URL 提供 request.jwt 的紧凑 JWT 内容。外层发送 client_id、response_type、包含 openid 的 scope 和 request_uri,不再发送 request。已注册的 request_uris 列表非空时,会限制可用引用;URI fragment 不改变获取的引用。

签名对象中的值覆盖其他外层参数。内层提供 client_id 和 response_type 时,必须与外层一致。提供 iss 时必须指向客户端,aud 必须包含 issuer。时间声明也会被验证。不接受未签名、加密或嵌套的 Request Object。不要将 token 端点的客户端 assertion 用作 Request Object,它们的 audience 和内容不同。

动态注册配置 userinfo_signed_response_alg="RS256",托管规则配置 userinfoSignedResponseAlg="RS256"。然后调用 discovery 中的 userinfo_endpoint:

终端窗口
curl "$USERINFO_ENDPOINT" \
-H "Authorization: Bearer $ACCESS_TOKEN"

响应是 application/jwt,不是 JSON。使用提供方 discovery 中的 jwks_uri 验证 RS256,按精确 kid 选择密钥,并验证 iss、aud 和到期时间。使用资料声明前,将 sub 与 ID Token 的 subject 比较。不要使用应用 access-token JWKS 验证签名 UserInfo。

签名响应按签发时的 scope、策略和用户同意返回资料。它是快照,撤销同意不能收回已交付的 JWT。Accept header 不会覆盖客户端注册的响应格式。

在普通授权请求中加入 response_mode=form_post。例如:

https://oidc.sudomimus.com/authorize
?client_id=example-rp
&redirect_uri=https%3A%2F%2Fapp.example.com%2Foidc%2Fcallback
&response_type=code
&response_mode=form_post
&scope=openid
&state=<transaction state>
&nonce=<transaction nonce>
&code_challenge=<S256 challenge>
&code_challenge_method=S256

HTTP(S) 回调收到的表单 POST 类似:

POST /oidc/callback HTTP/1.1
Host: app.example.com
Content-Type: application/x-www-form-urlencoded
code=<authorization-code>&state=<transaction-state>&iss=https%3A%2F%2Foidc.sudomimus.com

解析表单,再验证已保存请求的 state 和 iss。使用原始回调 URI 和 verifier 兑换授权码。Implicit 或 Hybrid 返回令牌时,先验证 ID Token、nonce 和适用哈希,再接受令牌。

如果用 Cookie 查找待完成的请求,Cookie 必须通过 SameSite=None; Secure 支持跨站 POST。不要因此跳过请求关联验证。原生自定义 scheme 不能接收 Form Post。

为托管原生客户端配置以下规则:

{
"returnMethod": "OIDC",
"payload": {
"applicationType": "native",
"redirectUris": ["com.example.app:/oidc/callback", "http://127.0.0.1:49152/oidc/callback"],
"postLogoutRedirectUris": [],
"allowedResponseTypes": ["code"],
"allowedGrantTypes": ["authorization_code"],
"allowedScopes": ["openid", "email", "profile"],
"tokenEndpointAuthMethod": "none"
}
}

通过系统浏览器启动 Code + PKCE。用已注册的自定义 scheme 或回环监听器接收响应。验证 state 和 iss 后,使用同一个精确回调 URI 和 verifier 兑换授权码。不要在安装到用户设备的应用中嵌入客户端密钥或平台私钥。

原生 HTTP 回调必须使用 localhost 或回环地址。application_type=native 不接受 HTTPS 回调。精确匹配包含端口;示例不允许任意回环端口。动态注册没有主机的自定义 scheme 时,还需要 sector 文档确定获准的归属。见注册元数据。

提供方支持成对 subject、RS256 签名和上表的响应类型。不支持 OAuth-only response_type=token、匿名动态注册、通过 RAT 更新元数据,或加密的 ID Token、UserInfo 和 Request Object。

第三方发起登录使用 RP 已注册的 initiate_login_uri。RP 接受 GET 和 POST,验证 iss,再启动普通授权请求。Issuer-host WebFinger 位于 /.well-known/webfinger,用于发现 issuer,不应据此假定邮箱域名映射。