@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 server API specification
|
|
2
|
+
|
|
3
|
+
[简体中文](server-integration-contract.md) | **English**
|
|
4
|
+
|
|
5
|
+
This document is written for identity-platform, model-platform, and integration-test teams. It defines, in API-reference form, the server capabilities required by `dsh-oidc`.
|
|
6
|
+
|
|
7
|
+
“MUST”, “MUST NOT”, “SHOULD”, and “MAY” are normative. The OIDC interfaces follow OpenID Connect and OAuth 2.0; Key Binding is the fixed protocol defined by this project; the model gateway uses an OpenAI Chat Completions-compatible interface.
|
|
8
|
+
|
|
9
|
+
## 1. Integration scope
|
|
10
|
+
|
|
11
|
+
A complete **institutional enterprise-model integration service** MUST provide all three interface groups:
|
|
12
|
+
|
|
13
|
+
| Interface group | Purpose | Enterprise Profile configuration | Requirement |
|
|
14
|
+
| --- | --- | --- | --- |
|
|
15
|
+
| OIDC Provider | Login, user identity, and OIDC Access Tokens | `oidc.issuer`, `oidc.clientId`, `oidc.scopes` | Complete implementation required |
|
|
16
|
+
| Key Binding | Bind an authenticated user to a revocable model runtime credential | `keyBinding.baseURL` | All four operations required |
|
|
17
|
+
| Model gateway | Invoke enterprise models with the bound runtime credential | `provider.baseURL`, `provider.id`, `provider.models` | Chat Completions required |
|
|
18
|
+
|
|
19
|
+
“One service” means one complete product-delivery, versioning, security, and operational boundary. It does not require every endpoint to share a hostname, gateway, or process. An institution may internally separate its identity platform, user center, and model platform, but it cannot deliver only one part and claim a complete `dsh-oidc` integration.
|
|
20
|
+
|
|
21
|
+
The following arrangements do not conform:
|
|
22
|
+
|
|
23
|
+
- OIDC alone, followed by distribution of one global model API key to all users;
|
|
24
|
+
- Key Binding that trusts an employee number, name, or subject supplied by the client;
|
|
25
|
+
- a model URL and catalog without user authorization and credential lifecycle management;
|
|
26
|
+
- a Key Binding service missing any of `bootstrap`, `provision`, `resolve`, or `renew`.
|
|
27
|
+
|
|
28
|
+
## 2. Addresses and client registration
|
|
29
|
+
|
|
30
|
+
### 2.1 Three service entry points
|
|
31
|
+
|
|
32
|
+
An Enterprise Profile declares the three entry points separately:
|
|
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
|
+
In production, `issuer`, `keyBinding.baseURL`, and `provider.baseURL` MUST use HTTPS. URLs MUST NOT contain credentials or fragments, and base URLs MUST NOT depend on query parameters to alter routing behavior.
|
|
56
|
+
|
|
57
|
+
### 2.2 OIDC Public Client
|
|
58
|
+
|
|
59
|
+
The identity platform MUST register a Public Client without a Client Secret and enable:
|
|
60
|
+
|
|
61
|
+
- Authorization Code Flow;
|
|
62
|
+
- PKCE `S256`;
|
|
63
|
+
- the `openid` and `profile` scopes;
|
|
64
|
+
- `offline_access` and the corresponding authorization policy when Refresh Tokens are required.
|
|
65
|
+
|
|
66
|
+
The redirect URI MUST be registered exactly as:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
http://127.0.0.1:3080/oauth/callback
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Constraints:
|
|
73
|
+
|
|
74
|
+
- the host is fixed to `127.0.0.1`, not `localhost`, a LAN address, or a public hostname;
|
|
75
|
+
- the path is fixed to `/oauth/callback`;
|
|
76
|
+
- `3080` is the recommended port; if DSH WebServer uses another port, the registered URI MUST use that actual port;
|
|
77
|
+
- this is the browser's return address to the user's local DSH process, not an institutional server address;
|
|
78
|
+
- the current implementation is a local single-user Web composition and does not support a shared public-Web callback.
|
|
79
|
+
|
|
80
|
+
## 3. API summary
|
|
81
|
+
|
|
82
|
+
| ID | Method | Address | Authentication | Required |
|
|
83
|
+
| --- | --- | --- | --- | --- |
|
|
84
|
+
| OIDC-01 | `GET` | `{issuer}/.well-known/openid-configuration` | None | Yes |
|
|
85
|
+
| OIDC-02 | `GET` | Discovery `authorization_endpoint` | User login session | Yes |
|
|
86
|
+
| OIDC-03 | `POST` | Discovery `token_endpoint` | Authorization Code + PKCE | Yes |
|
|
87
|
+
| OIDC-04 | `GET` | Discovery `jwks_uri` | None | Yes |
|
|
88
|
+
| OIDC-05 | `GET` | Discovery `userinfo_endpoint` | OIDC Bearer Access Token | Yes |
|
|
89
|
+
| OIDC-06 | `POST` | Discovery `revocation_endpoint` | OIDC Token | Recommended |
|
|
90
|
+
| KEY-01 | `GET` | `{keyBinding.baseURL}/bootstrap` | OIDC Bearer Access Token | Yes |
|
|
91
|
+
| KEY-02 | `POST` | `{keyBinding.baseURL}/runtime-credential/provision` | OIDC Bearer Access Token | Yes |
|
|
92
|
+
| KEY-03 | `POST` | `{keyBinding.baseURL}/runtime-credential/resolve` | OIDC Bearer Access Token | Yes |
|
|
93
|
+
| KEY-04 | `POST` | `{keyBinding.baseURL}/runtime-credential/renew` | OIDC Bearer Access Token | Yes |
|
|
94
|
+
| MODEL-01 | `POST` | `{provider.baseURL}/chat/completions` | Runtime API Key | Yes |
|
|
95
|
+
|
|
96
|
+
## 4. OIDC interface group
|
|
97
|
+
|
|
98
|
+
### OIDC-01: Discovery
|
|
99
|
+
|
|
100
|
+
Reads OIDC Provider metadata.
|
|
101
|
+
|
|
102
|
+
#### Request
|
|
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
|
+
When the issuer itself has a path, the Discovery address is:
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
{issuer-without-trailing-slash}/.well-known/openid-configuration
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### Success response
|
|
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
|
+
#### Field requirements
|
|
135
|
+
|
|
136
|
+
| Field | Required | Requirement |
|
|
137
|
+
| --- | --- | --- |
|
|
138
|
+
| `issuer` | Yes | Exact string match with Enterprise Profile `oidc.issuer` |
|
|
139
|
+
| `authorization_endpoint` | Yes | Absolute HTTPS URL |
|
|
140
|
+
| `token_endpoint` | Yes | Absolute HTTPS URL |
|
|
141
|
+
| `jwks_uri` | Yes | Absolute HTTPS URL |
|
|
142
|
+
| `userinfo_endpoint` | Yes | Required by this interoperability profile |
|
|
143
|
+
| `revocation_endpoint` | No | When present, the client attempts revocation during logout |
|
|
144
|
+
| `code_challenge_methods_supported` | Yes | MUST contain `S256` |
|
|
145
|
+
| `id_token_signing_alg_values_supported` | Should | SHOULD contain `RS256`; the actual ID Token MUST use RS256 |
|
|
146
|
+
|
|
147
|
+
Discovered endpoints may use different HTTPS origins. Development HTTP is accepted only for the single explicitly configured development origin or a loopback origin.
|
|
148
|
+
|
|
149
|
+
### OIDC-02: Authorization
|
|
150
|
+
|
|
151
|
+
The browser starts Authorization Code Flow with PKCE.
|
|
152
|
+
|
|
153
|
+
#### Request
|
|
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
|
+
#### Query parameters
|
|
169
|
+
|
|
170
|
+
| Parameter | Requirement |
|
|
171
|
+
| --- | --- |
|
|
172
|
+
| `response_type` | Fixed to `code` |
|
|
173
|
+
| `client_id` | Equal to Profile `oidc.clientId` |
|
|
174
|
+
| `redirect_uri` | Exact match with the registered loopback callback |
|
|
175
|
+
| `scope` | At least `openid profile` |
|
|
176
|
+
| `state` | Generated by the client and returned unchanged |
|
|
177
|
+
| `nonce` | Generated by the client and included in the corresponding ID Token |
|
|
178
|
+
| `code_challenge` | PKCE challenge |
|
|
179
|
+
| `code_challenge_method` | Fixed to `S256` |
|
|
180
|
+
|
|
181
|
+
#### Success redirect
|
|
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
|
+
An RFC 9207 `iss` response parameter MAY also be returned. When present, it MUST equal the Profile issuer.
|
|
189
|
+
|
|
190
|
+
#### Error redirect
|
|
191
|
+
|
|
192
|
+
```text
|
|
193
|
+
http://127.0.0.1:3080/oauth/callback?error=access_denied&error_description=...&state=<original-state>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The server MUST NOT omit `state`. An Authorization Code MUST be single-use, short-lived, and bound to the client, redirect URI, and PKCE challenge.
|
|
197
|
+
|
|
198
|
+
### OIDC-03: Token
|
|
199
|
+
|
|
200
|
+
The client exchanges an Authorization Code and PKCE verifier for tokens. The Public Client does not send a Client Secret.
|
|
201
|
+
|
|
202
|
+
#### Authorization Code request
|
|
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
|
+
#### Success response
|
|
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
|
+
| Field | Required | Requirement |
|
|
235
|
+
| --- | --- | --- |
|
|
236
|
+
| `access_token` | Yes | Non-empty; used for UserInfo and Key Binding |
|
|
237
|
+
| `token_type` | Yes | `Bearer`, case-insensitive |
|
|
238
|
+
| `expires_in` | Should | Positive seconds; the client uses 3600 seconds when absent |
|
|
239
|
+
| `id_token` | Yes | RS256-signed JWT |
|
|
240
|
+
| `refresh_token` | No | Recommended when `offline_access` is enabled |
|
|
241
|
+
| `scope` | No | Actually granted scopes |
|
|
242
|
+
|
|
243
|
+
The ID Token MUST contain at least:
|
|
244
|
+
|
|
245
|
+
| Claim | Requirement |
|
|
246
|
+
| --- | --- |
|
|
247
|
+
| `iss` | Equal to Profile `oidc.issuer` |
|
|
248
|
+
| `aud` | Contains `oidc.clientId`; with multiple audiences, `azp` MUST equal that client ID |
|
|
249
|
+
| `sub` | Non-empty, stable user identifier |
|
|
250
|
+
| `iat`, `exp` | Valid NumericDate values |
|
|
251
|
+
| `nonce` | Exact match with the Authorization request |
|
|
252
|
+
| `nbf` | Optional; valid when present |
|
|
253
|
+
| `at_hash` | Optional; MUST match the Access Token when present |
|
|
254
|
+
|
|
255
|
+
The JWT header MUST use `alg=RS256` and contain a non-empty `kid`.
|
|
256
|
+
|
|
257
|
+
#### Refresh Token request
|
|
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
|
+
The response MUST contain at least a new `access_token`, `token_type=Bearer`, and valid `expires_in`. When Refresh Token rotation is used, it SHOULD also return a new `refresh_token`. An `invalid_grant` response causes the client to clear its local OIDC session and model runtime credential.
|
|
270
|
+
|
|
271
|
+
#### Error response
|
|
272
|
+
|
|
273
|
+
Token errors follow 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
|
+
Publishes the public signing keys needed to verify ID Tokens.
|
|
289
|
+
|
|
290
|
+
#### Request
|
|
291
|
+
|
|
292
|
+
```http
|
|
293
|
+
GET /oidc/jwks HTTP/1.1
|
|
294
|
+
Accept: application/json
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
#### Success response
|
|
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
|
+
The `kid` used by an ID Token MUST identify exactly one RSA signing key. During rotation, the old public key SHOULD remain available until every valid Token signed by it has expired.
|
|
315
|
+
|
|
316
|
+
### OIDC-05: UserInfo
|
|
317
|
+
|
|
318
|
+
Returns the standard user identity represented by the current Access Token.
|
|
319
|
+
|
|
320
|
+
#### Request
|
|
321
|
+
|
|
322
|
+
```http
|
|
323
|
+
GET /oidc/userinfo HTTP/1.1
|
|
324
|
+
Authorization: Bearer <oidc-access-token>
|
|
325
|
+
Accept: application/json
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
#### Success response
|
|
329
|
+
|
|
330
|
+
```http
|
|
331
|
+
HTTP/1.1 200 OK
|
|
332
|
+
Content-Type: application/json
|
|
333
|
+
|
|
334
|
+
{
|
|
335
|
+
"sub": "user-12345",
|
|
336
|
+
"name": "Example User"
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
| Field | Required | Purpose |
|
|
341
|
+
| --- | --- | --- |
|
|
342
|
+
| `sub` | Yes | Exact match with ID Token `sub` |
|
|
343
|
+
| `name` | Should | Human-readable UI label; the client falls back to `sub` when absent or empty |
|
|
344
|
+
|
|
345
|
+
The server SHOULD provide the standard `name` claim directly. `dsh-oidc` does not call private institutional directory APIs or support JSONPath name mapping. Other standard claims may be returned, but they cannot replace `sub` for binding.
|
|
346
|
+
|
|
347
|
+
### OIDC-06: Revocation (recommended)
|
|
348
|
+
|
|
349
|
+
When Discovery publishes a `revocation_endpoint`, the client submits its Refresh Token during logout, or its Access Token when no Refresh Token exists.
|
|
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
|
+
Remote revocation is best-effort during logout. The local OIDC session and runtime credential are cleared whether or not the remote call succeeds. Remote revocation of the model runtime API key remains the responsibility of Key Binding and model-platform lifecycle policy.
|
|
359
|
+
|
|
360
|
+
## 5. Key Binding interface group
|
|
361
|
+
|
|
362
|
+
### 5.1 Common conventions
|
|
363
|
+
|
|
364
|
+
The relative paths, HTTP methods, and wire field names of all four operations are fixed and cannot be configured in the Profile.
|
|
365
|
+
|
|
366
|
+
Every request MUST include:
|
|
367
|
+
|
|
368
|
+
```http
|
|
369
|
+
Authorization: Bearer <oidc-access-token>
|
|
370
|
+
Accept: application/json
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
The server MUST validate token issuer, audience, expiry, not-before, scopes, subject, account state, and institutional authorization. The subject MUST be derived from the validated Token. A subject, employee number, or name supplied in the request body MUST NOT be trusted as identity.
|
|
374
|
+
|
|
375
|
+
Recommended scopes:
|
|
376
|
+
|
|
377
|
+
| Interface | 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
|
+
An institution may use different scope names, but the actual names MUST be included in Profile `oidc.scopes` and enforced by the server.
|
|
385
|
+
|
|
386
|
+
### KEY-01: Bootstrap
|
|
387
|
+
|
|
388
|
+
Reads the Provider, server capabilities, user entitlement, and runtime-credential state. This operation MUST NOT return a model API key.
|
|
389
|
+
|
|
390
|
+
#### Request
|
|
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
|
+
#### Success response
|
|
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
|
+
#### Response fields
|
|
427
|
+
|
|
428
|
+
| Field | Required | Description |
|
|
429
|
+
| --- | --- | --- |
|
|
430
|
+
| `protocol_version` | No | When present, MUST be `worker-user-center/v1` |
|
|
431
|
+
| `provider.id` | Yes | Exact match with Profile `provider.id` |
|
|
432
|
+
| `capabilities` | Yes | String array with at most 256 entries |
|
|
433
|
+
| `runtime_credential.status` | Yes | Current credential state |
|
|
434
|
+
| `runtime_credential.api_key_id` | Conditional | Recommended for an existing credential; required by Renew |
|
|
435
|
+
| `runtime_credential.expires_at` | No | RFC 3339 timestamp |
|
|
436
|
+
| `runtime_credential.provisioning.allowed` | No | Whether credential creation is allowed |
|
|
437
|
+
| `runtime_credential.provisioning.reason` | No | Non-sensitive user-facing reason, at most 500 characters |
|
|
438
|
+
| `subject` | No | Management-plane display only; cannot override OIDC identity |
|
|
439
|
+
|
|
440
|
+
`runtime_credential.status` values:
|
|
441
|
+
|
|
442
|
+
| State | Client behavior |
|
|
443
|
+
| --- | --- |
|
|
444
|
+
| `missing` | Wait for explicit user consent, then call Provision |
|
|
445
|
+
| `active` | Call Resolve |
|
|
446
|
+
| `expiring` | Call Renew when `api_key_id` exists, otherwise Resolve |
|
|
447
|
+
| `expired` | Call Renew when `api_key_id` exists, otherwise Resolve |
|
|
448
|
+
| `suspended` | Clear the local runtime credential and do not invoke a model |
|
|
449
|
+
| `unavailable` | Clear the local runtime credential and show service unavailable |
|
|
450
|
+
|
|
451
|
+
### KEY-02: Provision
|
|
452
|
+
|
|
453
|
+
Creates or binds a runtime credential for the authenticated and authorized subject. Completing the OIDC callback alone does not call this operation; the user must explicitly consent in the UI.
|
|
454
|
+
|
|
455
|
+
#### Request
|
|
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
|
+
| Field/header | Required | Requirement |
|
|
470
|
+
| --- | --- | --- |
|
|
471
|
+
| `Idempotency-Key` | Yes | One UUID for one logical create operation |
|
|
472
|
+
| `provider_id` | Yes | Exact match with Profile `provider.id` |
|
|
473
|
+
|
|
474
|
+
The idempotency scope MUST include authenticated subject, Provider, and Idempotency-Key. A retry with the same key and payload MUST produce the same logical result. Reusing the same key with a different payload MUST return `409 Conflict`.
|
|
475
|
+
|
|
476
|
+
#### Success response
|
|
477
|
+
|
|
478
|
+
See [Common runtime-credential response](#55-common-runtime-credential-response). A first creation MAY return `201 Created`; an existing or completed binding MAY return `200 OK`.
|
|
479
|
+
|
|
480
|
+
### KEY-03: Resolve
|
|
481
|
+
|
|
482
|
+
Resolves the active runtime credential already bound to the current subject. Resolve MUST NOT create a new entitlement.
|
|
483
|
+
|
|
484
|
+
#### Request
|
|
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
|
+
| Field | Required | Description |
|
|
499
|
+
| --- | --- | --- |
|
|
500
|
+
| `provider_id` | Yes | Provider ID declared in the Profile |
|
|
501
|
+
| `api_key_id` | No | Known credential ID returned by Bootstrap |
|
|
502
|
+
|
|
503
|
+
Return `404` when no credential can be resolved. The server may rotate opaque secret material while preserving a logical `api_key_id`, but MUST NOT return a credential owned by another subject or Provider.
|
|
504
|
+
|
|
505
|
+
### KEY-04: Renew
|
|
506
|
+
|
|
507
|
+
Rotates an expiring or expired runtime credential.
|
|
508
|
+
|
|
509
|
+
#### Request
|
|
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`, and `Idempotency-Key` are all required. The same idempotency rules as Provision apply. After the new credential becomes active, the server SHOULD invalidate the old secret as soon as safely possible and record any necessary overlap window.
|
|
525
|
+
|
|
526
|
+
### 5.5 Common runtime-credential response
|
|
527
|
+
|
|
528
|
+
Provision, Resolve, and Renew use the same success structure:
|
|
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
|
+
| Field | Required | Requirement |
|
|
546
|
+
| --- | --- | --- |
|
|
547
|
+
| `provider_id` | Yes | Exact match with the request and Profile |
|
|
548
|
+
| `api_key` | Yes | Non-empty, at most 16 KiB; used only for the model gateway |
|
|
549
|
+
| `api_key_id` | No | Recommended for lifecycle management and audit correlation |
|
|
550
|
+
| `status` | Yes | Fixed to `active` |
|
|
551
|
+
| `expires_at` | No | RFC 3339 timestamp |
|
|
552
|
+
|
|
553
|
+
A response containing `api_key` MUST use `Cache-Control: no-store` and SHOULD use `Pragma: no-cache`. Reverse proxies, WAFs, APM agents, application logs, and audit logs MUST redact the Authorization header and `api_key`.
|
|
554
|
+
|
|
555
|
+
### 5.6 Key Binding error response
|
|
556
|
+
|
|
557
|
+
Errors SHOULD use 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 status | Meaning |
|
|
573
|
+
| --- | --- |
|
|
574
|
+
| `400` | Invalid request structure or Provider ID |
|
|
575
|
+
| `401` | Missing, invalid, expired, or wrong-audience Access Token; SHOULD include `WWW-Authenticate` |
|
|
576
|
+
| `403` | Scope, institutional membership, model entitlement, account state, or provisioning policy denial |
|
|
577
|
+
| `404` | No resolvable or renewable credential |
|
|
578
|
+
| `409` | Idempotency or credential-lifecycle conflict |
|
|
579
|
+
| `429` | Rate limit; SHOULD include `Retry-After` |
|
|
580
|
+
| `5xx` | Server failure; response MUST NOT contain a Token or API Key |
|
|
581
|
+
|
|
582
|
+
`code` SHOULD remain stable for UI and log classification. `detail` may vary but MUST NOT disclose sensitive information.
|
|
583
|
+
|
|
584
|
+
The machine-readable Key Binding contract is [`protocol/openapi.yaml`](../protocol/openapi.yaml).
|
|
585
|
+
|
|
586
|
+
## 6. Model-gateway interface group
|
|
587
|
+
|
|
588
|
+
### MODEL-01: Chat Completions
|
|
589
|
+
|
|
590
|
+
`dsh-oidc` currently invokes an OpenAI Chat Completions-compatible interface through DSH's official Pi adapter.
|
|
591
|
+
|
|
592
|
+
#### Request
|
|
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": "Hello" }
|
|
605
|
+
],
|
|
606
|
+
"stream": true,
|
|
607
|
+
"max_tokens": 1024
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
| Item | Requirement |
|
|
612
|
+
| --- | --- |
|
|
613
|
+
| Authentication | Accept the `api_key` returned by Key Binding; do not accept an OIDC Token as a substitute |
|
|
614
|
+
| `model` | Implement every model ID declared in Profile `provider.models` |
|
|
615
|
+
| `messages` | Support the OpenAI Chat Completions message structure |
|
|
616
|
+
| `stream` | Streaming responses are required |
|
|
617
|
+
| Image input | Required only when the Profile declares `input: ["text", "image"]` for that model |
|
|
618
|
+
| Reasoning parameters | Match Profile `reasoning`, `reasoningEfforts`, and `compat` declarations |
|
|
619
|
+
| Tools | MUST support `tools`, `tool_choice`, and streamed `tool_calls` for DSH Agent tool use |
|
|
620
|
+
|
|
621
|
+
#### Streaming success response
|
|
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":"Hel"}}]}
|
|
629
|
+
|
|
630
|
+
data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"lo"},"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
|
+
For every runtime API key, the server MUST enforce subject binding, Provider scope, credential state, expiry, revocation, and quota. A revoked or rotated old key MUST NOT retain indefinite access.
|
|
638
|
+
|
|
639
|
+
#### Error response
|
|
640
|
+
|
|
641
|
+
Errors SHOULD interoperate with OpenAI-compatible clients, for example:
|
|
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
|
+
The gateway SHOULD distinguish at least: `400` invalid request, `401` invalid credential, `403` access denied, `404` unknown model, `429` rate/quota limit, and `5xx` server failure.
|
|
654
|
+
|
|
655
|
+
The trusted Enterprise Profile declares the model catalog. A gateway does not need to implement `/models` to conform to this specification.
|
|
656
|
+
|
|
657
|
+
## 7. Cross-interface invariants
|
|
658
|
+
|
|
659
|
+
The service MUST guarantee:
|
|
660
|
+
|
|
661
|
+
1. UserInfo `sub` exactly equals ID Token `sub`.
|
|
662
|
+
2. Key Binding derives the same subject from the Access Token and does not trust request-body identity.
|
|
663
|
+
3. Profile `provider.id`, Bootstrap `provider.id`, and runtime-credential `provider_id` are exactly equal.
|
|
664
|
+
4. A runtime API key is authorized only for its subject and Provider.
|
|
665
|
+
5. Model IDs, input modalities, and reasoning capabilities actually served by the gateway match the Profile.
|
|
666
|
+
6. OIDC Tokens are used only for UserInfo and Key Binding; runtime API keys are used only for the model gateway. They are not interchangeable.
|
|
667
|
+
7. Account suspension, authorization revocation, and credential rotation propagate to runtime credentials and the model gateway.
|
|
668
|
+
8. Logs may contain a correlation ID, opaque subject reference, Provider ID, and `api_key_id`, but never a Token or `api_key`.
|
|
669
|
+
9. Bootstrap `subject` data is display-only and cannot override OIDC `sub` or `name`.
|
|
670
|
+
|
|
671
|
+
## 8. Standard call sequence
|
|
672
|
+
|
|
673
|
+
```text
|
|
674
|
+
Client -> OIDC-01 Discovery
|
|
675
|
+
Client -> OIDC-02 Authorization (browser + PKCE)
|
|
676
|
+
OIDC -> 127.0.0.1:<port>/oauth/callback
|
|
677
|
+
Client -> OIDC-03 Token
|
|
678
|
+
Client -> OIDC-04 JWKS (verify ID Token)
|
|
679
|
+
Client -> OIDC-05 UserInfo
|
|
680
|
+
Client -> KEY-01 Bootstrap
|
|
681
|
+
Client -> KEY-02 Provision (first use with explicit consent)
|
|
682
|
+
or KEY-03 Resolve (active credential)
|
|
683
|
+
or KEY-04 Renew (expiring/expired credential)
|
|
684
|
+
Client -> MODEL-01 Chat Completions
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
When the Access Token is close to expiry, the client uses the Refresh Token grant on OIDC-03. Logout clears the local OIDC session and runtime credential and calls OIDC-06 when available.
|
|
688
|
+
|
|
689
|
+
## 9. Integration acceptance checklist
|
|
690
|
+
|
|
691
|
+
A complete integration MUST pass at least these tests:
|
|
692
|
+
|
|
693
|
+
- exact Discovery issuer match, all required endpoints present, and PKCE `S256` advertised;
|
|
694
|
+
- callback uses only `127.0.0.1:<actual-port>/oauth/callback`;
|
|
695
|
+
- state, nonce, PKCE verifier, ID Token signature, audience, and expiry are validated;
|
|
696
|
+
- UserInfo returns a non-empty `sub` matching the ID Token, and `name` displays correctly;
|
|
697
|
+
- all four Key Binding operations exist, with Provision, Resolve, or Renew selected from lifecycle state;
|
|
698
|
+
- Provision and Renew are idempotent, and incompatible replay returns `409`;
|
|
699
|
+
- Provider ID is identical across Profile, Bootstrap, and credential response;
|
|
700
|
+
- a Key Binding Access Token cannot call the model gateway, and a model API key cannot call UserInfo or Key Binding;
|
|
701
|
+
- expired/revoked runtime credentials, suspended accounts, and unauthorized access are rejected;
|
|
702
|
+
- Chat Completions completes at least one streaming text conversation; declared multimodal and reasoning capabilities pass their corresponding tests;
|
|
703
|
+
- Tokens, Authorization headers, and `api_key` never appear in URLs, logs, APM, error details, or audit data;
|
|
704
|
+
- every failure response has a stable, diagnosable, non-secret error structure.
|
|
705
|
+
|
|
706
|
+
See [Enterprise Profile](enterprise-profile.en.md) for configuration fields, [OIDC interoperability](oidc-interoperability.en.md) for additional OIDC details, and [Key Binding protocol](key-binding-protocol.en.md) for lifecycle and security semantics.
|