@eduwork/dsh-oidc 0.2.4 → 0.3.0-dev.2

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.
Files changed (72) hide show
  1. package/CHANGELOG.en.md +6 -0
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +20 -132
  4. package/README_EN.md +20 -177
  5. package/docs/EDUWORK-MIGRATION.en.md +3 -25
  6. package/docs/EDUWORK-MIGRATION.md +3 -25
  7. package/docs/account-extensions.en.md +6 -6
  8. package/docs/account-extensions.md +3 -3
  9. package/docs/architecture.en.md +12 -117
  10. package/docs/architecture.md +12 -118
  11. package/docs/compatibility.en.md +6 -40
  12. package/docs/compatibility.md +6 -40
  13. package/docs/decisions/0001-one-plugin-composition-boundary.en.md +2 -2
  14. package/docs/decisions/0001-one-plugin-composition-boundary.md +2 -2
  15. package/docs/desktop-host.en.md +4 -5
  16. package/docs/desktop-host.md +8 -9
  17. package/docs/development.en.md +2 -2
  18. package/docs/development.md +2 -2
  19. package/docs/dsh-integration.en.md +11 -46
  20. package/docs/dsh-integration.md +12 -47
  21. package/docs/ecnu-reference.en.md +4 -61
  22. package/docs/ecnu-reference.md +4 -61
  23. package/docs/enterprise-profile.en.md +11 -32
  24. package/docs/enterprise-profile.md +12 -33
  25. package/docs/gateway-auth/README.md +168 -0
  26. package/docs/gateway-auth/README_EN.md +70 -0
  27. package/docs/gateway-auth/experimental-oidc-llm.en.md +53 -0
  28. package/docs/gateway-auth/experimental-oidc-llm.md +53 -0
  29. package/docs/gateway-auth/litellm-setup.en.md +114 -0
  30. package/docs/gateway-auth/litellm-setup.md +114 -0
  31. package/docs/gateway-auth/oidc-llm-draft.en.md +141 -0
  32. package/docs/gateway-auth/oidc-llm-draft.md +141 -0
  33. package/docs/getting-started.en.md +17 -122
  34. package/docs/getting-started.md +16 -225
  35. package/docs/key-binding-protocol.en.md +10 -220
  36. package/docs/key-binding-protocol.md +10 -220
  37. package/docs/oidc-interoperability.en.md +1 -1
  38. package/docs/oidc-interoperability.md +1 -1
  39. package/docs/open-integration.en.md +5 -67
  40. package/docs/open-integration.md +5 -67
  41. package/docs/public-resource-protocol.en.md +8 -44
  42. package/docs/public-resource-protocol.md +8 -43
  43. package/docs/security-model.en.md +10 -132
  44. package/docs/security-model.md +10 -132
  45. package/docs/server-integration-contract.en.md +12 -813
  46. package/docs/server-integration-contract.md +12 -825
  47. package/examples/ecnu.enterprise-profile.example.json +16 -20
  48. package/examples/enterprise-profile.example.json +16 -20
  49. package/examples/identity-only.example.json +7 -2
  50. package/examples/litellm.enterprise-profile.example.json +10 -0
  51. package/examples/oidc-llm.enterprise-profile.example.json +13 -0
  52. package/examples/resources-discovery.example.json +7 -9
  53. package/lib/callback-page.js +5 -5
  54. package/lib/client.js +31 -48
  55. package/lib/desktop-oidc.js +7 -11
  56. package/lib/gateway-backend.js +443 -0
  57. package/lib/gateway-protocol.js +6 -0
  58. package/lib/index.js +14 -15
  59. package/lib/litellm-protocol.js +117 -0
  60. package/lib/model-resource-transport.js +19 -0
  61. package/lib/oidc-llm-protocol.js +87 -0
  62. package/lib/oidc.js +40 -338
  63. package/lib/profile.js +42 -74
  64. package/lib/provider/authorization-scope.js +46 -0
  65. package/lib/provider/core.js +5 -16
  66. package/lib/provider/index.js +11 -2
  67. package/lib/transport.js +5 -0
  68. package/lib/typert-schemas.js +1 -1
  69. package/package.json +2 -2
  70. package/protocol/openapi.yaml +33 -240
  71. package/protocol/resources.openapi.yaml +8 -9
  72. package/schema/enterprise-profile.v1alpha1.schema.json +508 -107
@@ -1,824 +1,23 @@
1
- # EduWork identity and resource integration (RFC EW-IDENTITY-1)
2
-
3
- > Capability scope: quota and activity heartbeat are institution-only extensions. The public package never requests /quota and its resources RPC contains only model metadata. No capability string or configuration flag enables quota in the public package. ECNU retains its existing server wire in a separate package.
4
-
5
- > Two integration modes are supported: identity-only OIDC can omit all resource interfaces and use personal API keys. The complete organization-resource mode below adds managed credentials and models; see [Public resource protocol](public-resource-protocol.en.md). The mandatory requirements below apply to complete integration, not identity-only login.
1
+ # Server integration contract
6
2
 
7
3
  [简体中文](server-integration-contract.md) | **English**
8
4
 
9
- 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`.
10
-
11
- For the goals behind these interfaces and ways to contribute to their evolution, see the [Open Identity and Model Integration Initiative](open-integration.en.md). This specification remains the implementation and integration reference; the initiative does not change existing fields, paths, or compatibility rules.
12
-
13
- Status: implemented client wire specification, not an IETF standard. Source code and HTTP contract tests determine current behavior. Section 10 provides curl examples; section 11 contains **unimplemented optional proposals**. Every client is a local, single-user Runtime; this is not a shared Web-login architecture.
14
-
15
- | Capability | Required interfaces | Optionality and implemented behavior |
16
- | --- | --- | --- |
17
- | Identity | Discovery, Authorization, Token, JWKS, UserInfo | No resource API needed; personal model keys remain available |
18
- | Managed models | Identity, Bootstrap, Key lifecycle, Chat Completions | Enabled by keyBinding/provider together; provisioning requires user consent |
19
- | Model discovery | GET /models | Required only for modelSource=discovery |
20
- | Token refresh/revocation | Refresh grant / Discovery revocation_endpoint | Optional; missing refresh means signing in again after expiry |
21
- | Remote Profile discovery, remote model-key logout | Section 11 proposals | Not automatically requested by the public client |
22
-
23
- Optional interfaces still require valid fields when implemented. Quota belongs to the ECNU institution extension and is not a public integration requirement. An identity-only integration must not be represented as complete model integration. Trusted local configuration owns branding, issuer, service origins and model policy. Bootstrap does not overwrite configuration or load executable extensions. No school-specific service is required.
24
-
25
- Servers must implement the complete fields and constraints in this document and OpenAPI. Client tolerance for some legacy missing values does not waive server security requirements.
26
-
27
- “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.
28
-
29
- ## 1. Integration scope
30
-
31
- A complete **institutional enterprise-model integration service** MUST provide all three interface groups:
32
-
33
- | Interface group | Purpose | Enterprise Profile configuration | Requirement |
34
- | --- | --- | --- | --- |
35
- | OIDC Provider | Login, user identity, and OIDC Access Tokens | `oidc.issuer`, `oidc.clientId`, `oidc.scopes` | Complete implementation required |
36
- | Key Binding | Bind an authenticated user to a revocable model runtime credential | `keyBinding.baseURL` | All four operations required |
37
- | Model gateway | Invoke enterprise models with the bound runtime credential | `provider.baseURL`, `provider.id`, `provider.models` | Chat Completions required |
38
-
39
- “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.
40
-
41
- The following arrangements do not conform:
42
-
43
- - OIDC alone, followed by distribution of one global model API key to all users;
44
- - Key Binding that trusts an employee number, name, or subject supplied by the client;
45
- - a model URL and catalog without user authorization and credential lifecycle management;
46
- - a Key Binding service missing any of `bootstrap`, `provision`, `resolve`, or `renew`.
47
-
48
- ## 2. Addresses and client registration
49
-
50
- ### 2.1 Three service entry points
51
-
52
- An Enterprise Profile declares the three entry points separately:
53
-
54
- ```json
55
- {
56
- "oidc": {
57
- "issuer": "https://id.example.edu/oidc",
58
- "clientId": "dsh-web-public-client",
59
- "scopes": ["openid", "profile", "offline_access"]
60
- },
61
- "keyBinding": {
62
- "baseURL": "https://ai.example.edu/api/worker/v1"
63
- },
64
- "provider": {
65
- "id": "example-ai",
66
- "baseURL": "https://ai.example.edu/open/api/v1",
67
- "adapter": "openai-compatible",
68
- "models": [
69
- { "id": "example-model", "input": ["text"] }
70
- ]
71
- }
72
- }
73
- ```
74
-
75
- 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.
76
-
77
- ### 2.2 OIDC Public Client
78
-
79
- The identity platform MUST register a Public Client without a Client Secret and enable:
80
-
81
- - Authorization Code Flow;
82
- - PKCE `S256`;
83
- - the `openid` and `profile` scopes;
84
- - `offline_access` and the corresponding authorization policy when Refresh Tokens are required.
85
-
86
- Desktop callbacks use an available random loopback port by default:
87
-
88
- ```text
89
- http://127.0.0.1:<actual-port>/oauth/callback
90
- ```
91
-
92
- Constraints:
93
-
94
- - the host is fixed to `127.0.0.1`, not `localhost`, a LAN address, or a public hostname;
95
- - the path is fixed to `/oauth/callback`;
96
- - Desktop uses an available loopback port chosen for each attempt (RFC 8252). The authorization server MUST support this native-client registration pattern; `3080` below is only an example. `desktop.callbackPort` optionally fixes the port and fails clearly on conflicts. Web validation uses its actual WebServer port;
97
- - this is the browser's return address to the user's local DSH process, not an institutional server address;
98
- - the current implementation is a local single-user client and does not support a shared public-Web callback.
99
-
100
- ## 3. API summary
101
-
102
- | ID | Method | Address | Authentication | Required |
103
- | --- | --- | --- | --- | --- |
104
- | OIDC-01 | `GET` | `{issuer}/.well-known/openid-configuration` | None | Yes |
105
- | OIDC-02 | `GET` | Discovery `authorization_endpoint` | User login session | Yes |
106
- | OIDC-03 | `POST` | Discovery `token_endpoint` | Authorization Code + PKCE | Yes |
107
- | OIDC-04 | `GET` | Discovery `jwks_uri` | None | Yes |
108
- | OIDC-05 | `GET` | Discovery `userinfo_endpoint` | OIDC Bearer Access Token | Yes |
109
- | OIDC-06 | `POST` | Discovery `revocation_endpoint` | OIDC Token | Recommended |
110
- | KEY-01 | `GET` | `{keyBinding.baseURL}/bootstrap` | OIDC Bearer Access Token | Yes |
111
- | KEY-02 | `POST` | `{keyBinding.baseURL}/runtime-credential/provision` | OIDC Bearer Access Token | Yes |
112
- | KEY-03 | `POST` | `{keyBinding.baseURL}/runtime-credential/resolve` | OIDC Bearer Access Token | Yes |
113
- | KEY-04 | `POST` | `{keyBinding.baseURL}/runtime-credential/renew` | OIDC Bearer Access Token | Yes |
114
- | MODEL-01 | `POST` | `{provider.baseURL}/chat/completions` | Runtime API Key | Yes |
115
- | MODEL-02 | `GET` | `{provider.baseURL}/models` | Runtime API Key | If `modelSource=discovery` |
116
-
117
- The loopback callback and local `oidcAccounts.*` RPC methods belong to the client, not the enterprise server.
118
-
119
- ## 4. OIDC interface group
120
-
121
- ### OIDC-01: Discovery
122
-
123
- Reads OIDC Provider metadata.
124
-
125
- #### Request
126
-
127
- ```http
128
- GET /oidc/.well-known/openid-configuration HTTP/1.1
129
- Host: id.example.edu
130
- Accept: application/json
131
- ```
132
-
133
- When the issuer itself has a path, the Discovery address is:
134
-
135
- ```text
136
- {issuer-without-trailing-slash}/.well-known/openid-configuration
137
- ```
138
-
139
- #### Success response
140
-
141
- ```http
142
- HTTP/1.1 200 OK
143
- Content-Type: application/json
144
-
145
- {
146
- "issuer": "https://id.example.edu/oidc",
147
- "authorization_endpoint": "https://id.example.edu/oidc/authorize",
148
- "token_endpoint": "https://id.example.edu/oidc/token",
149
- "jwks_uri": "https://id.example.edu/oidc/jwks",
150
- "userinfo_endpoint": "https://id.example.edu/oidc/userinfo",
151
- "revocation_endpoint": "https://id.example.edu/oidc/revoke",
152
- "code_challenge_methods_supported": ["S256"],
153
- "id_token_signing_alg_values_supported": ["RS256"]
154
- }
155
- ```
156
-
157
- #### Field requirements
158
-
159
- | Field | Required | Requirement |
160
- | --- | --- | --- |
161
- | `issuer` | Yes | Exact string match with Enterprise Profile `oidc.issuer` |
162
- | `authorization_endpoint` | Yes | Absolute HTTPS URL |
163
- | `token_endpoint` | Yes | Absolute HTTPS URL |
164
- | `jwks_uri` | Yes | Absolute HTTPS URL |
165
- | `userinfo_endpoint` | Yes | Required by this interoperability profile |
166
- | `revocation_endpoint` | No | When present, the client attempts revocation during logout |
167
- | `code_challenge_methods_supported` | Yes | MUST contain `S256` |
168
- | `id_token_signing_alg_values_supported` | Should | SHOULD contain `RS256`; the actual ID Token MUST use RS256 |
169
-
170
- Discovered endpoints may use different HTTPS origins. Development HTTP is accepted only for the single explicitly configured development origin or a loopback origin.
171
-
172
- ### OIDC-02: Authorization
173
-
174
- The browser starts Authorization Code Flow with PKCE.
175
-
176
- #### Request
177
-
178
- ```http
179
- GET /oidc/authorize?
180
- response_type=code&
181
- client_id=dsh-web-public-client&
182
- redirect_uri=http%3A%2F%2F127.0.0.1%3A3080%2Foauth%2Fcallback&
183
- scope=openid%20profile%20offline_access&
184
- state=<random-state>&
185
- nonce=<random-nonce>&
186
- code_challenge=<base64url-sha256>&
187
- code_challenge_method=S256 HTTP/1.1
188
- Host: id.example.edu
189
- ```
190
-
191
- #### Query parameters
192
-
193
- | Parameter | Requirement |
194
- | --- | --- |
195
- | `response_type` | Fixed to `code` |
196
- | `client_id` | Equal to Profile `oidc.clientId` |
197
- | `redirect_uri` | Exact match with the registered loopback callback |
198
- | `scope` | At least `openid profile` |
199
- | `state` | Generated by the client and returned unchanged |
200
- | `nonce` | Generated by the client and included in the corresponding ID Token |
201
- | `code_challenge` | PKCE challenge |
202
- | `code_challenge_method` | Fixed to `S256` |
203
-
204
- #### Success redirect
205
-
206
- ```http
207
- HTTP/1.1 302 Found
208
- Location: http://127.0.0.1:3080/oauth/callback?code=<authorization-code>&state=<original-state>
209
- ```
210
-
211
- An RFC 9207 `iss` response parameter MAY also be returned. When present, it MUST equal the Profile issuer.
212
-
213
- #### Error redirect
214
-
215
- ```text
216
- http://127.0.0.1:3080/oauth/callback?error=access_denied&error_description=...&state=<original-state>
217
- ```
218
-
219
- 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.
220
-
221
- ### OIDC-03: Token
222
-
223
- The client exchanges an Authorization Code and PKCE verifier for tokens. The Public Client does not send a Client Secret.
224
-
225
- #### Authorization Code request
226
-
227
- ```http
228
- POST /oidc/token HTTP/1.1
229
- Host: id.example.edu
230
- Content-Type: application/x-www-form-urlencoded
231
- Accept: application/json
232
-
233
- grant_type=authorization_code&
234
- client_id=dsh-web-public-client&
235
- code=<authorization-code>&
236
- redirect_uri=http%3A%2F%2F127.0.0.1%3A3080%2Foauth%2Fcallback&
237
- code_verifier=<pkce-verifier>
238
- ```
239
-
240
- #### Success response
241
-
242
- ```http
243
- HTTP/1.1 200 OK
244
- Content-Type: application/json
245
- Cache-Control: no-store
246
-
247
- {
248
- "access_token": "<oidc-access-token>",
249
- "token_type": "Bearer",
250
- "expires_in": 3600,
251
- "id_token": "<signed-id-token>",
252
- "refresh_token": "<optional-refresh-token>",
253
- "scope": "openid profile offline_access"
254
- }
255
- ```
256
-
257
- | Field | Required | Requirement |
258
- | --- | --- | --- |
259
- | `access_token` | Yes | Non-empty; used for UserInfo and Key Binding |
260
- | `token_type` | Yes | `Bearer`, case-insensitive |
261
- | `expires_in` | Should | Positive seconds; the client uses 3600 seconds when absent |
262
- | `id_token` | Yes | RS256-signed JWT |
263
- | `refresh_token` | No | Recommended when `offline_access` is enabled |
264
- | `scope` | No | Actually granted scopes |
265
-
266
- The ID Token MUST contain at least:
267
-
268
- | Claim | Requirement |
269
- | --- | --- |
270
- | `iss` | Equal to Profile `oidc.issuer` |
271
- | `aud` | Contains `oidc.clientId`; with multiple audiences, `azp` MUST equal that client ID |
272
- | `sub` | Non-empty, stable user identifier |
273
- | `iat`, `exp` | Valid NumericDate values |
274
- | `nonce` | Exact match with the Authorization request |
275
- | `nbf` | Optional; valid when present |
276
- | `at_hash` | Optional; MUST match the Access Token when present |
277
-
278
- The JWT header MUST use `alg=RS256` and contain a non-empty `kid`.
279
-
280
- #### Refresh Token request
281
-
282
- ```http
283
- POST /oidc/token HTTP/1.1
284
- Content-Type: application/x-www-form-urlencoded
285
- Accept: application/json
286
-
287
- grant_type=refresh_token&
288
- client_id=dsh-web-public-client&
289
- refresh_token=<refresh-token>
290
- ```
291
-
292
- 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.
293
-
294
- #### Error response
295
-
296
- Token errors follow OAuth 2.0:
297
-
298
- ```http
299
- HTTP/1.1 400 Bad Request
300
- Content-Type: application/json
301
- Cache-Control: no-store
302
-
303
- {
304
- "error": "invalid_grant",
305
- "error_description": "Authorization code is invalid or expired"
306
- }
307
- ```
308
-
309
- ### OIDC-04: JWKS
310
-
311
- Publishes the public signing keys needed to verify ID Tokens.
312
-
313
- #### Request
314
-
315
- ```http
316
- GET /oidc/jwks HTTP/1.1
317
- Accept: application/json
318
- ```
319
-
320
- #### Success response
321
-
322
- ```json
323
- {
324
- "keys": [
325
- {
326
- "kty": "RSA",
327
- "kid": "signing-key-2026-01",
328
- "use": "sig",
329
- "alg": "RS256",
330
- "n": "<base64url-modulus>",
331
- "e": "AQAB"
332
- }
333
- ]
334
- }
335
- ```
336
-
337
- 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.
338
-
339
- ### OIDC-05: UserInfo
340
-
341
- Returns the standard user identity represented by the current Access Token.
342
-
343
- #### Request
344
-
345
- ```http
346
- GET /oidc/userinfo HTTP/1.1
347
- Authorization: Bearer <oidc-access-token>
348
- Accept: application/json
349
- ```
350
-
351
- #### Success response
352
-
353
- ```http
354
- HTTP/1.1 200 OK
355
- Content-Type: application/json
356
-
357
- {
358
- "sub": "user-12345",
359
- "name": "Example User"
360
- }
361
- ```
362
-
363
- | Field | Required | Purpose |
364
- | --- | --- | --- |
365
- | `sub` | Yes | Exact match with ID Token `sub` |
366
- | `name` | Should | Human-readable UI label; the client falls back to `sub` when absent or empty |
367
-
368
- 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.
369
-
370
- ### OIDC-06: Revocation (recommended)
371
-
372
- When Discovery publishes a `revocation_endpoint`, the client submits its Refresh Token during logout, or its Access Token when no Refresh Token exists.
373
-
374
- ```http
375
- POST /oidc/revoke HTTP/1.1
376
- Content-Type: application/x-www-form-urlencoded
377
-
378
- token=<refresh-or-access-token>&client_id=dsh-web-public-client
379
- ```
380
-
381
- 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.
382
-
383
- ## 5. Key Binding interface group
384
-
385
- ### 5.1 Common conventions
386
-
387
- The relative paths, HTTP methods, and wire field names of all four operations are fixed and cannot be configured in the Profile.
388
-
389
- Every request MUST include:
390
-
391
- ```http
392
- Authorization: Bearer <oidc-access-token>
393
- Accept: application/json
394
- ```
395
-
396
- 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.
397
-
398
- Recommended scopes:
399
-
400
- | Interface | Scope |
401
- | --- | --- |
402
- | KEY-01 Bootstrap | `worker.bootstrap.read` |
403
- | KEY-02 Provision | `worker.credential.provision` |
404
- | KEY-03 Resolve | `worker.credential.read` |
405
- | KEY-04 Renew | `worker.credential.renew` |
406
-
407
- An institution may use different scope names, but the actual names MUST be included in Profile `oidc.scopes` and enforced by the server.
408
-
409
- ### KEY-01: Bootstrap
410
-
411
- Reads the Provider, server capabilities, user entitlement, and runtime-credential state. This operation MUST NOT return a model API key.
412
-
413
- #### Request
414
-
415
- ```http
416
- GET /api/worker/v1/bootstrap HTTP/1.1
417
- Host: ai.example.edu
418
- Authorization: Bearer <oidc-access-token>
419
- Accept: application/json
420
- ```
421
-
422
- #### Success response
423
-
424
- ```http
425
- HTTP/1.1 200 OK
426
- Content-Type: application/json
427
- Cache-Control: no-store
428
-
429
- {
430
- "protocol_version": "worker-user-center/v1",
431
- "provider": {
432
- "id": "example-ai"
433
- },
434
- "capabilities": [
435
- "worker.bootstrap.read",
436
- "worker.credential.provision",
437
- "worker.credential.read",
438
- "worker.credential.renew"
439
- ],
440
- "runtime_credential": {
441
- "status": "missing",
442
- "provisioning": {
443
- "allowed": true
444
- }
445
- }
446
- }
447
- ```
448
-
449
- #### Response fields
450
-
451
- | Field | Required | Description |
452
- | --- | --- | --- |
453
- | `protocol_version` | Optional for legacy; new servers SHOULD provide it | Accepted aliases with identical wire: `worker.user-center.v1`, `worker-user-center/v1`, `eduwork-resources/v1`; unknown versions are rejected |
454
- | `provider.id` | Yes | Exact match with Profile `provider.id` |
455
- | `capabilities` | Yes | String array with at most 256 entries |
456
- | `runtime_credential.status` | Yes | Current credential state |
457
- | `runtime_credential.api_key_id` | Conditional | Recommended for an existing credential; required by Renew |
458
- | `runtime_credential.expires_at` | No | RFC 3339 timestamp |
459
- | `runtime_credential.provisioning.allowed` | No | Whether credential creation is allowed |
460
- | `runtime_credential.provisioning.reason` | No | Non-sensitive user-facing reason, at most 500 characters |
461
- | `subject` | No | Management-plane display only; cannot override OIDC identity |
462
-
463
- `runtime_credential.status` values:
464
-
465
- | State | Client behavior |
466
- | --- | --- |
467
- | `missing` | Wait for explicit user consent, then call Provision |
468
- | `active` | Call Resolve |
469
- | `expiring` | Call Renew when `api_key_id` exists, otherwise Resolve |
470
- | `expired` | Call Renew when `api_key_id` exists, otherwise Resolve |
471
- | `suspended` | Clear the local runtime credential and do not invoke a model |
472
- | `unavailable` | Clear the local runtime credential and show service unavailable |
473
-
474
- ### KEY-02: Provision
475
-
476
- 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.
477
-
478
- #### Request
479
-
480
- ```http
481
- POST /api/worker/v1/runtime-credential/provision HTTP/1.1
482
- Authorization: Bearer <oidc-access-token>
483
- Content-Type: application/json
484
- Accept: application/json
485
- Idempotency-Key: 2b9b6c80-7758-4d3f-94c6-b72d91ca8c53
486
-
487
- {
488
- "provider_id": "example-ai"
489
- }
490
- ```
491
-
492
- | Field/header | Required | Requirement |
493
- | --- | --- | --- |
494
- | `Idempotency-Key` | Yes | One UUID for one logical create operation |
495
- | `provider_id` | Yes | Exact match with Profile `provider.id` |
496
-
497
- 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`.
498
-
499
- #### Success response
500
-
501
- 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`.
502
-
503
- ### KEY-03: Resolve
504
-
505
- Resolves the active runtime credential already bound to the current subject. Resolve MUST NOT create a new entitlement.
506
-
507
- #### Request
508
-
509
- ```http
510
- POST /api/worker/v1/runtime-credential/resolve HTTP/1.1
511
- Authorization: Bearer <oidc-access-token>
512
- Content-Type: application/json
513
- Accept: application/json
514
-
515
- {
516
- "provider_id": "example-ai",
517
- "api_key_id": "key-123"
518
- }
519
- ```
520
-
521
- | Field | Required | Description |
522
- | --- | --- | --- |
523
- | `provider_id` | Yes | Provider ID declared in the Profile |
524
- | `api_key_id` | No | Known credential ID returned by Bootstrap |
525
-
526
- 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.
527
-
528
- ### KEY-04: Renew
529
-
530
- Rotates an expiring or expired runtime credential.
531
-
532
- #### Request
533
-
534
- ```http
535
- POST /api/worker/v1/runtime-credential/renew HTTP/1.1
536
- Authorization: Bearer <oidc-access-token>
537
- Content-Type: application/json
538
- Accept: application/json
539
- Idempotency-Key: 2f94dc48-201a-475e-a61a-69d7880e1186
540
-
541
- {
542
- "provider_id": "example-ai",
543
- "api_key_id": "key-123"
544
- }
545
- ```
546
-
547
- `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.
548
-
549
- ### 5.5 Common runtime-credential response
550
-
551
- Provision, Resolve, and Renew use the same success structure:
552
-
553
- ```http
554
- HTTP/1.1 200 OK
555
- Content-Type: application/json
556
- Cache-Control: no-store
557
- Pragma: no-cache
558
-
559
- {
560
- "provider_id": "example-ai",
561
- "api_key": "<secret-runtime-api-key>",
562
- "api_key_id": "key-123",
563
- "status": "active",
564
- "expires_at": "2026-09-01T00:00:00Z"
565
- }
566
- ```
567
-
568
- | Field | Required | Requirement |
569
- | --- | --- | --- |
570
- | `provider_id` | Yes | Exact match with the request and Profile |
571
- | `api_key` | Yes | Non-empty, at most 16 KiB; used only for the model gateway |
572
- | `api_key_id` | No | Recommended for lifecycle management and audit correlation |
573
- | `status` | Yes | Fixed to `active` |
574
- | `expires_at` | No | RFC 3339 timestamp |
575
-
576
- 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`.
577
-
578
- ### 5.6 Key Binding error response
579
-
580
- Errors SHOULD use RFC 9457 `application/problem+json`:
581
-
582
- ```http
583
- HTTP/1.1 403 Forbidden
584
- Content-Type: application/problem+json
585
-
586
- {
587
- "type": "https://ai.example.edu/problems/model-entitlement-denied",
588
- "title": "Model entitlement denied",
589
- "status": 403,
590
- "detail": "The current account cannot use this provider",
591
- "code": "model_entitlement_denied"
592
- }
593
- ```
594
-
595
- | HTTP status | Meaning |
596
- | --- | --- |
597
- | `400` | Invalid request structure or Provider ID |
598
- | `401` | Missing, invalid, expired, or wrong-audience Access Token; SHOULD include `WWW-Authenticate` |
599
- | `403` | Scope, institutional membership, model entitlement, account state, or provisioning policy denial |
600
- | `404` | No resolvable or renewable credential |
601
- | `409` | Idempotency or credential-lifecycle conflict |
602
- | `429` | Rate limit; SHOULD include `Retry-After` |
603
- | `5xx` | Server failure; response MUST NOT contain a Token or API Key |
604
-
605
- `code` SHOULD remain stable for UI and log classification. `detail` may vary but MUST NOT disclose sensitive information.
606
-
607
- The machine-readable Key Binding contract is [`protocol/openapi.yaml`](../protocol/openapi.yaml).
608
-
609
- ## 6. Model-gateway interface group
610
-
611
- ### MODEL-01: Chat Completions
612
-
613
- `dsh-oidc` currently invokes an OpenAI Chat Completions-compatible interface through DSH's official Pi adapter.
614
-
615
- #### Request
616
-
617
- ```http
618
- POST /open/api/v1/chat/completions HTTP/1.1
619
- Host: ai.example.edu
620
- Authorization: Bearer <runtime-api-key>
621
- Content-Type: application/json
622
- Accept: text/event-stream
623
-
624
- {
625
- "model": "example-model",
626
- "messages": [
627
- { "role": "user", "content": "Hello" }
628
- ],
629
- "stream": true,
630
- "max_tokens": 1024
631
- }
632
- ```
633
-
634
- | Item | Requirement |
635
- | --- | --- |
636
- | Authentication | Accept the `api_key` returned by Key Binding; do not accept an OIDC Token as a substitute |
637
- | `model` | Implement every model ID declared in Profile `provider.models` |
638
- | `messages` | Support the OpenAI Chat Completions message structure |
639
- | `stream` | Streaming responses are required |
640
- | Image input | Required only when the Profile declares `input: ["text", "image"]` for that model |
641
- | Reasoning parameters | Match Profile `reasoning`, `reasoningEfforts`, and `compat` declarations |
642
- | Tools | MUST support `tools`, `tool_choice`, and streamed `tool_calls` for DSH Agent tool use |
643
-
644
- #### Streaming success response
645
-
646
- ```http
647
- HTTP/1.1 200 OK
648
- Content-Type: text/event-stream
649
- Cache-Control: no-cache
650
-
651
- data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hel"}}]}
652
-
653
- data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"lo"},"finish_reason":null}]}
654
-
655
- data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
656
-
657
- data: [DONE]
658
- ```
659
-
660
- For every runtime API key, the server MUST enforce subject binding, Provider scope, credential state, expiry, revocation, and quota policy when enabled. This server-side policy does not require public quota UI or a `/quota` endpoint. A revoked or rotated old key MUST NOT retain indefinite access.
661
-
662
- #### Error response
663
-
664
- Errors SHOULD interoperate with OpenAI-compatible clients, for example:
665
-
666
- ```json
667
- {
668
- "error": {
669
- "message": "Runtime credential has expired",
670
- "type": "authentication_error",
671
- "code": "credential_expired"
672
- }
673
- }
674
- ```
675
-
676
- 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.
677
-
678
- Static `modelSource=profile` deployments may omit `/models`; `modelSource=discovery` deployments implement MODEL-02. A missing discovery catalog must not be replaced with invented model IDs.
679
-
680
- ### MODEL-02: Model catalog
681
-
682
- `GET {provider.baseURL}/models`, authenticated with `Authorization: Bearer <runtime-api-key>`, returns HTTP 200 JSON:
683
-
684
- ```json
685
- {"data":[{"id":"example-model","object":"model","owned_by":"example-ai"}]}
686
- ```
687
-
688
- `data` contains at most 128 distinct IDs, each 1–256 characters, available to that key. The client consumes IDs only. Profile-reviewed models retain their declared context, input modalities and reasoning effort levels; new IDs default to text input with no reasoning_effort. Additional OpenAI-style fields are ignored. Empty/invalid/unavailable discovery retains the last valid catalog and reports `models_unavailable`. Remote capabilities, arbitrary URLs and executable configuration are not accepted. Accurate advanced capabilities currently belong in the trusted Profile.
689
-
690
- ## 7. Cross-interface invariants
691
-
692
- The service MUST guarantee:
693
-
694
- 1. UserInfo `sub` exactly equals ID Token `sub`.
695
- 2. Key Binding derives the same subject from the Access Token and does not trust request-body identity.
696
- 3. Profile `provider.id`, Bootstrap `provider.id`, and runtime-credential `provider_id` are exactly equal.
697
- 4. A runtime API key is authorized only for its subject and Provider.
698
- 5. Model IDs, input modalities, and reasoning capabilities actually served by the gateway match the Profile.
699
- 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.
700
- 7. Account suspension, authorization revocation, and credential rotation propagate to runtime credentials and the model gateway.
701
- 8. Logs may contain a correlation ID, opaque subject reference, Provider ID, and `api_key_id`, but never a Token or `api_key`.
702
- 9. Bootstrap `subject` data is display-only and cannot override OIDC `sub` or `name`.
703
-
704
- ## 8. Standard call sequence
705
-
706
- ```text
707
- Client -> OIDC-01 Discovery
708
- Client -> OIDC-02 Authorization (browser + PKCE)
709
- OIDC -> 127.0.0.1:<port>/oauth/callback
710
- Client -> OIDC-03 Token
711
- Client -> OIDC-04 JWKS (verify ID Token)
712
- Client -> OIDC-05 UserInfo
713
- Client -> KEY-01 Bootstrap
714
- Client -> KEY-02 Provision (first use with explicit consent)
715
- or KEY-03 Resolve (active credential)
716
- or KEY-04 Renew (expiring/expired credential)
717
- Client -> MODEL-01 Chat Completions
718
- ```
719
-
720
- 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.
721
-
722
- ## 9. Integration acceptance checklist
723
-
724
- A complete integration MUST pass at least these tests:
725
-
726
- - exact Discovery issuer match, all required endpoints present, and PKCE `S256` advertised;
727
- - callback uses only `127.0.0.1:<actual-port>/oauth/callback`;
728
- - state, nonce, PKCE verifier, ID Token signature, audience, and expiry are validated;
729
- - UserInfo returns a non-empty `sub` matching the ID Token, and `name` displays correctly;
730
- - all four Key Binding operations exist, with Provision, Resolve, or Renew selected from lifecycle state;
731
- - Provision and Renew are idempotent, and incompatible replay returns `409`;
732
- - Provider ID is identical across Profile, Bootstrap, and credential response;
733
- - a Key Binding Access Token cannot call the model gateway, and a model API key cannot call UserInfo or Key Binding;
734
- - expired/revoked runtime credentials, suspended accounts, and unauthorized access are rejected;
735
- - Chat Completions completes at least one streaming text conversation; declared multimodal and reasoning capabilities pass their corresponding tests;
736
- - Tokens, Authorization headers, and `api_key` never appear in URLs, logs, APM, error details, or audit data;
737
- - every failure response has a stable, diagnosable, non-secret error structure.
738
-
739
- 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.
740
-
741
- ## 10. Refresh, logout and HTTP request examples
742
-
743
- The client refreshes access tokens within 90 seconds of expiry and coalesces concurrent refreshes. A management `401` refreshes and retries once with the same body and idempotency key. `invalid_grant` clears local identity and only a model key still owned by it; transient failures preserve valid credentials. `status` projects state; `reconcile` checks the server.
744
-
745
- Normal OIDC/management/resource timeouts are 20 seconds, revocation 10 seconds. Management/resource requests reject redirects. There is no scheduled `Retry-After` retry for management operations; a later user action creates a new idempotency key. Provision must also prevent duplicate active credentials for the same subject/provider.
746
-
747
- Logout cancels pending desktop login and clears the profile's identity and model snapshot. It deletes a local managed key only while its saved fingerprint still owns that value, preserving another login's key and personal keys. Revocation is best effort when advertised; browser-wide SSO logout and remote model-key revocation are not implemented. The server remains responsible for denying expired, revoked or disabled credentials.
748
-
749
- The following POSIX shell examples contain placeholders; Windows can use WSL. Requests originate in the user's local Runtime, so browser CORS is not needed. Do not put real secrets into shared logs or tickets. `ACCESS_TOKEN` and `RUNTIME_KEY` have different purposes. All OIDC endpoints must come from verified Discovery.
750
-
751
- ```bash
752
- ISSUER='https://id.example.edu/oidc'
753
- MANAGEMENT='https://ai.example.edu/api/worker/v1'
754
- MODEL_BASE='https://ai.example.edu/open/api/v1'
755
- CLIENT_ID='eduwork-public-client'
756
- REDIRECT_URI='http://127.0.0.1:3080/oauth/callback'
757
- curl --fail-with-body --max-time 20 "$ISSUER/.well-known/openid-configuration"
758
- # Copy these URLs from verified Discovery; their paths are not fixed.
759
- AUTHORIZATION_ENDPOINT='https://id.example.edu/oidc/authorize'
760
- TOKEN_ENDPOINT='https://id.example.edu/oidc/token'
761
- JWKS_URI='https://id.example.edu/oidc/jwks'
762
- USERINFO_ENDPOINT='https://id.example.edu/oidc/userinfo'
763
- REVOCATION_ENDPOINT='https://id.example.edu/oidc/revoke'
764
- curl --fail-with-body --max-time 20 "$JWKS_URI"
765
- ```
766
-
767
- Authorization must complete in the system browser. The GET below illustrates the request, not a way to bypass institutional login. Generate state, nonce and the verifier with a secure random source; the S256 challenge is BASE64URL(SHA256(verifier)). The original client owns and validates the callback and ID Token.
768
-
769
- ```bash
770
- curl --get --max-time 20 "$AUTHORIZATION_ENDPOINT" \
771
- --data-urlencode 'response_type=code' --data-urlencode "client_id=$CLIENT_ID" \
772
- --data-urlencode "redirect_uri=$REDIRECT_URI" --data-urlencode 'scope=openid profile offline_access' \
773
- --data-urlencode "state=$STATE" --data-urlencode "nonce=$NONCE" \
774
- --data-urlencode "code_challenge=$PKCE_CHALLENGE" --data-urlencode 'code_challenge_method=S256'
775
- curl --fail-with-body --max-time 20 "$TOKEN_ENDPOINT" \
776
- --data-urlencode 'grant_type=authorization_code' --data-urlencode "client_id=$CLIENT_ID" \
777
- --data-urlencode "code=$CODE" --data-urlencode "redirect_uri=$REDIRECT_URI" --data-urlencode "code_verifier=$PKCE_VERIFIER"
778
- curl --fail-with-body --max-time 20 -H "Authorization: Bearer $ACCESS_TOKEN" "$USERINFO_ENDPOINT"
779
- curl --fail-with-body --max-time 20 -H "Authorization: Bearer $ACCESS_TOKEN" "$MANAGEMENT/bootstrap"
780
- ```
781
-
782
- Perform only the lifecycle operation selected by Bootstrap. Provision needs user consent. Reuse one idempotency key for retries of a logical write, with a separate key for Renew. `api_key_id` is the string identifier returned by the server. Supply the actual resource scopes described in section 5.1 when the deployment requires them.
783
-
784
- ```bash
785
- curl --fail-with-body --max-time 20 "$MANAGEMENT/runtime-credential/provision" \
786
- -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Content-Type: application/json' \
787
- -H "Idempotency-Key: $IDEMPOTENCY_KEY" --data '{"provider_id":"example-ai"}'
788
- curl --fail-with-body --max-time 20 "$MANAGEMENT/runtime-credential/resolve" \
789
- -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Content-Type: application/json' \
790
- --data '{"provider_id":"example-ai","api_key_id":"key-123"}'
791
- curl --fail-with-body --max-time 20 "$MANAGEMENT/runtime-credential/renew" \
792
- -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Content-Type: application/json' \
793
- -H "Idempotency-Key: $RENEW_IDEMPOTENCY_KEY" --data '{"provider_id":"example-ai","api_key_id":"key-123"}'
794
- curl --fail-with-body --max-time 20 -H "Authorization: Bearer $RUNTIME_KEY" "$MODEL_BASE/models"
795
- curl --no-buffer --fail-with-body "$MODEL_BASE/chat/completions" \
796
- -H "Authorization: Bearer $RUNTIME_KEY" -H 'Content-Type: application/json' \
797
- --data '{"model":"example-model","messages":[{"role":"user","content":"Reply with OK"}],"stream":true}'
798
- curl --fail-with-body --max-time 20 "$TOKEN_ENDPOINT" \
799
- --data-urlencode 'grant_type=refresh_token' --data-urlencode "client_id=$CLIENT_ID" --data-urlencode "refresh_token=$REFRESH_TOKEN"
800
- curl --fail-with-body --max-time 10 "$REVOCATION_ENDPOINT" \
801
- --data-urlencode "client_id=$CLIENT_ID" --data-urlencode "token=$REFRESH_TOKEN"
802
- ```
803
-
804
- Skip revocation when Discovery omits the endpoint. The OIDC paths shown are examples, while management suffixes, `/models` and `/chat/completions` are fixed. A code from another browser attempt cannot be exchanged without that attempt's verifier. Validate every ID Token requirement in section 4; base64 decoding alone proves nothing.
805
-
806
- ## 11. Optional extension proposals — not enabled or called by this release
807
-
808
- ### 11.1 Remote profile discovery
5
+ The client consumes two explicit gateway contracts. Discovery declarations must match endpoint behavior. It does not infer token semantics from branding or URLs or fall back to legacy model-key provisioning.
809
6
 
810
- A future `GET /.well-known/eduwork-configuration` could declare `protocol_version: "eduwork-discovery/v1alpha1"`, an OIDC issuer and resource capabilities. The current client reads its trusted local configuration and does not request this endpoint. Any later import must require explicit trust before adding origins, issuer or permissions; a discovery response must never install code, plugins or arbitrary commands. The existing OIDC Discovery endpoint remains implemented and required for identity; it is a different mechanism.
7
+ - [LiteLLM native contract 1](gateway-auth/README_EN.md): dynamic registration, Code + PKCE, token-authorized models, refresh and revocation.
8
+ - [Experimental oidc-llm 0.1](gateway-auth/experimental-oidc-llm.en.md): explicit registration, identity mode, model resource, API base and scopes; still a draft.
9
+ - [Standard identity-only OIDC](oidc-interoperability.en.md): identity profiles do not provide model resources.
811
10
 
812
- ### 11.2 Institution activity heartbeat
11
+ ## OIDC requirements
813
12
 
814
- Heartbeat is outside this public protocol. Institution plugins own its private wire and explicit enablement; it is never a login, model, or token-refresh requirement. The public package does not schedule telemetry.
13
+ Provide matching issuer, authorization, token, JWKS and UserInfo endpoints with public-client S256 PKCE. The client checks state, ID Token signature/claims and UserInfo subject binding. When discovery advertises authorization response iss support, the callback must include the matching issuer.
815
14
 
816
- ### 11.3 Other separate future capabilities
15
+ Catalogs and inference accept the same authorized Access Token. Validated discovery determines API/resource boundaries; the server enforces user permissions, model access and quota. Catalogs use data[].id and inference reuses OpenAI-compatible JSON/SSE.
817
16
 
818
- RP-initiated/global SSO logout, server model-key revocation, dynamic OIDC client registration and remotely advertised advanced model capabilities are not implemented by this release. Omission of these capabilities must not break basic identity or manually configured personal model access.
17
+ Token responses provide expiry and refresh credentials. Refresh cannot change the authorized subject or context. Immediate server-side Access Token invalidation on logout is not required; the client clears local access and attempts Refresh Token revocation.
819
18
 
820
- ## 12. Versioning and primary references
19
+ ## Quota and compatibility
821
20
 
822
- Profile schema (`v1alpha1`), resource wire (`v1`), npm package version and DSH runtime version are independent. The accepted Bootstrap aliases `worker.user-center.v1`, `worker-user-center/v1`, and `eduwork-resources/v1` describe the same deployed wire. Missing version is accepted for existing v1 servers; unknown explicit versions fail with a resource-connection error and do not invalidate an already verified identity. New servers SHOULD return an explicit supported version. Additive optional fields may be ignored; removing/renaming fields or changing credentials/scopes requires a new major wire version and reviewed client support. Institution quotas are not part of the public schema.
21
+ The public package defines no unified quota, team-management or Key Binding protocol. [Account extensions](account-extensions.en.md) can reuse authorized Host GET transport and existing quota parsers.
823
22
 
824
- Primary standards: [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html), [OAuth for Native Apps / RFC 8252](https://www.rfc-editor.org/rfc/rfc8252), [OAuth Security Best Current Practice / RFC 9700](https://www.rfc-editor.org/rfc/rfc9700). This resource protocol complements OIDC; it does not redefine identity validation or infer a model credential from an ID Token.
23
+ Servers may retain Key Binding endpoints for old clients. New clients follow the [migration guide](key-binding-protocol.en.md) and never request model keys. The [catalog OpenAPI](../protocol/resources.openapi.yaml) describes resource operations, not the gateway authentication contract.