@cloudflare/workers-oauth-provider 0.5.0 → 0.7.0
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/README.md +87 -0
- package/dist/oauth-provider.d.ts +420 -3
- package/dist/oauth-provider.js +1220 -84
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -117,6 +117,10 @@ export default new OAuthProvider({
|
|
|
117
117
|
// Defaults to false.
|
|
118
118
|
allowTokenExchangeGrant: false,
|
|
119
119
|
|
|
120
|
+
// Optional: Experimental MCP Enterprise-Managed Authorization support.
|
|
121
|
+
// When enabled, the token endpoint accepts ID-JAG JWTs with the JWT bearer grant.
|
|
122
|
+
enterpriseManagedAuthorization: undefined,
|
|
123
|
+
|
|
120
124
|
// Optional: Explicitly enable Client ID Metadata Document (CIMD) support.
|
|
121
125
|
// When true, URL-formatted client_ids will be fetched as metadata documents.
|
|
122
126
|
// Requires the 'global_fetch_strictly_public' compatibility flag.
|
|
@@ -306,6 +310,88 @@ The `accessTokenTTL` override is particularly useful when the application is als
|
|
|
306
310
|
|
|
307
311
|
The `props` values are end-to-end encrypted, so they can safely contain sensitive information.
|
|
308
312
|
|
|
313
|
+
### Reporting errors from the callback
|
|
314
|
+
|
|
315
|
+
Throw `OAuthError` from `tokenExchangeCallback` to return a structured OAuth `/token` error (`{ error, error_description }`) instead of a generic 500:
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
import { OAuthError, OAuthProvider } from '@cloudflare/workers-oauth-provider';
|
|
319
|
+
|
|
320
|
+
new OAuthProvider({
|
|
321
|
+
// …
|
|
322
|
+
tokenExchangeCallback: async (options) => {
|
|
323
|
+
if (options.grantType === 'refresh_token') {
|
|
324
|
+
return { newProps: await refreshUpstream(options.props) };
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
async function refreshUpstream(props) {
|
|
330
|
+
const res = await fetch(/* upstream token endpoint */);
|
|
331
|
+
|
|
332
|
+
if (res.status === 401) {
|
|
333
|
+
throw new OAuthError('invalid_grant', {
|
|
334
|
+
description: 'upstream refresh token is invalid',
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (res.status === 429) {
|
|
339
|
+
throw new OAuthError('temporarily_unavailable', {
|
|
340
|
+
description: 'upstream rate limited',
|
|
341
|
+
statusCode: 429,
|
|
342
|
+
headers: { 'Retry-After': res.headers.get('retry-after') ?? '60' },
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return await res.json();
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`OAuthError(code, options)` takes:
|
|
351
|
+
|
|
352
|
+
- `code` — OAuth error code returned in the `error` field. This may be a standard code (`OAuthTokenErrorCode`) or an application-defined string.
|
|
353
|
+
- `options.description` — human-readable text returned in `error_description`.
|
|
354
|
+
- `options.statusCode` — HTTP status code (default `400`).
|
|
355
|
+
- `options.headers` — additional response headers, such as `Retry-After` for transient failures. There is no implicit `Retry-After` default for callback-thrown errors.
|
|
356
|
+
|
|
357
|
+
Only `OAuthError` from this package is converted into a structured `/token` response. Plain errors, plain objects with a `code` field, and app-local error classes continue to surface as 500s so unexpected failures stay visible. Import `OAuthError` from `@cloudflare/workers-oauth-provider` rather than copying or re-implementing it.
|
|
358
|
+
|
|
359
|
+
## Enterprise-Managed Authorization (Experimental)
|
|
360
|
+
|
|
361
|
+
Accepts ID-JAG assertions at `/token` per the [MCP Enterprise-Managed Authorization extension](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization). The enterprise IdP issues an ID-JAG JWT and the MCP client exchanges it here for an opaque access token.
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
new OAuthProvider({
|
|
365
|
+
// ... other options ...
|
|
366
|
+
resourceMetadata: { resource: 'https://mcp.example.com/mcp' },
|
|
367
|
+
enterpriseManagedAuthorization: {
|
|
368
|
+
trustedIssuers: async ({ iss }) =>
|
|
369
|
+
iss === 'https://idp.example.com'
|
|
370
|
+
? { issuer: iss, jwksUri: 'https://idp.example.com/.well-known/jwks.json', algorithms: ['RS256'] }
|
|
371
|
+
: null,
|
|
372
|
+
async mapClaims({ claims, requestedScope }) {
|
|
373
|
+
return {
|
|
374
|
+
// Opaque tokens use ':' as a separator — encode subjects that may contain it.
|
|
375
|
+
userId: `enterprise-${claims.sub}`,
|
|
376
|
+
scope: requestedScope,
|
|
377
|
+
metadata: { enterpriseIssuer: claims.iss, enterpriseSubject: claims.sub },
|
|
378
|
+
props: { enterprise: true, subject: claims.sub, email: claims.email },
|
|
379
|
+
};
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Setup:
|
|
386
|
+
|
|
387
|
+
1. Configure your IdP as an ID-JAG issuer with this worker's origin as the resource and a public JWKS endpoint.
|
|
388
|
+
2. Set `resourceMetadata.resource` to the MCP endpoint URL (required when EMA is enabled).
|
|
389
|
+
3. Implement `trustedIssuers` as a resolver — for multi-tenant deployments it can read `env` / `clientInfo` to look up per-tenant IdP config without redeploying.
|
|
390
|
+
|
|
391
|
+
The AS enforces `resolved.issuer === iss` (confused-deputy guard) and validates ID-JAG `typ`, signature, audience, client binding, resource, `exp` / `iat` / `nbf`, max lifetime, and `jti` replay. Refresh tokens are not issued for this grant — the ID-JAG itself is the renewable assertion.
|
|
392
|
+
|
|
393
|
+
Experimental — the MCP extension is still a draft.
|
|
394
|
+
|
|
309
395
|
## Custom Error Responses
|
|
310
396
|
|
|
311
397
|
By using the `onError` option, you can emit notifications or take other actions when an error response was to be emitted:
|
|
@@ -388,6 +474,7 @@ This library implements the following OAuth and MCP specifications:
|
|
|
388
474
|
- [OAuth 2.0 Protected Resource Metadata (RFC 9728)](https://datatracker.ietf.org/doc/html/rfc9728) — `/.well-known/oauth-protected-resource` discovery endpoint
|
|
389
475
|
- [OAuth 2.0 Dynamic Client Registration (RFC 7591)](https://datatracker.ietf.org/doc/html/rfc7591) — Dynamic client registration endpoint
|
|
390
476
|
- [OAuth Client ID Metadata Documents (draft-ietf-oauth-client-id-metadata-document)](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document) — HTTPS URLs as client IDs
|
|
477
|
+
- [MCP Enterprise-Managed Authorization extension](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization) — experimental ID-JAG JWT bearer grant support
|
|
391
478
|
|
|
392
479
|
These are the specifications required by the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization).
|
|
393
480
|
|
package/dist/oauth-provider.d.ts
CHANGED
|
@@ -1,7 +1,277 @@
|
|
|
1
1
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
2
2
|
|
|
3
|
-
//#region src/
|
|
3
|
+
//#region src/ema/result.d.ts
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Tagged union of every validation failure that can occur on the EMA token
|
|
7
|
+
* endpoint path. Extend by adding a new arm; the exhaustive `switch` in
|
|
8
|
+
* `emaErrorToWire` will surface unhandled cases at compile time.
|
|
9
|
+
*/
|
|
10
|
+
type EmaValidationError = {
|
|
11
|
+
reason: 'assertion_missing';
|
|
12
|
+
} | {
|
|
13
|
+
reason: 'assertion_too_large';
|
|
14
|
+
size: number;
|
|
15
|
+
max: number;
|
|
16
|
+
} | {
|
|
17
|
+
reason: 'assertion_malformed';
|
|
18
|
+
} | {
|
|
19
|
+
reason: 'invalid_typ';
|
|
20
|
+
got?: unknown;
|
|
21
|
+
} | {
|
|
22
|
+
reason: 'invalid_alg';
|
|
23
|
+
got?: unknown;
|
|
24
|
+
} | {
|
|
25
|
+
reason: 'issuer_not_trusted';
|
|
26
|
+
iss: string;
|
|
27
|
+
} | {
|
|
28
|
+
reason: 'no_matching_key';
|
|
29
|
+
kid?: string;
|
|
30
|
+
} | {
|
|
31
|
+
reason: 'signature_failed';
|
|
32
|
+
} | {
|
|
33
|
+
reason: 'jwks_fetch_failed';
|
|
34
|
+
status?: number;
|
|
35
|
+
} | {
|
|
36
|
+
reason: 'invalid_claim';
|
|
37
|
+
claim: string;
|
|
38
|
+
} | {
|
|
39
|
+
reason: 'aud_mismatch';
|
|
40
|
+
expected: string;
|
|
41
|
+
got: string | string[];
|
|
42
|
+
} | {
|
|
43
|
+
reason: 'expired';
|
|
44
|
+
exp: number;
|
|
45
|
+
now: number;
|
|
46
|
+
} | {
|
|
47
|
+
reason: 'iat_in_future';
|
|
48
|
+
iat: number;
|
|
49
|
+
now: number;
|
|
50
|
+
skew: number;
|
|
51
|
+
} | {
|
|
52
|
+
reason: 'nbf_in_future';
|
|
53
|
+
nbf: number;
|
|
54
|
+
now: number;
|
|
55
|
+
skew: number;
|
|
56
|
+
} | {
|
|
57
|
+
reason: 'lifetime_too_long';
|
|
58
|
+
lifetime: number;
|
|
59
|
+
max: number;
|
|
60
|
+
} | {
|
|
61
|
+
reason: 'replayed';
|
|
62
|
+
jti: string;
|
|
63
|
+
} | {
|
|
64
|
+
reason: 'client_id_mismatch';
|
|
65
|
+
expected: string;
|
|
66
|
+
got: string;
|
|
67
|
+
} | {
|
|
68
|
+
reason: 'resource_invalid';
|
|
69
|
+
resource: string;
|
|
70
|
+
} | {
|
|
71
|
+
reason: 'resource_mismatch';
|
|
72
|
+
expected: string;
|
|
73
|
+
got: string;
|
|
74
|
+
} | {
|
|
75
|
+
reason: 'invalid_scope_param';
|
|
76
|
+
} | {
|
|
77
|
+
reason: 'invalid_mapped_user';
|
|
78
|
+
} | {
|
|
79
|
+
reason: 'invalid_mapped_scope';
|
|
80
|
+
} | {
|
|
81
|
+
reason: 'invalid_mapped_props';
|
|
82
|
+
} | {
|
|
83
|
+
reason: 'invalid_mapped_ttl';
|
|
84
|
+
} | {
|
|
85
|
+
reason: 'mapper_denied';
|
|
86
|
+
} | {
|
|
87
|
+
reason: 'mapper_threw';
|
|
88
|
+
} | {
|
|
89
|
+
reason: 'assertion_expired_after_processing';
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/ema/types.d.ts
|
|
93
|
+
/**
|
|
94
|
+
* Claims expected in an MCP Enterprise-Managed Authorization ID-JAG assertion.
|
|
95
|
+
* Additional issuer-specific claims (e.g. `email`) are preserved verbatim
|
|
96
|
+
* under the index signature.
|
|
97
|
+
*/
|
|
98
|
+
interface EmaIdJagClaims {
|
|
99
|
+
/** Identity provider issuer URL. */
|
|
100
|
+
iss: string;
|
|
101
|
+
/** Enterprise subject identifier for the resource owner. */
|
|
102
|
+
sub: string;
|
|
103
|
+
/** Authorization server issuer URL or URLs for which this assertion is intended. */
|
|
104
|
+
aud: string | string[];
|
|
105
|
+
/** RFC 9728 resource identifier of the MCP server. */
|
|
106
|
+
resource: string;
|
|
107
|
+
/** OAuth client identifier this assertion was issued to. */
|
|
108
|
+
client_id: string;
|
|
109
|
+
/** Unique assertion identifier used for replay protection. */
|
|
110
|
+
jti: string;
|
|
111
|
+
/** Assertion expiration time as a Unix timestamp in seconds. */
|
|
112
|
+
exp: number;
|
|
113
|
+
/** Assertion issued-at time as a Unix timestamp in seconds. */
|
|
114
|
+
iat: number;
|
|
115
|
+
/** Optional space-separated OAuth scope string. */
|
|
116
|
+
scope?: string;
|
|
117
|
+
/** Optional email claim supplied by the enterprise IdP. */
|
|
118
|
+
email?: string;
|
|
119
|
+
/** Additional enterprise IdP claims. */
|
|
120
|
+
[claim: string]: unknown;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Trusted enterprise IdP configuration for ID-JAG validation.
|
|
124
|
+
*/
|
|
125
|
+
interface EmaTrustedIssuer {
|
|
126
|
+
/** Issuer URL that must exactly match the assertion `iss` claim. */
|
|
127
|
+
issuer: string;
|
|
128
|
+
/** HTTPS JWKS endpoint used to validate assertion signatures. */
|
|
129
|
+
jwksUri: string;
|
|
130
|
+
/** Allowed JWT signing algorithms for this issuer. Defaults to `['RS256']`. */
|
|
131
|
+
algorithms?: string[];
|
|
132
|
+
/** Expected authorization server audience. Defaults to this provider's issuer URL. */
|
|
133
|
+
audience?: string;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Input passed to `enterpriseManagedAuthorization.mapClaims` after ID-JAG validation.
|
|
137
|
+
*/
|
|
138
|
+
interface EmaClaimsMapperInput<Env = Cloudflare.Env> {
|
|
139
|
+
/** Validated ID-JAG claims. */
|
|
140
|
+
claims: EmaIdJagClaims;
|
|
141
|
+
/** Authenticated OAuth client that presented the assertion. */
|
|
142
|
+
clientInfo: ClientInfo;
|
|
143
|
+
/** Validated MCP resource identifier from the assertion. */
|
|
144
|
+
resource: string;
|
|
145
|
+
/** Requested scopes after downscoping to the assertion's scope claim, if present. */
|
|
146
|
+
requestedScope: string[];
|
|
147
|
+
/** The original HTTP token request, e.g. for inspecting Host header in multi-tenant routing. */
|
|
148
|
+
request: Request;
|
|
149
|
+
/** Cloudflare Worker environment variables. */
|
|
150
|
+
env: Env;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Result returned by `enterpriseManagedAuthorization.mapClaims`.
|
|
154
|
+
*/
|
|
155
|
+
interface EmaClaimsMapperResult {
|
|
156
|
+
/**
|
|
157
|
+
* User ID to associate with the issued grant and access token.
|
|
158
|
+
*
|
|
159
|
+
* Must not contain `:` — the opaque access token format issued by this
|
|
160
|
+
* provider uses `:` as an internal separator, so a `userId` containing
|
|
161
|
+
* it will produce tokens that fail to parse on validation. If the IdP
|
|
162
|
+
* subject may contain `:` (e.g. an email), encode or hash it first
|
|
163
|
+
* (e.g. ``userId: `enterprise-${encodeURIComponent(claims.sub)}` ``).
|
|
164
|
+
*/
|
|
165
|
+
userId: string;
|
|
166
|
+
/** Scopes to grant to the issued access token. */
|
|
167
|
+
scope: string[];
|
|
168
|
+
/**
|
|
169
|
+
* Optional grant metadata used for audit and grant listing. This is not
|
|
170
|
+
* encrypted. Stored on the grant in KV alongside the user ID — visible to
|
|
171
|
+
* server-side code via `OAuthHelpers` but never exposed to the MCP client.
|
|
172
|
+
* Use for audit logs, admin UIs, or "list active sessions" features.
|
|
173
|
+
*/
|
|
174
|
+
metadata?: unknown;
|
|
175
|
+
/**
|
|
176
|
+
* Application props encrypted into the issued access token and exposed to
|
|
177
|
+
* API handlers on every authenticated request (via `getMcpAuthContext()`
|
|
178
|
+
* for MCP servers, or `ctx.props` in the underlying OAuth helpers). Use
|
|
179
|
+
* for per-request data the protected resource needs without hitting the
|
|
180
|
+
* IdP again — e.g. `enterprise: true`, subject, email, role claims.
|
|
181
|
+
*/
|
|
182
|
+
props: unknown;
|
|
183
|
+
/**
|
|
184
|
+
* Optional access token TTL override in seconds. Overrides the AS's
|
|
185
|
+
* configured default. Not clamped to the assertion lifetime — the ID-JAG
|
|
186
|
+
* `exp` governs how long the assertion remains a usable grant, not the
|
|
187
|
+
* lifetime of the access token it mints (RFC 7523 §3).
|
|
188
|
+
*/
|
|
189
|
+
accessTokenTTL?: number;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Maps validated enterprise ID-JAG claims to this provider's local user, scopes,
|
|
193
|
+
* metadata, and props. Return `null` to deny token issuance.
|
|
194
|
+
*
|
|
195
|
+
* Always async — mirrors `EmaTrustedIssuerResolver` so the two enterprise
|
|
196
|
+
* callbacks have the same shape and downstream lookups (KV, D1, IdP federation)
|
|
197
|
+
* don't require a later API change to add `Promise<…>` return support.
|
|
198
|
+
*/
|
|
199
|
+
type EmaClaimsMapper<Env = Cloudflare.Env> = (input: EmaClaimsMapperInput<Env>) => Promise<EmaClaimsMapperResult | null>;
|
|
200
|
+
/**
|
|
201
|
+
* Input passed to a dynamic `EmaTrustedIssuerResolver`.
|
|
202
|
+
*
|
|
203
|
+
* The `iss` value comes from the assertion's unverified payload — it is
|
|
204
|
+
* used here only as a routing key to choose which IdP's JWKS to load.
|
|
205
|
+
* The cryptographic trust comes from signature verification later, so
|
|
206
|
+
* the resolver may safely treat `iss` as untrusted input.
|
|
207
|
+
*/
|
|
208
|
+
interface EmaTrustedIssuerResolverInput<Env = Cloudflare.Env> {
|
|
209
|
+
/** Issuer URL from the assertion's `iss` claim (unverified). */
|
|
210
|
+
iss: string;
|
|
211
|
+
/** Cloudflare Worker environment variables (bindings, secrets, KV, D1). */
|
|
212
|
+
env: Env;
|
|
213
|
+
/** The original HTTP request, e.g. for inspecting the Host header in multi-tenant routing. */
|
|
214
|
+
request: Request;
|
|
215
|
+
/** Authenticated OAuth client that presented the assertion. */
|
|
216
|
+
clientInfo: ClientInfo;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Dynamic resolver for `EmaOptions.trustedIssuers`.
|
|
220
|
+
*
|
|
221
|
+
* Returns the trusted issuer configuration for an incoming `iss` claim,
|
|
222
|
+
* or `null` if the issuer is not trusted for this client / tenant. The
|
|
223
|
+
* returned `issuer` field must equal the input `iss` — the AS enforces
|
|
224
|
+
* this to prevent a resolver from being tricked into returning a config
|
|
225
|
+
* for a different IdP than the one the assertion claims to be from.
|
|
226
|
+
*
|
|
227
|
+
* Useful for B2B platforms where new tenants onboard their own IdPs
|
|
228
|
+
* dynamically and the AS cannot ship a static list at deploy time.
|
|
229
|
+
*
|
|
230
|
+
* **SSRF warning**: `jwksUri` is fetched outbound by the AS. If your
|
|
231
|
+
* resolver reads `jwksUri` from tenant-controlled storage (self-service
|
|
232
|
+
* tenant onboarding, etc.), an attacker who controls a tenant config
|
|
233
|
+
* can point the AS at arbitrary HTTPS endpoints — internal services,
|
|
234
|
+
* cloud metadata endpoints, victim hosts for DoS amplification. The
|
|
235
|
+
* library only enforces `https:`; deployers must validate `jwksUri`
|
|
236
|
+
* against their own allowlist (e.g. registered IdP vendor domains)
|
|
237
|
+
* before storing or returning it.
|
|
238
|
+
*/
|
|
239
|
+
type EmaTrustedIssuerResolver<Env = Cloudflare.Env> = (input: EmaTrustedIssuerResolverInput<Env>) => Promise<EmaTrustedIssuer | null>;
|
|
240
|
+
/**
|
|
241
|
+
* MCP Enterprise-Managed Authorization configuration.
|
|
242
|
+
*
|
|
243
|
+
* Presence of this option on `OAuthProviderOptions` enables the EMA grant.
|
|
244
|
+
* There is intentionally no `enabled` flag — forgetting to set it would
|
|
245
|
+
* silently disable the feature despite full configuration.
|
|
246
|
+
*/
|
|
247
|
+
interface EmaOptions<Env = Cloudflare.Env> {
|
|
248
|
+
/**
|
|
249
|
+
* Resolver that returns the trusted issuer configuration for an
|
|
250
|
+
* incoming `iss` claim, or `null` to reject the assertion.
|
|
251
|
+
*
|
|
252
|
+
* Always a function — for a fixed list of IdPs, write a one-line closure:
|
|
253
|
+
*
|
|
254
|
+
* ```ts
|
|
255
|
+
* const issuers = [{ issuer: 'https://idp.example.com', jwksUri: '...' }];
|
|
256
|
+
* trustedIssuers: async ({ iss }) => issuers.find((i) => i.issuer === iss) ?? null,
|
|
257
|
+
* ```
|
|
258
|
+
*
|
|
259
|
+
* For B2B / multi-tenant deployments, the resolver can consult `env`,
|
|
260
|
+
* `request`, or `clientInfo` to look up the issuer dynamically (e.g.
|
|
261
|
+
* a per-tenant config in KV / D1) without redeploying.
|
|
262
|
+
*/
|
|
263
|
+
trustedIssuers: EmaTrustedIssuerResolver<Env>;
|
|
264
|
+
/** Maps validated enterprise claims to local token data. */
|
|
265
|
+
mapClaims: EmaClaimsMapper<Env>;
|
|
266
|
+
/** JWKS cache TTL in seconds. Defaults to 300 seconds. */
|
|
267
|
+
jwksCacheTtlSeconds?: number;
|
|
268
|
+
/** Allowed clock skew for `exp` and `iat` checks in seconds. Defaults to 60 seconds. */
|
|
269
|
+
clockSkewSeconds?: number;
|
|
270
|
+
/** Maximum accepted assertion lifetime in seconds. Defaults to 300 seconds. */
|
|
271
|
+
maxAssertionLifetimeSeconds?: number;
|
|
272
|
+
}
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/oauth-provider.d.ts
|
|
5
275
|
/**
|
|
6
276
|
* Enum representing OAuth grant types
|
|
7
277
|
*/
|
|
@@ -9,6 +279,7 @@ declare enum GrantType {
|
|
|
9
279
|
AUTHORIZATION_CODE = "authorization_code",
|
|
10
280
|
REFRESH_TOKEN = "refresh_token",
|
|
11
281
|
TOKEN_EXCHANGE = "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
282
|
+
JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
12
283
|
}
|
|
13
284
|
/**
|
|
14
285
|
* Aliases for either type of Handler that makes .fetch required
|
|
@@ -20,6 +291,16 @@ type WorkerEntrypointWithFetch<Env = Cloudflare.Env> = WorkerEntrypoint<Env> & {
|
|
|
20
291
|
/**
|
|
21
292
|
* Configuration options for the OAuth Provider
|
|
22
293
|
*/
|
|
294
|
+
/**
|
|
295
|
+
* Registered OAuth 2.0 error codes that the `/token` endpoint may return.
|
|
296
|
+
*
|
|
297
|
+
* Union of:
|
|
298
|
+
* - RFC 6749 §5.2 (token endpoint)
|
|
299
|
+
* - RFC 6750 §3.1 (bearer / resource server) — included so callbacks
|
|
300
|
+
* doing audience validation can use them
|
|
301
|
+
* - RFC 8693 §2.2.2 (token exchange)
|
|
302
|
+
*/
|
|
303
|
+
type OAuthTokenErrorCode = 'invalid_request' | 'invalid_client' | 'invalid_grant' | 'unauthorized_client' | 'unsupported_grant_type' | 'invalid_scope' | 'invalid_token' | 'insufficient_scope' | 'invalid_target' | 'server_error' | 'temporarily_unavailable';
|
|
23
304
|
/**
|
|
24
305
|
* Result of a token exchange callback function.
|
|
25
306
|
* Allows updating the props stored in both the access token and the grant.
|
|
@@ -74,6 +355,14 @@ interface TokenExchangeCallbackOptions {
|
|
|
74
355
|
* User who authorized this grant
|
|
75
356
|
*/
|
|
76
357
|
userId: string;
|
|
358
|
+
/**
|
|
359
|
+
* Identifier of the grant record this callback is operating on. Stable across
|
|
360
|
+
* refreshes for the lifetime of the grant. Pass this together with `userId`
|
|
361
|
+
* to {@link OAuthHelpers.revokeGrant} when the callback decides the grant
|
|
362
|
+
* should be torn down (for example, after an upstream refresh fails with a
|
|
363
|
+
* terminal error code).
|
|
364
|
+
*/
|
|
365
|
+
grantId: string;
|
|
77
366
|
/**
|
|
78
367
|
* List of scopes that were granted
|
|
79
368
|
*/
|
|
@@ -217,6 +506,15 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
|
|
|
217
506
|
* Defaults to false.
|
|
218
507
|
*/
|
|
219
508
|
allowTokenExchangeGrant?: boolean;
|
|
509
|
+
/**
|
|
510
|
+
* Experimental support for the MCP Enterprise-Managed Authorization extension.
|
|
511
|
+
* When enabled, the token endpoint accepts ID-JAG assertions using the JWT bearer
|
|
512
|
+
* grant type (`urn:ietf:params:oauth:grant-type:jwt-bearer`).
|
|
513
|
+
*
|
|
514
|
+
* This feature is opt-in because the MCP extension and underlying OAuth drafts are
|
|
515
|
+
* still evolving. Trusted issuers and a claim mapper are required.
|
|
516
|
+
*/
|
|
517
|
+
enterpriseManagedAuthorization?: EmaOptions<Env>;
|
|
220
518
|
/**
|
|
221
519
|
* Controls whether public clients (clients without a secret, like SPAs) can register via the
|
|
222
520
|
* dynamic client registration endpoint. When true, only confidential clients can register.
|
|
@@ -245,16 +543,26 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
|
|
|
245
543
|
*/
|
|
246
544
|
resolveExternalToken?: (input: ResolveExternalTokenInput) => Promise<ResolveExternalTokenResult | null>;
|
|
247
545
|
/**
|
|
248
|
-
* Optional callback function that is called whenever the OAuthProvider returns an error response
|
|
546
|
+
* Optional callback function that is called whenever the OAuthProvider returns an error response.
|
|
249
547
|
* This allows the client to emit notifications or perform other actions when an error occurs.
|
|
250
548
|
*
|
|
251
549
|
* If the function returns a Response, that will be used in place of the OAuthProvider's default one.
|
|
550
|
+
*
|
|
551
|
+
* `internal` (when present) carries a tagged, server-side-only reason that the library
|
|
552
|
+
* deliberately did NOT put on the wire — used for richer diagnostics where the public
|
|
553
|
+
* response must stay generic (e.g. JWT validation failures on the EMA path). Backwards
|
|
554
|
+
* compatible: existing callbacks ignoring this field continue to work unchanged.
|
|
252
555
|
*/
|
|
253
556
|
onError?: (error: {
|
|
254
557
|
code: string;
|
|
255
558
|
description: string;
|
|
256
559
|
status: number;
|
|
257
560
|
headers: Record<string, string>;
|
|
561
|
+
internal?: {
|
|
562
|
+
category: string;
|
|
563
|
+
reason: string;
|
|
564
|
+
detail?: unknown;
|
|
565
|
+
};
|
|
258
566
|
}) => Response | void;
|
|
259
567
|
/**
|
|
260
568
|
* Explicitly enable Client ID Metadata Document (CIMD) support.
|
|
@@ -889,5 +1197,114 @@ declare class OAuthProvider<Env = Cloudflare.Env> {
|
|
|
889
1197
|
* @returns An instance of OAuthHelpers
|
|
890
1198
|
*/
|
|
891
1199
|
declare function getOAuthApi<Env = Cloudflare.Env>(options: OAuthProviderOptions<Env>, env: Env): OAuthHelpers;
|
|
1200
|
+
/**
|
|
1201
|
+
* Error class for OAuth operations
|
|
1202
|
+
* Carries OAuth error code and description for proper error responses
|
|
1203
|
+
*/
|
|
1204
|
+
/**
|
|
1205
|
+
* Options accepted by the {@link OAuthError} constructor.
|
|
1206
|
+
*/
|
|
1207
|
+
interface OAuthErrorOptions {
|
|
1208
|
+
/**
|
|
1209
|
+
* Human-readable text returned in the `error_description` field.
|
|
1210
|
+
*/
|
|
1211
|
+
description: string;
|
|
1212
|
+
/**
|
|
1213
|
+
* HTTP status code for the error response. Defaults to `400`.
|
|
1214
|
+
*/
|
|
1215
|
+
statusCode?: number;
|
|
1216
|
+
/**
|
|
1217
|
+
* Additional response headers.
|
|
1218
|
+
*
|
|
1219
|
+
* For transient failures (e.g. upstream rate limits), set
|
|
1220
|
+
* `Retry-After` here so well-behaved clients back off instead of
|
|
1221
|
+
* retry-storming. Per RFC 7231 §7.1.3 the value may be either a
|
|
1222
|
+
* number of seconds or an HTTP-date.
|
|
1223
|
+
*/
|
|
1224
|
+
headers?: Record<string, string>;
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Structured OAuth 2.0 error.
|
|
1228
|
+
*
|
|
1229
|
+
* Throw from a `tokenExchangeCallback` (or any code it calls — the error
|
|
1230
|
+
* propagates naturally up through deep call stacks) to surface a standard
|
|
1231
|
+
* `/token` error response (`{ error, error_description }`) instead of a
|
|
1232
|
+
* generic `500 Internal Server Error`.
|
|
1233
|
+
*
|
|
1234
|
+
* Anything thrown that is **not** an `OAuthError` continues to surface as
|
|
1235
|
+
* a 500 so unexpected failures remain visible — the provider does not
|
|
1236
|
+
* catch-everything-and-return-400.
|
|
1237
|
+
*
|
|
1238
|
+
* @example
|
|
1239
|
+
* ```ts
|
|
1240
|
+
* import { OAuthError } from '@cloudflare/workers-oauth-provider';
|
|
1241
|
+
*
|
|
1242
|
+
* tokenExchangeCallback: async (options) => {
|
|
1243
|
+
* if (options.grantType === 'refresh_token') {
|
|
1244
|
+
* // refreshUpstream() may throw OAuthError from any depth
|
|
1245
|
+
* return { newProps: await refreshUpstream(options.props) };
|
|
1246
|
+
* }
|
|
1247
|
+
* }
|
|
1248
|
+
*
|
|
1249
|
+
* async function refreshUpstream(props) {
|
|
1250
|
+
* const res = await fetch(...);
|
|
1251
|
+
* if (res.status === 401) {
|
|
1252
|
+
* throw new OAuthError('invalid_grant', { description: 'upstream refresh token is invalid' });
|
|
1253
|
+
* }
|
|
1254
|
+
* if (res.status === 429) {
|
|
1255
|
+
* // Mirror upstream's Retry-After if present, otherwise pick a default.
|
|
1256
|
+
* throw new OAuthError('temporarily_unavailable', {
|
|
1257
|
+
* description: 'upstream rate limited',
|
|
1258
|
+
* statusCode: 429,
|
|
1259
|
+
* headers: { 'Retry-After': res.headers.get('retry-after') ?? '60' },
|
|
1260
|
+
* });
|
|
1261
|
+
* }
|
|
1262
|
+
* return await res.json();
|
|
1263
|
+
* }
|
|
1264
|
+
* ```
|
|
1265
|
+
*/
|
|
1266
|
+
declare class OAuthError extends Error {
|
|
1267
|
+
/** OAuth 2.0 error code. */
|
|
1268
|
+
readonly code: string;
|
|
1269
|
+
/** Options controlling the OAuth error response. */
|
|
1270
|
+
readonly options: OAuthErrorOptions & {
|
|
1271
|
+
statusCode: number;
|
|
1272
|
+
};
|
|
1273
|
+
/** Human-readable description sent in the `error_description` field. */
|
|
1274
|
+
readonly description: string;
|
|
1275
|
+
/** HTTP status code for the error response. */
|
|
1276
|
+
readonly statusCode: number;
|
|
1277
|
+
/** Additional response headers. */
|
|
1278
|
+
readonly headers?: Record<string, string>;
|
|
1279
|
+
constructor(code: string, options: OAuthErrorOptions);
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Validates a resource URI per RFC 8707 Section 2
|
|
1283
|
+
* @param uri - The URI string to validate
|
|
1284
|
+
* @returns true if valid, false otherwise
|
|
1285
|
+
*/
|
|
1286
|
+
declare function validateResourceUri(uri: string): boolean;
|
|
1287
|
+
/**
|
|
1288
|
+
* Checks if a requested resource matches a granted resource.
|
|
1289
|
+
* When originOnly is true, compares only the origin (scheme + host + port),
|
|
1290
|
+
* allowing path-aware resources to match origin-only grants.
|
|
1291
|
+
*/
|
|
1292
|
+
declare function resourceMatches(requested: string, granted: string, originOnly: boolean): boolean;
|
|
1293
|
+
/**
|
|
1294
|
+
* Decodes a base64url-encoded string to bytes.
|
|
1295
|
+
*/
|
|
1296
|
+
declare function base64UrlToBytes(base64Url: string): Uint8Array;
|
|
1297
|
+
/**
|
|
1298
|
+
* Parses a base64url-encoded JWT JSON part into an object.
|
|
1299
|
+
*/
|
|
1300
|
+
declare function parseJwtJsonPart(encoded: string): Record<string, unknown>;
|
|
1301
|
+
declare function isValidOAuthScopeToken(scopeToken: string): boolean;
|
|
1302
|
+
/**
|
|
1303
|
+
* Gets WebCrypto import and verify parameters for supported JOSE algorithms.
|
|
1304
|
+
*/
|
|
1305
|
+
declare function getJwtCryptoAlgorithms(alg: string): {
|
|
1306
|
+
importAlgorithm: Parameters<SubtleCrypto['importKey']>[2];
|
|
1307
|
+
verifyAlgorithm: Parameters<SubtleCrypto['verify']>[0];
|
|
1308
|
+
};
|
|
892
1309
|
//#endregion
|
|
893
|
-
export { AuthRequest, ClientInfo, CompleteAuthorizationOptions, ExchangeTokenOptions, Grant, GrantSummary, GrantType, ListOptions, ListResult, OAuthHelpers, OAuthProvider, OAuthProvider as default, OAuthProviderOptions, PurgeOptions, PurgeResult, ResolveExternalTokenInput, ResolveExternalTokenResult, Token, TokenBase, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, TokenSummary, getOAuthApi };
|
|
1310
|
+
export { AuthRequest, ClientInfo, CompleteAuthorizationOptions, type EmaClaimsMapper, type EmaClaimsMapperInput, type EmaClaimsMapperResult, type EmaIdJagClaims, type EmaOptions, type EmaTrustedIssuer, type EmaTrustedIssuerResolver, type EmaTrustedIssuerResolverInput, type EmaValidationError, ExchangeTokenOptions, Grant, GrantSummary, GrantType, ListOptions, ListResult, OAuthError, OAuthErrorOptions, OAuthHelpers, OAuthProvider, OAuthProvider as default, OAuthProviderOptions, OAuthTokenErrorCode, PurgeOptions, PurgeResult, ResolveExternalTokenInput, ResolveExternalTokenResult, Token, TokenBase, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, TokenSummary, base64UrlToBytes, getJwtCryptoAlgorithms, getOAuthApi, isValidOAuthScopeToken, parseJwtJsonPart, resourceMatches, validateResourceUri };
|