@eduwork/dsh-oidc 0.1.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +120 -0
- package/CHANGELOG.md +120 -0
- package/LICENSE +21 -0
- package/README.en.md +184 -0
- package/README.md +143 -0
- package/THIRD_PARTY_NOTICES.en.md +51 -0
- package/THIRD_PARTY_NOTICES.md +53 -0
- package/cordis.patch.yml +8 -0
- package/docs/EDUWORK-MIGRATION.en.md +23 -0
- package/docs/EDUWORK-MIGRATION.md +23 -0
- package/docs/architecture.en.md +125 -0
- package/docs/architecture.md +125 -0
- package/docs/compatibility.en.md +61 -0
- package/docs/compatibility.md +61 -0
- package/docs/decisions/0001-one-plugin-composition-boundary.en.md +34 -0
- package/docs/decisions/0001-one-plugin-composition-boundary.md +34 -0
- package/docs/dsh-integration.en.md +171 -0
- package/docs/dsh-integration.md +171 -0
- package/docs/ecnu-reference.en.md +66 -0
- package/docs/ecnu-reference.md +66 -0
- package/docs/enterprise-profile.en.md +159 -0
- package/docs/enterprise-profile.md +159 -0
- package/docs/getting-started.en.md +137 -0
- package/docs/getting-started.md +239 -0
- package/docs/key-binding-protocol.en.md +227 -0
- package/docs/key-binding-protocol.md +227 -0
- package/docs/oidc-interoperability.en.md +100 -0
- package/docs/oidc-interoperability.md +100 -0
- package/docs/release-checklist.en.md +60 -0
- package/docs/release-checklist.md +60 -0
- package/docs/security-model.en.md +138 -0
- package/docs/security-model.md +138 -0
- package/docs/server-integration-contract.en.md +706 -0
- package/docs/server-integration-contract.md +706 -0
- package/examples/ecnu.enterprise-profile.example.json +81 -0
- package/examples/enterprise-profile.example.json +77 -0
- package/lib/client.js +5855 -0
- package/lib/index.js +59 -0
- package/lib/oidc.js +649 -0
- package/lib/profile.js +371 -0
- package/lib/provider/core.js +138 -0
- package/lib/provider/index.js +156 -0
- package/lib/provider/transform-adapter.js +65 -0
- package/lib/provider/transforms.js +59 -0
- package/lib/typert-schemas.js +84 -0
- package/lib/typert.host.js +38 -0
- package/lib/typert.remote-client.js +37 -0
- package/package.json +120 -0
- package/protocol/openapi.yaml +257 -0
- package/schema/enterprise-profile.v1alpha1.schema.json +174 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
# dsh-oidc 服务端接口规范
|
|
2
|
+
|
|
3
|
+
**简体中文** | [English](server-integration-contract.en.md)
|
|
4
|
+
|
|
5
|
+
本文面向身份平台、模型平台和联调测试人员,以接口说明的形式定义接入 `dsh-oidc` 时服务端必须提供的能力。
|
|
6
|
+
|
|
7
|
+
关键词“必须”“不得”“应该”“可以”具有规范性含义。OIDC 接口遵循 OpenID Connect 与 OAuth 2.0 标准;Key Binding 是本项目定义的固定协议;模型网关使用 OpenAI Chat Completions 兼容接口。
|
|
8
|
+
|
|
9
|
+
## 1. 接入范围
|
|
10
|
+
|
|
11
|
+
一个完整的**机构企业模型集成服务**必须同时提供以下三组接口:
|
|
12
|
+
|
|
13
|
+
| 接口组 | 用途 | Enterprise Profile 配置 | 要求 |
|
|
14
|
+
| --- | --- | --- | --- |
|
|
15
|
+
| OIDC Provider | 登录、用户身份、OIDC Access Token | `oidc.issuer`、`oidc.clientId`、`oidc.scopes` | 必须完整实现 |
|
|
16
|
+
| Key Binding | 将已认证用户绑定到可撤销的模型运行凭据 | `keyBinding.baseURL` | 四个接口全部必须实现 |
|
|
17
|
+
| 模型网关 | 使用绑定后的运行凭据调用企业模型 | `provider.baseURL`、`provider.id`、`provider.models` | 必须实现 Chat Completions |
|
|
18
|
+
|
|
19
|
+
“一个服务”是指一套完整的产品交付、版本、安全和运维责任,不要求所有接口使用相同域名、网关或进程。机构可以在内部拆分身份平台、用户中心和模型平台,但不能只交付其中一部分并声明已经完成 `dsh-oidc` 接入。
|
|
20
|
+
|
|
21
|
+
以下组合不符合本规范:
|
|
22
|
+
|
|
23
|
+
- 只实现 OIDC,然后向所有用户下发同一个全局模型 API Key;
|
|
24
|
+
- 只实现 Key Binding,并相信客户端提交的工号、姓名或 subject;
|
|
25
|
+
- 只提供模型地址和模型列表,不实现用户授权与凭据生命周期;
|
|
26
|
+
- 缺少 `bootstrap`、`provision`、`resolve`、`renew` 中任意一个 Key Binding 接口。
|
|
27
|
+
|
|
28
|
+
## 2. 地址与客户端注册
|
|
29
|
+
|
|
30
|
+
### 2.1 三个服务入口
|
|
31
|
+
|
|
32
|
+
Enterprise Profile 分别声明三个入口:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"oidc": {
|
|
37
|
+
"issuer": "https://id.example.edu/oidc",
|
|
38
|
+
"clientId": "dsh-web-public-client",
|
|
39
|
+
"scopes": ["openid", "profile", "offline_access"]
|
|
40
|
+
},
|
|
41
|
+
"keyBinding": {
|
|
42
|
+
"baseURL": "https://ai.example.edu/api/worker/v1"
|
|
43
|
+
},
|
|
44
|
+
"provider": {
|
|
45
|
+
"id": "example-ai",
|
|
46
|
+
"baseURL": "https://ai.example.edu/open/api/v1",
|
|
47
|
+
"adapter": "openai-compatible",
|
|
48
|
+
"models": [
|
|
49
|
+
{ "id": "example-model", "input": ["text"] }
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
生产环境中的 `issuer`、`keyBinding.baseURL` 和 `provider.baseURL` 必须使用 HTTPS。URL 不得包含账号密码或 fragment;基础 URL 不得依靠 query 参数改变路由行为。
|
|
56
|
+
|
|
57
|
+
### 2.2 OIDC Public Client
|
|
58
|
+
|
|
59
|
+
身份平台必须注册一个不带 Client Secret 的 Public Client,并启用:
|
|
60
|
+
|
|
61
|
+
- Authorization Code Flow;
|
|
62
|
+
- PKCE `S256`;
|
|
63
|
+
- `openid`、`profile` scope;
|
|
64
|
+
- 如需 Refresh Token,再启用 `offline_access` 及相应授权策略。
|
|
65
|
+
|
|
66
|
+
回调地址必须精确登记为:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
http://127.0.0.1:3080/oauth/callback
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
约束如下:
|
|
73
|
+
|
|
74
|
+
- host 固定为 `127.0.0.1`,不得改成 `localhost`、局域网 IP 或公网域名;
|
|
75
|
+
- path 固定为 `/oauth/callback`;
|
|
76
|
+
- `3080` 是推荐端口;如果 DSH WebServer 使用其他端口,登记地址必须同步使用实际端口;
|
|
77
|
+
- 这是浏览器返回用户本机 DSH 的地址,不是机构服务端地址;
|
|
78
|
+
- 当前实现是本机单用户 Web 组合,不支持把回调部署成共享公网 Web 会话。
|
|
79
|
+
|
|
80
|
+
## 3. 接口总表
|
|
81
|
+
|
|
82
|
+
| 编号 | 方法 | 地址 | 认证 | 是否必须 |
|
|
83
|
+
| --- | --- | --- | --- | --- |
|
|
84
|
+
| OIDC-01 | `GET` | `{issuer}/.well-known/openid-configuration` | 无 | 是 |
|
|
85
|
+
| OIDC-02 | `GET` | Discovery 的 `authorization_endpoint` | 用户登录会话 | 是 |
|
|
86
|
+
| OIDC-03 | `POST` | Discovery 的 `token_endpoint` | Authorization Code + PKCE | 是 |
|
|
87
|
+
| OIDC-04 | `GET` | Discovery 的 `jwks_uri` | 无 | 是 |
|
|
88
|
+
| OIDC-05 | `GET` | Discovery 的 `userinfo_endpoint` | OIDC Bearer Access Token | 是 |
|
|
89
|
+
| OIDC-06 | `POST` | Discovery 的 `revocation_endpoint` | OIDC Token | 建议 |
|
|
90
|
+
| KEY-01 | `GET` | `{keyBinding.baseURL}/bootstrap` | OIDC Bearer Access Token | 是 |
|
|
91
|
+
| KEY-02 | `POST` | `{keyBinding.baseURL}/runtime-credential/provision` | OIDC Bearer Access Token | 是 |
|
|
92
|
+
| KEY-03 | `POST` | `{keyBinding.baseURL}/runtime-credential/resolve` | OIDC Bearer Access Token | 是 |
|
|
93
|
+
| KEY-04 | `POST` | `{keyBinding.baseURL}/runtime-credential/renew` | OIDC Bearer Access Token | 是 |
|
|
94
|
+
| MODEL-01 | `POST` | `{provider.baseURL}/chat/completions` | 运行时 API Key | 是 |
|
|
95
|
+
|
|
96
|
+
## 4. OIDC 接口组
|
|
97
|
+
|
|
98
|
+
### OIDC-01:Discovery
|
|
99
|
+
|
|
100
|
+
读取 OIDC Provider 元数据。
|
|
101
|
+
|
|
102
|
+
#### 请求
|
|
103
|
+
|
|
104
|
+
```http
|
|
105
|
+
GET /oidc/.well-known/openid-configuration HTTP/1.1
|
|
106
|
+
Host: id.example.edu
|
|
107
|
+
Accept: application/json
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
当 `issuer` 本身带路径时,Discovery 地址为:
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
{issuer去除末尾斜杠}/.well-known/openid-configuration
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### 成功响应
|
|
117
|
+
|
|
118
|
+
```http
|
|
119
|
+
HTTP/1.1 200 OK
|
|
120
|
+
Content-Type: application/json
|
|
121
|
+
|
|
122
|
+
{
|
|
123
|
+
"issuer": "https://id.example.edu/oidc",
|
|
124
|
+
"authorization_endpoint": "https://id.example.edu/oidc/authorize",
|
|
125
|
+
"token_endpoint": "https://id.example.edu/oidc/token",
|
|
126
|
+
"jwks_uri": "https://id.example.edu/oidc/jwks",
|
|
127
|
+
"userinfo_endpoint": "https://id.example.edu/oidc/userinfo",
|
|
128
|
+
"revocation_endpoint": "https://id.example.edu/oidc/revoke",
|
|
129
|
+
"code_challenge_methods_supported": ["S256"],
|
|
130
|
+
"id_token_signing_alg_values_supported": ["RS256"]
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### 字段要求
|
|
135
|
+
|
|
136
|
+
| 字段 | 必需 | 要求 |
|
|
137
|
+
| --- | --- | --- |
|
|
138
|
+
| `issuer` | 是 | 必须与 Enterprise Profile 的 `oidc.issuer` 逐字符相同 |
|
|
139
|
+
| `authorization_endpoint` | 是 | 绝对 HTTPS URL |
|
|
140
|
+
| `token_endpoint` | 是 | 绝对 HTTPS URL |
|
|
141
|
+
| `jwks_uri` | 是 | 绝对 HTTPS URL |
|
|
142
|
+
| `userinfo_endpoint` | 是 | 本互操作规范强制要求 |
|
|
143
|
+
| `revocation_endpoint` | 否 | 提供后,客户端退出时会尽力撤销 Token |
|
|
144
|
+
| `code_challenge_methods_supported` | 是 | 必须包含 `S256` |
|
|
145
|
+
| `id_token_signing_alg_values_supported` | 应该 | 应包含 `RS256`;实际 ID Token 必须使用 RS256 |
|
|
146
|
+
|
|
147
|
+
Discovery 声明的 endpoint 可以位于不同的 HTTPS origin。开发环境 HTTP 只允许 Profile 明确声明的单一开发 origin,或 loopback origin。
|
|
148
|
+
|
|
149
|
+
### OIDC-02:Authorization
|
|
150
|
+
|
|
151
|
+
浏览器通过 Authorization Code + PKCE 发起登录。
|
|
152
|
+
|
|
153
|
+
#### 请求
|
|
154
|
+
|
|
155
|
+
```http
|
|
156
|
+
GET /oidc/authorize?
|
|
157
|
+
response_type=code&
|
|
158
|
+
client_id=dsh-web-public-client&
|
|
159
|
+
redirect_uri=http%3A%2F%2F127.0.0.1%3A3080%2Foauth%2Fcallback&
|
|
160
|
+
scope=openid%20profile%20offline_access&
|
|
161
|
+
state=<random-state>&
|
|
162
|
+
nonce=<random-nonce>&
|
|
163
|
+
code_challenge=<base64url-sha256>&
|
|
164
|
+
code_challenge_method=S256 HTTP/1.1
|
|
165
|
+
Host: id.example.edu
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### 查询参数
|
|
169
|
+
|
|
170
|
+
| 参数 | 要求 |
|
|
171
|
+
| --- | --- |
|
|
172
|
+
| `response_type` | 固定为 `code` |
|
|
173
|
+
| `client_id` | 等于 Profile 的 `oidc.clientId` |
|
|
174
|
+
| `redirect_uri` | 与注册的 loopback 回调精确一致 |
|
|
175
|
+
| `scope` | 至少包含 `openid profile` |
|
|
176
|
+
| `state` | 客户端生成,服务端必须原样返回 |
|
|
177
|
+
| `nonce` | 客户端生成,必须进入对应 ID Token |
|
|
178
|
+
| `code_challenge` | PKCE challenge |
|
|
179
|
+
| `code_challenge_method` | 固定为 `S256` |
|
|
180
|
+
|
|
181
|
+
#### 成功回调
|
|
182
|
+
|
|
183
|
+
```http
|
|
184
|
+
HTTP/1.1 302 Found
|
|
185
|
+
Location: http://127.0.0.1:3080/oauth/callback?code=<authorization-code>&state=<original-state>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
如果支持 RFC 9207,可以额外返回 `iss`;其值必须等于 Profile 中的 issuer。
|
|
189
|
+
|
|
190
|
+
#### 失败回调
|
|
191
|
+
|
|
192
|
+
```text
|
|
193
|
+
http://127.0.0.1:3080/oauth/callback?error=access_denied&error_description=...&state=<original-state>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
服务端不得省略 `state`。Authorization Code 必须一次性使用、短期有效,并绑定到 client、redirect URI 和 PKCE challenge。
|
|
197
|
+
|
|
198
|
+
### OIDC-03:Token
|
|
199
|
+
|
|
200
|
+
客户端使用 Authorization Code 和 PKCE verifier 换取 Token。Public Client 不发送 Client Secret。
|
|
201
|
+
|
|
202
|
+
#### Authorization Code 请求
|
|
203
|
+
|
|
204
|
+
```http
|
|
205
|
+
POST /oidc/token HTTP/1.1
|
|
206
|
+
Host: id.example.edu
|
|
207
|
+
Content-Type: application/x-www-form-urlencoded
|
|
208
|
+
Accept: application/json
|
|
209
|
+
|
|
210
|
+
grant_type=authorization_code&
|
|
211
|
+
client_id=dsh-web-public-client&
|
|
212
|
+
code=<authorization-code>&
|
|
213
|
+
redirect_uri=http%3A%2F%2F127.0.0.1%3A3080%2Foauth%2Fcallback&
|
|
214
|
+
code_verifier=<pkce-verifier>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
#### 成功响应
|
|
218
|
+
|
|
219
|
+
```http
|
|
220
|
+
HTTP/1.1 200 OK
|
|
221
|
+
Content-Type: application/json
|
|
222
|
+
Cache-Control: no-store
|
|
223
|
+
|
|
224
|
+
{
|
|
225
|
+
"access_token": "<oidc-access-token>",
|
|
226
|
+
"token_type": "Bearer",
|
|
227
|
+
"expires_in": 3600,
|
|
228
|
+
"id_token": "<signed-id-token>",
|
|
229
|
+
"refresh_token": "<optional-refresh-token>",
|
|
230
|
+
"scope": "openid profile offline_access"
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
| 字段 | 必需 | 要求 |
|
|
235
|
+
| --- | --- | --- |
|
|
236
|
+
| `access_token` | 是 | 非空;供 UserInfo 和 Key Binding 使用 |
|
|
237
|
+
| `token_type` | 是 | 必须为 `Bearer`,大小写不敏感 |
|
|
238
|
+
| `expires_in` | 应该 | 正数秒;缺失时客户端按 3600 秒处理 |
|
|
239
|
+
| `id_token` | 是 | RS256 签名 JWT |
|
|
240
|
+
| `refresh_token` | 否 | 启用 `offline_access` 时建议返回 |
|
|
241
|
+
| `scope` | 否 | 实际授权 scope |
|
|
242
|
+
|
|
243
|
+
ID Token 至少必须包含:
|
|
244
|
+
|
|
245
|
+
| Claim | 要求 |
|
|
246
|
+
| --- | --- |
|
|
247
|
+
| `iss` | 等于 Profile 的 `oidc.issuer` |
|
|
248
|
+
| `aud` | 包含 `oidc.clientId`;多 audience 时 `azp` 必须等于该 client ID |
|
|
249
|
+
| `sub` | 非空、稳定的用户标识 |
|
|
250
|
+
| `iat`、`exp` | 有效 NumericDate |
|
|
251
|
+
| `nonce` | 与 Authorization 请求完全一致 |
|
|
252
|
+
| `nbf` | 可选;提供时必须有效 |
|
|
253
|
+
| `at_hash` | 可选;提供时必须与 Access Token 匹配 |
|
|
254
|
+
|
|
255
|
+
JWT header 必须使用 `alg=RS256`,并提供非空 `kid`。
|
|
256
|
+
|
|
257
|
+
#### Refresh Token 请求
|
|
258
|
+
|
|
259
|
+
```http
|
|
260
|
+
POST /oidc/token HTTP/1.1
|
|
261
|
+
Content-Type: application/x-www-form-urlencoded
|
|
262
|
+
Accept: application/json
|
|
263
|
+
|
|
264
|
+
grant_type=refresh_token&
|
|
265
|
+
client_id=dsh-web-public-client&
|
|
266
|
+
refresh_token=<refresh-token>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
响应必须至少包含新的 `access_token`、`token_type=Bearer` 和有效 `expires_in`。如果轮换 Refresh Token,应同时返回新的 `refresh_token`。`invalid_grant` 会导致客户端清除本地 OIDC 会话和模型运行凭据。
|
|
270
|
+
|
|
271
|
+
#### 错误响应
|
|
272
|
+
|
|
273
|
+
Token 错误遵循 OAuth 2.0:
|
|
274
|
+
|
|
275
|
+
```http
|
|
276
|
+
HTTP/1.1 400 Bad Request
|
|
277
|
+
Content-Type: application/json
|
|
278
|
+
Cache-Control: no-store
|
|
279
|
+
|
|
280
|
+
{
|
|
281
|
+
"error": "invalid_grant",
|
|
282
|
+
"error_description": "Authorization code is invalid or expired"
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### OIDC-04:JWKS
|
|
287
|
+
|
|
288
|
+
提供验证 ID Token 所需的公开签名密钥。
|
|
289
|
+
|
|
290
|
+
#### 请求
|
|
291
|
+
|
|
292
|
+
```http
|
|
293
|
+
GET /oidc/jwks HTTP/1.1
|
|
294
|
+
Accept: application/json
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
#### 成功响应
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
{
|
|
301
|
+
"keys": [
|
|
302
|
+
{
|
|
303
|
+
"kty": "RSA",
|
|
304
|
+
"kid": "signing-key-2026-01",
|
|
305
|
+
"use": "sig",
|
|
306
|
+
"alg": "RS256",
|
|
307
|
+
"n": "<base64url-modulus>",
|
|
308
|
+
"e": "AQAB"
|
|
309
|
+
}
|
|
310
|
+
]
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
用于签发 ID Token 的 `kid` 必须唯一匹配一个 RSA 签名密钥。密钥轮换时应该保留旧公钥,直到所有使用旧密钥签发的有效 Token 均已过期。
|
|
315
|
+
|
|
316
|
+
### OIDC-05:UserInfo
|
|
317
|
+
|
|
318
|
+
返回当前 Access Token 对应的标准用户身份。
|
|
319
|
+
|
|
320
|
+
#### 请求
|
|
321
|
+
|
|
322
|
+
```http
|
|
323
|
+
GET /oidc/userinfo HTTP/1.1
|
|
324
|
+
Authorization: Bearer <oidc-access-token>
|
|
325
|
+
Accept: application/json
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
#### 成功响应
|
|
329
|
+
|
|
330
|
+
```http
|
|
331
|
+
HTTP/1.1 200 OK
|
|
332
|
+
Content-Type: application/json
|
|
333
|
+
|
|
334
|
+
{
|
|
335
|
+
"sub": "user-12345",
|
|
336
|
+
"name": "示例用户"
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
| 字段 | 必需 | 用途 |
|
|
341
|
+
| --- | --- | --- |
|
|
342
|
+
| `sub` | 是 | 必须与 ID Token `sub` 完全一致 |
|
|
343
|
+
| `name` | 应该 | 界面显示姓名;缺失或为空时客户端显示 `sub` |
|
|
344
|
+
|
|
345
|
+
服务端应该直接提供标准 `name`。`dsh-oidc` 不调用机构私有人员接口,也不支持通过 JSON Path 映射姓名。其他标准 Claim 可以返回,但不得代替 `sub` 参与绑定。
|
|
346
|
+
|
|
347
|
+
### OIDC-06:Revocation(建议)
|
|
348
|
+
|
|
349
|
+
如果 Discovery 发布 `revocation_endpoint`,客户端退出时会提交 Refresh Token;没有 Refresh Token 时提交 Access Token。
|
|
350
|
+
|
|
351
|
+
```http
|
|
352
|
+
POST /oidc/revoke HTTP/1.1
|
|
353
|
+
Content-Type: application/x-www-form-urlencoded
|
|
354
|
+
|
|
355
|
+
token=<refresh-or-access-token>&client_id=dsh-web-public-client
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
退出操作对撤销采用尽力而为语义。无论远端撤销是否成功,本地 OIDC 会话和模型运行凭据都会清除。模型运行 API Key 的远程撤销仍由 Key Binding 与模型平台的生命周期策略负责。
|
|
359
|
+
|
|
360
|
+
## 5. Key Binding 接口组
|
|
361
|
+
|
|
362
|
+
### 5.1 通用约定
|
|
363
|
+
|
|
364
|
+
四个接口的相对路径、HTTP 方法和字段名称固定,不能在 Profile 中配置。
|
|
365
|
+
|
|
366
|
+
所有请求必须携带:
|
|
367
|
+
|
|
368
|
+
```http
|
|
369
|
+
Authorization: Bearer <oidc-access-token>
|
|
370
|
+
Accept: application/json
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
服务端必须验证 Token 的 issuer、audience、有效期、not-before、scope、subject、账号状态和机构授权。subject 必须从验证后的 Token 派生;不得接受客户端在 body 中提交 subject、工号或姓名作为可信身份。
|
|
374
|
+
|
|
375
|
+
建议 scope:
|
|
376
|
+
|
|
377
|
+
| 接口 | Scope |
|
|
378
|
+
| --- | --- |
|
|
379
|
+
| KEY-01 Bootstrap | `worker.bootstrap.read` |
|
|
380
|
+
| KEY-02 Provision | `worker.credential.provision` |
|
|
381
|
+
| KEY-03 Resolve | `worker.credential.read` |
|
|
382
|
+
| KEY-04 Renew | `worker.credential.renew` |
|
|
383
|
+
|
|
384
|
+
机构可以使用其他 scope 名称,但必须将实际名称加入 Profile 的 `oidc.scopes`,并在服务端执行对应授权。
|
|
385
|
+
|
|
386
|
+
### KEY-01:Bootstrap
|
|
387
|
+
|
|
388
|
+
读取 Provider、服务端能力、用户授权状态和运行凭据状态。该接口不得返回模型 API Key。
|
|
389
|
+
|
|
390
|
+
#### 请求
|
|
391
|
+
|
|
392
|
+
```http
|
|
393
|
+
GET /api/worker/v1/bootstrap HTTP/1.1
|
|
394
|
+
Host: ai.example.edu
|
|
395
|
+
Authorization: Bearer <oidc-access-token>
|
|
396
|
+
Accept: application/json
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
#### 成功响应
|
|
400
|
+
|
|
401
|
+
```http
|
|
402
|
+
HTTP/1.1 200 OK
|
|
403
|
+
Content-Type: application/json
|
|
404
|
+
Cache-Control: no-store
|
|
405
|
+
|
|
406
|
+
{
|
|
407
|
+
"protocol_version": "worker-user-center/v1",
|
|
408
|
+
"provider": {
|
|
409
|
+
"id": "example-ai"
|
|
410
|
+
},
|
|
411
|
+
"capabilities": [
|
|
412
|
+
"worker.bootstrap.read",
|
|
413
|
+
"worker.credential.provision",
|
|
414
|
+
"worker.credential.read",
|
|
415
|
+
"worker.credential.renew"
|
|
416
|
+
],
|
|
417
|
+
"runtime_credential": {
|
|
418
|
+
"status": "missing",
|
|
419
|
+
"provisioning": {
|
|
420
|
+
"allowed": true
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
#### 响应字段
|
|
427
|
+
|
|
428
|
+
| 字段 | 必需 | 说明 |
|
|
429
|
+
| --- | --- | --- |
|
|
430
|
+
| `protocol_version` | 否 | 提供时必须为 `worker-user-center/v1` |
|
|
431
|
+
| `provider.id` | 是 | 必须与 Profile `provider.id` 完全一致 |
|
|
432
|
+
| `capabilities` | 是 | 字符串数组,最多 256 项 |
|
|
433
|
+
| `runtime_credential.status` | 是 | 当前凭据状态 |
|
|
434
|
+
| `runtime_credential.api_key_id` | 条件 | 已存在凭据时建议返回;Renew 需要该值 |
|
|
435
|
+
| `runtime_credential.expires_at` | 否 | RFC 3339 时间 |
|
|
436
|
+
| `runtime_credential.provisioning.allowed` | 否 | 是否允许创建凭据 |
|
|
437
|
+
| `runtime_credential.provisioning.reason` | 否 | 面向用户的非敏感原因,最多 500 字符 |
|
|
438
|
+
| `subject` | 否 | 仅供管理面展示;不得覆盖 OIDC 身份 |
|
|
439
|
+
|
|
440
|
+
`runtime_credential.status` 的取值:
|
|
441
|
+
|
|
442
|
+
| 状态 | 客户端行为 |
|
|
443
|
+
| --- | --- |
|
|
444
|
+
| `missing` | 等待用户明确同意后调用 Provision |
|
|
445
|
+
| `active` | 调用 Resolve |
|
|
446
|
+
| `expiring` | 有 `api_key_id` 时调用 Renew,否则调用 Resolve |
|
|
447
|
+
| `expired` | 有 `api_key_id` 时调用 Renew,否则调用 Resolve |
|
|
448
|
+
| `suspended` | 清除本地运行凭据,不继续调用模型 |
|
|
449
|
+
| `unavailable` | 清除本地运行凭据,显示服务不可用 |
|
|
450
|
+
|
|
451
|
+
### KEY-02:Provision
|
|
452
|
+
|
|
453
|
+
为已认证且已获授权的 subject 创建或绑定运行凭据。仅完成 OIDC 回调不会触发该接口;用户必须在界面中明确同意。
|
|
454
|
+
|
|
455
|
+
#### 请求
|
|
456
|
+
|
|
457
|
+
```http
|
|
458
|
+
POST /api/worker/v1/runtime-credential/provision HTTP/1.1
|
|
459
|
+
Authorization: Bearer <oidc-access-token>
|
|
460
|
+
Content-Type: application/json
|
|
461
|
+
Accept: application/json
|
|
462
|
+
Idempotency-Key: 2b9b6c80-7758-4d3f-94c6-b72d91ca8c53
|
|
463
|
+
|
|
464
|
+
{
|
|
465
|
+
"provider_id": "example-ai"
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
| 字段/Header | 必需 | 要求 |
|
|
470
|
+
| --- | --- | --- |
|
|
471
|
+
| `Idempotency-Key` | 是 | 一个逻辑创建操作对应一个 UUID |
|
|
472
|
+
| `provider_id` | 是 | 必须与 Profile `provider.id` 一致 |
|
|
473
|
+
|
|
474
|
+
服务端必须以“已认证 subject + Provider + Idempotency-Key”为幂等范围。相同 key 和相同 payload 的重试必须返回相同逻辑结果;相同 key 携带不同 payload 必须返回 `409 Conflict`。
|
|
475
|
+
|
|
476
|
+
#### 成功响应
|
|
477
|
+
|
|
478
|
+
返回格式见[运行凭据统一响应](#55-运行凭据统一响应)。首次创建可以返回 `201 Created`,已经存在或完成绑定时可以返回 `200 OK`。
|
|
479
|
+
|
|
480
|
+
### KEY-03:Resolve
|
|
481
|
+
|
|
482
|
+
解析已经绑定到当前 subject 的活动运行凭据。Resolve 不得创建新的授权资格。
|
|
483
|
+
|
|
484
|
+
#### 请求
|
|
485
|
+
|
|
486
|
+
```http
|
|
487
|
+
POST /api/worker/v1/runtime-credential/resolve HTTP/1.1
|
|
488
|
+
Authorization: Bearer <oidc-access-token>
|
|
489
|
+
Content-Type: application/json
|
|
490
|
+
Accept: application/json
|
|
491
|
+
|
|
492
|
+
{
|
|
493
|
+
"provider_id": "example-ai",
|
|
494
|
+
"api_key_id": "key-123"
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
| 字段 | 必需 | 说明 |
|
|
499
|
+
| --- | --- | --- |
|
|
500
|
+
| `provider_id` | 是 | Profile 中声明的 Provider ID |
|
|
501
|
+
| `api_key_id` | 否 | Bootstrap 返回的已知凭据 ID |
|
|
502
|
+
|
|
503
|
+
不存在可解析凭据时返回 `404`。服务端可以轮换不透明秘密值,但不得把其他 subject 或 Provider 的凭据返回给调用方。
|
|
504
|
+
|
|
505
|
+
### KEY-04:Renew
|
|
506
|
+
|
|
507
|
+
轮换即将过期或已经过期的运行凭据。
|
|
508
|
+
|
|
509
|
+
#### 请求
|
|
510
|
+
|
|
511
|
+
```http
|
|
512
|
+
POST /api/worker/v1/runtime-credential/renew HTTP/1.1
|
|
513
|
+
Authorization: Bearer <oidc-access-token>
|
|
514
|
+
Content-Type: application/json
|
|
515
|
+
Accept: application/json
|
|
516
|
+
Idempotency-Key: 2f94dc48-201a-475e-a61a-69d7880e1186
|
|
517
|
+
|
|
518
|
+
{
|
|
519
|
+
"provider_id": "example-ai",
|
|
520
|
+
"api_key_id": "key-123"
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
`provider_id`、`api_key_id` 和 `Idempotency-Key` 均为必需。幂等规则与 Provision 相同。新凭据生效后,服务端应该尽快使旧秘密失效,并记录任何必要的重叠有效窗口。
|
|
525
|
+
|
|
526
|
+
### 5.5 运行凭据统一响应
|
|
527
|
+
|
|
528
|
+
Provision、Resolve 和 Renew 成功时使用相同响应结构:
|
|
529
|
+
|
|
530
|
+
```http
|
|
531
|
+
HTTP/1.1 200 OK
|
|
532
|
+
Content-Type: application/json
|
|
533
|
+
Cache-Control: no-store
|
|
534
|
+
Pragma: no-cache
|
|
535
|
+
|
|
536
|
+
{
|
|
537
|
+
"provider_id": "example-ai",
|
|
538
|
+
"api_key": "<secret-runtime-api-key>",
|
|
539
|
+
"api_key_id": "key-123",
|
|
540
|
+
"status": "active",
|
|
541
|
+
"expires_at": "2026-09-01T00:00:00Z"
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
| 字段 | 必需 | 要求 |
|
|
546
|
+
| --- | --- | --- |
|
|
547
|
+
| `provider_id` | 是 | 必须与请求及 Profile 完全一致 |
|
|
548
|
+
| `api_key` | 是 | 非空,最多 16 KiB;只用于模型网关 |
|
|
549
|
+
| `api_key_id` | 否 | 建议返回,用于生命周期管理和审计 |
|
|
550
|
+
| `status` | 是 | 固定为 `active` |
|
|
551
|
+
| `expires_at` | 否 | RFC 3339 时间 |
|
|
552
|
+
|
|
553
|
+
包含 `api_key` 的响应必须使用 `Cache-Control: no-store`,并且应该使用 `Pragma: no-cache`。反向代理、WAF、APM、应用日志和审计日志都不得记录 Authorization header 或 `api_key`。
|
|
554
|
+
|
|
555
|
+
### 5.6 Key Binding 错误响应
|
|
556
|
+
|
|
557
|
+
错误应该使用 RFC 9457 `application/problem+json`:
|
|
558
|
+
|
|
559
|
+
```http
|
|
560
|
+
HTTP/1.1 403 Forbidden
|
|
561
|
+
Content-Type: application/problem+json
|
|
562
|
+
|
|
563
|
+
{
|
|
564
|
+
"type": "https://ai.example.edu/problems/model-entitlement-denied",
|
|
565
|
+
"title": "Model entitlement denied",
|
|
566
|
+
"status": 403,
|
|
567
|
+
"detail": "The current account cannot use this provider",
|
|
568
|
+
"code": "model_entitlement_denied"
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
| HTTP 状态 | 含义 |
|
|
573
|
+
| --- | --- |
|
|
574
|
+
| `400` | 请求结构或 Provider ID 无效 |
|
|
575
|
+
| `401` | Access Token 缺失、无效、过期或 audience 错误;应该返回 `WWW-Authenticate` |
|
|
576
|
+
| `403` | scope、机构成员资格、模型授权、账号状态或创建策略拒绝 |
|
|
577
|
+
| `404` | 不存在可解析或可更新的凭据 |
|
|
578
|
+
| `409` | 幂等冲突或凭据生命周期状态冲突 |
|
|
579
|
+
| `429` | 限流;应该返回 `Retry-After` |
|
|
580
|
+
| `5xx` | 服务端错误;响应不得包含 Token 或 API Key |
|
|
581
|
+
|
|
582
|
+
`code` 应保持稳定,供界面和日志分类;`detail` 可以变化,但不得泄露敏感信息。
|
|
583
|
+
|
|
584
|
+
机器可读的 Key Binding 契约见 [`protocol/openapi.yaml`](../protocol/openapi.yaml)。
|
|
585
|
+
|
|
586
|
+
## 6. 模型网关接口组
|
|
587
|
+
|
|
588
|
+
### MODEL-01:Chat Completions
|
|
589
|
+
|
|
590
|
+
`dsh-oidc` 当前通过 DSH 官方 Pi adapter 调用 OpenAI Chat Completions 兼容接口。
|
|
591
|
+
|
|
592
|
+
#### 请求
|
|
593
|
+
|
|
594
|
+
```http
|
|
595
|
+
POST /open/api/v1/chat/completions HTTP/1.1
|
|
596
|
+
Host: ai.example.edu
|
|
597
|
+
Authorization: Bearer <runtime-api-key>
|
|
598
|
+
Content-Type: application/json
|
|
599
|
+
Accept: text/event-stream
|
|
600
|
+
|
|
601
|
+
{
|
|
602
|
+
"model": "example-model",
|
|
603
|
+
"messages": [
|
|
604
|
+
{ "role": "user", "content": "你好" }
|
|
605
|
+
],
|
|
606
|
+
"stream": true,
|
|
607
|
+
"max_tokens": 1024
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
| 项目 | 要求 |
|
|
612
|
+
| --- | --- |
|
|
613
|
+
| 认证 | 接受 Key Binding 返回的 `api_key`,不得接受 OIDC Token 代替 |
|
|
614
|
+
| `model` | 必须实现 Profile `provider.models` 中声明的模型 ID |
|
|
615
|
+
| `messages` | 支持 OpenAI Chat Completions 消息结构 |
|
|
616
|
+
| `stream` | 必须支持流式响应 |
|
|
617
|
+
| 图片输入 | 只有 Profile 为该模型声明 `input: ["text", "image"]` 时才要求支持 |
|
|
618
|
+
| Reasoning 参数 | 必须与 Profile 的 `reasoning`、`reasoningEfforts` 和 `compat` 声明一致 |
|
|
619
|
+
| Tools | 必须支持 `tools`、`tool_choice` 和流式 `tool_calls`,供 DSH Agent 调用工具 |
|
|
620
|
+
|
|
621
|
+
#### 流式成功响应
|
|
622
|
+
|
|
623
|
+
```http
|
|
624
|
+
HTTP/1.1 200 OK
|
|
625
|
+
Content-Type: text/event-stream
|
|
626
|
+
Cache-Control: no-cache
|
|
627
|
+
|
|
628
|
+
data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"你"}}]}
|
|
629
|
+
|
|
630
|
+
data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"好"},"finish_reason":null}]}
|
|
631
|
+
|
|
632
|
+
data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
|
|
633
|
+
|
|
634
|
+
data: [DONE]
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
服务端必须按运行 API Key 检查 subject 绑定、Provider scope、凭据状态、过期时间、撤销状态和配额。已经撤销或轮换的旧 Key 不得无限期继续访问。
|
|
638
|
+
|
|
639
|
+
#### 错误响应
|
|
640
|
+
|
|
641
|
+
错误应与 OpenAI-compatible 客户端互操作,例如:
|
|
642
|
+
|
|
643
|
+
```json
|
|
644
|
+
{
|
|
645
|
+
"error": {
|
|
646
|
+
"message": "Runtime credential has expired",
|
|
647
|
+
"type": "authentication_error",
|
|
648
|
+
"code": "credential_expired"
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
至少应正确区分:`400` 请求错误、`401` 凭据无效、`403` 无权访问、`404` 模型不存在、`429` 限流或配额不足、`5xx` 服务端错误。
|
|
654
|
+
|
|
655
|
+
模型目录由受信任的 Enterprise Profile 声明;模型网关不需要实现 `/models` 才能符合本规范。
|
|
656
|
+
|
|
657
|
+
## 7. 三组接口的一致性要求
|
|
658
|
+
|
|
659
|
+
服务端必须保证:
|
|
660
|
+
|
|
661
|
+
1. UserInfo `sub` 与 ID Token `sub` 完全一致;
|
|
662
|
+
2. Key Binding 从 Access Token 得到同一个 subject,不信任请求 body 中的身份;
|
|
663
|
+
3. Profile `provider.id`、Bootstrap `provider.id` 和运行凭据 `provider_id` 完全一致;
|
|
664
|
+
4. 运行 API Key 只授权给对应 subject 和 Provider;
|
|
665
|
+
5. 模型网关实际提供的模型 ID、输入模态和 reasoning 能力与 Profile 声明一致;
|
|
666
|
+
6. OIDC Token 只用于 UserInfo 和 Key Binding,运行 API Key 只用于模型网关,两者不能互换;
|
|
667
|
+
7. 账号停用、授权撤销和凭据轮换能够传播到运行凭据及模型网关;
|
|
668
|
+
8. 日志可以记录关联 ID、不透明 subject 引用、Provider ID 和 `api_key_id`,但不得记录 Token 或 `api_key`;
|
|
669
|
+
9. Bootstrap 中的 `subject` 只能用于辅助展示,不能覆盖 OIDC `sub` 或 `name`。
|
|
670
|
+
|
|
671
|
+
## 8. 标准调用顺序
|
|
672
|
+
|
|
673
|
+
```text
|
|
674
|
+
客户端 -> OIDC-01 Discovery
|
|
675
|
+
客户端 -> OIDC-02 Authorization(浏览器 + PKCE)
|
|
676
|
+
OIDC -> 127.0.0.1:<port>/oauth/callback
|
|
677
|
+
客户端 -> OIDC-03 Token
|
|
678
|
+
客户端 -> OIDC-04 JWKS(校验 ID Token)
|
|
679
|
+
客户端 -> OIDC-05 UserInfo
|
|
680
|
+
客户端 -> KEY-01 Bootstrap
|
|
681
|
+
客户端 -> KEY-02 Provision(首次且用户明确同意)
|
|
682
|
+
或 KEY-03 Resolve(凭据活动)
|
|
683
|
+
或 KEY-04 Renew(凭据即将/已经过期)
|
|
684
|
+
客户端 -> MODEL-01 Chat Completions
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
客户端在 Access Token 即将过期时使用 OIDC-03 的 Refresh Token grant。退出时清除本地 OIDC 会话和运行凭据,并在可用时调用 OIDC-06。
|
|
688
|
+
|
|
689
|
+
## 9. 联调验收清单
|
|
690
|
+
|
|
691
|
+
完整接入至少必须通过以下测试:
|
|
692
|
+
|
|
693
|
+
- Discovery issuer 精确匹配、必需 endpoint 完整、PKCE `S256` 已发布;
|
|
694
|
+
- callback 只使用 `127.0.0.1:<实际端口>/oauth/callback`;
|
|
695
|
+
- state、nonce、PKCE verifier、ID Token 签名、audience 和有效期均被校验;
|
|
696
|
+
- UserInfo 返回非空 `sub`,并与 ID Token 一致;`name` 可以正常显示;
|
|
697
|
+
- 四个 Key Binding 接口全部存在,并按状态正确选择 Provision、Resolve 或 Renew;
|
|
698
|
+
- Provision/Renew 幂等,错误重放返回 `409`;
|
|
699
|
+
- Provider ID 在 Profile、Bootstrap 和凭据响应中完全一致;
|
|
700
|
+
- Key Binding 的 Access Token 不能调用模型网关,模型 API Key 不能调用 UserInfo 或 Key Binding;
|
|
701
|
+
- 运行凭据过期、撤销、账号停用和越权访问均被拒绝;
|
|
702
|
+
- Chat Completions 可以完成至少一次流式文本对话;声明多模态或 reasoning 的模型通过对应测试;
|
|
703
|
+
- Token、Authorization header 和 `api_key` 不出现在 URL、日志、APM、错误详情或审计数据中;
|
|
704
|
+
- 所有失败响应均有稳定、可诊断且不泄密的错误结构。
|
|
705
|
+
|
|
706
|
+
Enterprise Profile 的完整字段见 [Enterprise Profile 规范](enterprise-profile.md),OIDC 细节见 [OIDC 互操作规范](oidc-interoperability.md),Key Binding 的生命周期与安全语义见 [Key Binding 协议](key-binding-protocol.md)。
|