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