---
title: OIDC 流程
description: 通过 discovery、authorization code + PKCE、/token、/userinfo 与
  /end-session 接入 OpenID Connect。
editUrl: true
head: []
template: doc
sidebar:
  order: 1
  hidden: false
  attrs: {}
pagefind: true
draft: false
---

import { Tabs, TabItem } from "@astrojs/starlight/components";

如果应用已经支持 **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 协议](/zh-cn/connect/flow/) 通常更短。

## Discovery

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

```bash
curl https://oidc.sudomimus.com/.well-known/openid-configuration
```

discovery 文档声明：

- **`response_types_supported`**：`["code"]`（仅 authorization code flow）。
- **`grant_types_supported`**：`["authorization_code", "refresh_token"]`。
- **`scopes_supported`**：`["openid", "email", "profile", "offline_access"]`。
- **`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"]`。

## 注册应用

在 [`with.sudomimus.com`](https://with.sudomimus.com) 中，为需要启用 OIDC 的应用完成以下配置：

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

   ```json
   {
     "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_USERNAMELESS` 或 `PASSKEY_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。**

应用的 **`applicationAnchor`** 就是你的 `client_id`。

## OIDC 流程

### 1. 授权请求

把用户浏览器跳转到 `/authorize`，附带标准 OIDC 参数：

```text
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
  &state=<csrf-token>
  &nonce=<random-nonce>
  &code_challenge=<S256-of-verifier>
  &code_challenge_method=S256
```

必填：`client_id`、`redirect_uri`、`response_type=code`、`scope`（必须包含 `openid`）、`code_challenge`、`code_challenge_method=S256`。
选填（建议带上）：`state`、`nonce`。

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

### 2. 令牌交换

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

<Tabs syncKey="oidc-client">
<TabItem label="private_key_jwt">
```bash
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_id`、`sub = client_id`、`aud = 完整且精确的 token endpoint URL`（生产环境为 `https://oidc.sudomimus.com/token`）、全新的 `jti`、`iat` 和 `exp`（与 `iat` 相差不超过 300 秒）。签名算法为 RS256。
</TabItem>
<TabItem label="none (PKCE)">
```bash
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"
```

不提交 client assertion。PKCE（`code_verifier` 与授权请求的 `code_challenge` 匹配）是唯一的客户端认证。
</TabItem>
</Tabs>

成功响应（JSON）：

```json
{
  "access_token": "<JWT>",
  "token_type": "Bearer",
  "expires_in": 10800,
  "id_token": "<JWT>",
  "scope": "openid email profile",
  "refresh_token": "<JWT —— 只在请求了 'offline_access' 时返回>"
}
```

- **`id_token`** —— 由 Sudomimus 的平台级 OIDC 密钥签发，通过 `https://oidc.sudomimus.com/.well-known/jwks.json` 验证。它包含标准 OIDC 声明（`iss`、`sub`、`aud`、`exp`、`iat`，以及可选的 `nonce`、`email`、`name`）。
- **`access_token`** —— 由应用的 token-signing 密钥签发，结构与 Connect 协议的访问令牌相同。
- **`refresh_token`** —— 仅在请求 `offline_access` scope 时返回。

### 3. Userinfo

```bash
curl https://oidc.sudomimus.com/userinfo \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

返回 scope 允许的声明：

```json
{
  "sub": "<sector subject>",
  "email": "<只在授予 'email' scope 时返回>",
  "name": "<只在授予 'profile' scope 时返回>"
}
```

`sub` 是 **扇区主体（sector subject）** —— 按扇区维度、应用可见的标识符（与 `id_token` 的 `sub` 相同），而非原始账户 UUID。`/userinfo` 同时支持 `GET` 与 `POST`。

### 4. Refresh

如果请求了 `offline_access` 并拿到了 refresh token，去 `/token` 兑换：

```bash
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
```

可以传入可选的 `scope`，请求原始授权范围的子集。根据 [OIDC §12.1](https://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken)，刷新得到的 ID token 不会包含新的 `nonce`。

### 5. 结束会话

```text
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 完全匹配）。它**不会**自动吊销 refresh token —— 真正使底层会话失效要调 Connect 的 `/logout`（单个会话）或 `/revoke-all`（账户的所有会话）。详见 [管理会话](/zh-cn/guides/managing-sessions/)。

## 使用 OIDC 库

绝大多数现代 OIDC 库（Node 的 `openid-client`、Python 的 `authlib`、Java 的 `nimbus-jose-jwt` 等）都能从 issuer URL 自动发现，并自动处理 PKCE、JWKS、令牌验证。一个最小的 Node 示例：

```js
import { Issuer } from "openid-client";

const issuer = await Issuer.discover("https://oidc.sudomimus.com");
const client = new issuer.Client({
    client_id: "my-app",
    redirect_uris: ["https://app.example.com/oidc/callback"],
    response_types: ["code"],
    token_endpoint_auth_method: "none",   // 或 "private_key_jwt"
});

// 登录时：
const codeVerifier = generators.codeVerifier();
const codeChallenge = generators.codeChallenge(codeVerifier);
const authUrl = client.authorizationUrl({
    scope: "openid email profile",
    code_challenge: codeChallenge,
    code_challenge_method: "S256",
    state, nonce,
});

// 回调时：
const params = client.callbackParams(req);
const tokenSet = await client.callback(redirectUri, params, { code_verifier: codeVerifier, state, nonce });
const userinfo = await client.userinfo(tokenSet.access_token);
```

## 令牌验证提醒

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

但 `/token` 返回的 `access_token` **不是**使用平台 JWKS 验证的令牌。它是应用专属的访问令牌，结构与 Connect 流程相同，需要使用 `POST /info` 返回的 `applicationPublicKey` 验证。如果客户端库会在本地验证访问令牌，请为它配置 `/info` 提供的密钥；也可以将访问令牌视为不透明字符串，只通过 `/userinfo` 获取声明。详见[令牌与验证](/zh-cn/concepts/tokens-and-verification/)。