@cloudflare/workers-oauth-provider 0.8.3 → 0.9.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/README.md +356 -451
- package/dist/oauth-provider.d.ts +140 -29
- package/dist/oauth-provider.js +430 -149
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,565 +1,470 @@
|
|
|
1
1
|
# OAuth 2.1 Provider Framework for Cloudflare Workers
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@cloudflare/workers-oauth-provider` adds OAuth 2.1 authorization to HTTP APIs and remote MCP servers running on Cloudflare Workers.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- The library is agnostic to how you manage and authenticate users.
|
|
11
|
-
- The library is agnostic to how you build your UI. Your authorization flow can be implemented using whatever UI framework you use for everything else.
|
|
12
|
-
- The library's storage does not store any secrets, only hashes of them.
|
|
7
|
+
```sh
|
|
8
|
+
npm install @cloudflare/workers-oauth-provider
|
|
9
|
+
```
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
The Worker needs a KV namespace bound as `OAUTH_KV`:
|
|
15
12
|
|
|
16
|
-
|
|
13
|
+
```jsonc
|
|
14
|
+
{
|
|
15
|
+
"kv_namespaces": [
|
|
16
|
+
{
|
|
17
|
+
"binding": "OAUTH_KV",
|
|
18
|
+
"id": "YOUR_KV_NAMESPACE_ID",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
}
|
|
22
|
+
```
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
import { OAuthProvider } from '@cloudflare/workers-oauth-provider';
|
|
20
|
-
import { WorkerEntrypoint } from 'cloudflare:workers';
|
|
24
|
+
To enable Client ID Metadata Documents, also add Cloudflare's SSRF protection compatibility flag:
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// and then, if the token is valid, send the request to the API handler.
|
|
28
|
-
// You can provide:
|
|
29
|
-
// - A single route (string) or multiple routes (array)
|
|
30
|
-
// - Full URLs (which will match the hostname) or just paths (which will match any hostname)
|
|
31
|
-
apiRoute: [
|
|
32
|
-
'/api/', // Path only - will match any hostname
|
|
33
|
-
'https://api.example.com/', // Full URL - will check hostname
|
|
34
|
-
],
|
|
26
|
+
```jsonc
|
|
27
|
+
{
|
|
28
|
+
"compatibility_flags": ["global_fetch_strictly_public"],
|
|
29
|
+
}
|
|
30
|
+
```
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
// to this handler object's fetch method.
|
|
38
|
-
// You can provide either an object with a fetch method (ExportedHandler)
|
|
39
|
-
// or a class extending WorkerEntrypoint.
|
|
40
|
-
apiHandler: ApiHandler, // Using a WorkerEntrypoint class
|
|
41
|
-
|
|
42
|
-
// For multi-handler setups, you can use apiHandlers instead of apiRoute+apiHandler.
|
|
43
|
-
// This allows you to use different handlers for different API routes.
|
|
44
|
-
// Note: You must use either apiRoute+apiHandler (single-handler) OR apiHandlers (multi-handler), not both.
|
|
45
|
-
// Example:
|
|
46
|
-
// apiHandlers: {
|
|
47
|
-
// "/api/users/": UsersApiHandler,
|
|
48
|
-
// "/api/documents/": DocumentsApiHandler,
|
|
49
|
-
// "https://api.example.com/": ExternalApiHandler,
|
|
50
|
-
// },
|
|
51
|
-
|
|
52
|
-
// Any requests which aren't API request will be passed to the default handler instead.
|
|
53
|
-
// Again, this can be either an object or a WorkerEntrypoint.
|
|
54
|
-
defaultHandler: defaultHandler, // Using an object with a fetch method
|
|
55
|
-
|
|
56
|
-
// This specifies the URL of the OAuth authorization flow UI. This UI is NOT implemented by
|
|
57
|
-
// the OAuthProvider. It is up to the application to implement a UI here. The only reason why
|
|
58
|
-
// this URL is given to the OAuthProvider is so that it can implement the RFC-8414 metadata
|
|
59
|
-
// discovery endpoint, i.e. `.well-known/oauth-authorization-server`.
|
|
60
|
-
// Can also be specified as just a path (e.g., "/authorize").
|
|
61
|
-
authorizeEndpoint: 'https://example.com/authorize',
|
|
62
|
-
|
|
63
|
-
// This specifies the OAuth 2 token exchange endpoint. The OAuthProvider will implement this
|
|
64
|
-
// endpoint (by directly responding to requests with a matching URL).
|
|
65
|
-
// Can also be specified as just a path (e.g., "/oauth/token").
|
|
66
|
-
tokenEndpoint: 'https://example.com/oauth/token',
|
|
67
|
-
|
|
68
|
-
// This specifies the RFC-7591 dynamic client registration endpoint. This setting is optional,
|
|
69
|
-
// but if provided, the OAuthProvider will implement this endpoint to allow dynamic client
|
|
70
|
-
// registration.
|
|
71
|
-
// Can also be specified as just a path (e.g., "/oauth/register").
|
|
72
|
-
clientRegistrationEndpoint: 'https://example.com/oauth/register',
|
|
73
|
-
|
|
74
|
-
// Optional list of scopes supported by this OAuth provider.
|
|
75
|
-
// If provided, this will be included in the RFC 8414 metadata as 'scopes_supported'.
|
|
76
|
-
// If not provided, the 'scopes_supported' field will be omitted from the metadata.
|
|
77
|
-
scopesSupported: ['document.read', 'document.write', 'profile'],
|
|
78
|
-
|
|
79
|
-
// Optional: Controls whether the OAuth implicit flow is allowed.
|
|
80
|
-
// The implicit flow is discouraged in OAuth 2.1 but may be needed for some clients.
|
|
81
|
-
// Defaults to false.
|
|
82
|
-
allowImplicitFlow: false,
|
|
83
|
-
|
|
84
|
-
// Optional: Controls whether the plain PKCE code_challenge_method is allowed.
|
|
85
|
-
// OAuth 2.1 recommends using S256 exclusively as plain offers no cryptographic protection.
|
|
86
|
-
// When false, only S256 is accepted and advertised in the metadata endpoint.
|
|
87
|
-
// Defaults to true for backward compatibility.
|
|
88
|
-
allowPlainPKCE: true,
|
|
89
|
-
|
|
90
|
-
// Optional: Controls whether public clients (clients without a secret, like SPAs)
|
|
91
|
-
// can register via the dynamic client registration endpoint.
|
|
92
|
-
// When true, only confidential clients can register.
|
|
93
|
-
// Note: Creating public clients via the OAuthHelpers.createClient() method
|
|
94
|
-
// is always allowed regardless of this setting.
|
|
95
|
-
// Defaults to false.
|
|
96
|
-
disallowPublicClientRegistration: false,
|
|
97
|
-
|
|
98
|
-
// Optional: Time-to-live for refresh tokens in seconds.
|
|
99
|
-
// Defaults to 30 days (2,592,000 seconds).
|
|
100
|
-
// Set to 0 to disable refresh tokens (only access tokens will be issued).
|
|
101
|
-
// Set to `undefined` explicitly for refresh tokens that never expire.
|
|
102
|
-
refreshTokenTTL: 2592000, // 30 days (the default)
|
|
103
|
-
|
|
104
|
-
// Optional: Time-to-live for access tokens in seconds.
|
|
105
|
-
// Defaults to 1 hour (3600 seconds) if not specified.
|
|
106
|
-
accessTokenTTL: 3600,
|
|
107
|
-
|
|
108
|
-
// Optional: Time-to-live for dynamically registered clients in seconds.
|
|
109
|
-
// Defaults to 90 days (7,776,000 seconds).
|
|
110
|
-
// Clients created via OAuthHelpers.createClient() are not affected.
|
|
111
|
-
// Set to `undefined` explicitly for clients that never expire.
|
|
112
|
-
clientRegistrationTTL: 7776000, // 90 days (the default)
|
|
113
|
-
|
|
114
|
-
// Optional: Controls whether OAuth 2.0 Token Exchange (RFC 8693) is allowed.
|
|
115
|
-
// When false, the token exchange grant type will not be advertised in metadata
|
|
116
|
-
// and token exchange requests will be rejected.
|
|
117
|
-
// Defaults to false.
|
|
118
|
-
allowTokenExchangeGrant: false,
|
|
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
|
-
|
|
124
|
-
// Optional: Explicitly enable Client ID Metadata Document (CIMD) support.
|
|
125
|
-
// When true, URL-formatted client_ids will be fetched as metadata documents.
|
|
126
|
-
// Requires the 'global_fetch_strictly_public' compatibility flag.
|
|
127
|
-
// See the CIMD section below for details. Defaults to false.
|
|
128
|
-
clientIdMetadataDocumentEnabled: false,
|
|
129
|
-
});
|
|
32
|
+
See [Client registration](#client-registration) for the matching provider option.
|
|
130
33
|
|
|
131
|
-
|
|
132
|
-
// if they aren't API requests or do not have a valid access token
|
|
133
|
-
const defaultHandler = {
|
|
134
|
-
// This fetch method works just like a standard Cloudflare Workers fetch handler
|
|
135
|
-
//
|
|
136
|
-
// The `request`, `env`, and `ctx` parameters are the same as for a normal Cloudflare Workers fetch
|
|
137
|
-
// handler, and are exactly the objects that the `OAuthProvider` itself received from the Workers
|
|
138
|
-
// runtime.
|
|
139
|
-
//
|
|
140
|
-
// The `env.OAUTH_PROVIDER` provides an API by which the application can call back to the
|
|
141
|
-
// OAuthProvider.
|
|
142
|
-
async fetch(request: Request, env, ctx) {
|
|
143
|
-
let url = new URL(request.url);
|
|
144
|
-
|
|
145
|
-
if (url.pathname == '/authorize') {
|
|
146
|
-
// This is a request for our OAuth authorization flow UI. It is up to the application to
|
|
147
|
-
// implement this. However, the OAuthProvider library provides some helpers to assist.
|
|
148
|
-
|
|
149
|
-
// `env.OAUTH_PROVIDER.parseAuthRequest()` parses the OAuth authorization request to extract the parameters
|
|
150
|
-
// required by the OAuth 2 standard, namely response_type, client_id, redirect_uri, scope, and
|
|
151
|
-
// state. It returns an object containing all these (using idiomatic camelCase naming).
|
|
152
|
-
let oauthReqInfo = await env.OAUTH_PROVIDER.parseAuthRequest(request);
|
|
153
|
-
|
|
154
|
-
// `env.OAUTH_PROVIDER.lookupClient()` looks up metadata about the client, as definetd by RFC-7591. This
|
|
155
|
-
// includes things like redirect_uris, client_name, logo_uri, etc.
|
|
156
|
-
let clientInfo = await env.OAUTH_PROVIDER.lookupClient(oauthReqInfo.clientId);
|
|
157
|
-
|
|
158
|
-
// At this point, the application should use `oauthReqInfo` and `clientInfo` to render an
|
|
159
|
-
// authorization consent UI to the user. The details of this are up to the app so are not
|
|
160
|
-
// shown here.
|
|
161
|
-
|
|
162
|
-
// After the user has granted consent, the application calls `env.OAUTH_PROVIDER.completeAuthorization()` to
|
|
163
|
-
// grant the authorization.
|
|
164
|
-
let { redirectTo } = await env.OAUTH_PROVIDER.completeAuthorization({
|
|
165
|
-
// The application passes back the original OAuth request info that was returned by
|
|
166
|
-
// `parseAuthRequest()` earlier.
|
|
167
|
-
request: oauthReqInfo,
|
|
168
|
-
|
|
169
|
-
// The application must specify the user's ID, which is some sort of string. This is needed
|
|
170
|
-
// so that the application can later query the OAuthProvider to enumerate all grants
|
|
171
|
-
// belonging to a particular user, e.g. to implement an audit and revocation UI.
|
|
172
|
-
userId: '1234',
|
|
173
|
-
|
|
174
|
-
// The application can specify some arbitary metadata which describes this grant. The
|
|
175
|
-
// metadata can contain any JSON-serializable content. This metadata is not used by the
|
|
176
|
-
// OAuthProvider, but the application can read back the metadata attached to specific
|
|
177
|
-
// grants when enumerating them later, again e.g. to implement an udit and revocation UI.
|
|
178
|
-
metadata: { label: 'foo' },
|
|
179
|
-
|
|
180
|
-
// The application specifies the list of OAuth scope identifiers that were granted. This
|
|
181
|
-
// may or may not be the same as was requested in `oauthReqInfo.scope`.
|
|
182
|
-
scope: ['document.read', 'document.write'],
|
|
183
|
-
|
|
184
|
-
// `props` is an arbitrary JSON-serializable object which will be passed back to the API
|
|
185
|
-
// handler for every request authorized by this grant.
|
|
186
|
-
props: {
|
|
187
|
-
userId: 1234,
|
|
188
|
-
username: 'Bob',
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// `completeAuthorization()` will have returned the URL to which the user should be redirected
|
|
193
|
-
// in order to complete the authorization flow. This is the requesting client's OAuth
|
|
194
|
-
// redirect_uri with the appropriate query parameters added to complete the flow and obtain
|
|
195
|
-
// tokens.
|
|
196
|
-
return Response.redirect(redirectTo, 302);
|
|
197
|
-
}
|
|
34
|
+
## Quick start
|
|
198
35
|
|
|
199
|
-
|
|
36
|
+
The provider accepts either plain `ExportedHandler` objects or classes extending `WorkerEntrypoint`. This example uses both.
|
|
200
37
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
};
|
|
38
|
+
```ts
|
|
39
|
+
import { OAuthProvider, type AuthRequest, type OAuthHelpers } from '@cloudflare/workers-oauth-provider';
|
|
40
|
+
import { WorkerEntrypoint } from 'cloudflare:workers';
|
|
204
41
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// The `this.ctx.props` property contains the `props` value that was passed to
|
|
215
|
-
// `env.OAUTH_PROVIDER.completeAuthorization()` during the authorization flow that authorized this client.
|
|
216
|
-
fetch(request: Request) {
|
|
217
|
-
// The application can implement its API endpoints like normal. This app implements a single
|
|
218
|
-
// endpoint, `/api/whoami`, which returns the user's authenticated identity.
|
|
219
|
-
|
|
220
|
-
let url = new URL(request.url);
|
|
221
|
-
if (url.pathname == '/api/whoami') {
|
|
222
|
-
// Since the username is embedded in `ctx.props`, which came from the access token that the
|
|
223
|
-
// OAuthProivder already verified, we don't need to do any other authentication steps.
|
|
224
|
-
return new Response(`You are authenticated as: ${this.ctx.props.username}`);
|
|
225
|
-
}
|
|
42
|
+
interface AuthProps {
|
|
43
|
+
userId: string;
|
|
44
|
+
displayName: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface Env {
|
|
48
|
+
OAUTH_KV: KVNamespace;
|
|
49
|
+
OAUTH_PROVIDER: OAuthHelpers;
|
|
50
|
+
}
|
|
226
51
|
|
|
227
|
-
|
|
52
|
+
class McpApiHandler extends WorkerEntrypoint<Env, AuthProps> {
|
|
53
|
+
fetch(request: Request): Response {
|
|
54
|
+
return Response.json({
|
|
55
|
+
authenticated: true,
|
|
56
|
+
userId: this.ctx.props.userId,
|
|
57
|
+
displayName: this.ctx.props.displayName,
|
|
58
|
+
});
|
|
228
59
|
}
|
|
229
60
|
}
|
|
230
|
-
```
|
|
231
61
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
user and client.
|
|
62
|
+
const defaultHandler: ExportedHandler<Env> = {
|
|
63
|
+
async fetch(request, env) {
|
|
64
|
+
const url = new URL(request.url);
|
|
236
65
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
66
|
+
if (url.pathname !== '/authorize') {
|
|
67
|
+
return new Response('Not found', { status: 404 });
|
|
68
|
+
}
|
|
240
69
|
|
|
241
|
-
This
|
|
70
|
+
// This parses the OAuth parameters and validates the client, redirect URI,
|
|
71
|
+
// response type, resource indicators, and configured PKCE restrictions.
|
|
72
|
+
let oauthRequest: AuthRequest;
|
|
73
|
+
try {
|
|
74
|
+
oauthRequest = await env.OAUTH_PROVIDER.parseAuthRequest(request);
|
|
75
|
+
} catch {
|
|
76
|
+
// Do not redirect until the client and redirect URI have been validated.
|
|
77
|
+
return new Response('Invalid authorization request', { status: 400 });
|
|
78
|
+
}
|
|
242
79
|
|
|
243
|
-
|
|
80
|
+
const client = await env.OAUTH_PROVIDER.lookupClient(oauthRequest.clientId);
|
|
244
81
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
- Purge expired and orphaned data from the KV namespace.
|
|
82
|
+
if (!client) {
|
|
83
|
+
return new Response('Unknown OAuth client', { status: 400 });
|
|
84
|
+
}
|
|
249
85
|
|
|
250
|
-
|
|
86
|
+
// Authenticate the user and obtain consent here. Do not automatically
|
|
87
|
+
// approve a request in production. This example assumes those steps have
|
|
88
|
+
// produced the following user and scope values.
|
|
89
|
+
const user = { id: 'user-123', displayName: 'Ada' };
|
|
90
|
+
const grantedScopes = oauthRequest.scope.filter((scope) => scope === 'mcp:read');
|
|
91
|
+
|
|
92
|
+
const { redirectTo } = await env.OAUTH_PROVIDER.completeAuthorization({
|
|
93
|
+
request: oauthRequest,
|
|
94
|
+
userId: user.id,
|
|
95
|
+
metadata: { clientName: client.clientName },
|
|
96
|
+
scope: grantedScopes,
|
|
97
|
+
props: {
|
|
98
|
+
userId: user.id,
|
|
99
|
+
displayName: user.displayName,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
251
102
|
|
|
252
|
-
|
|
103
|
+
return Response.redirect(redirectTo, 302);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
253
106
|
|
|
254
|
-
|
|
107
|
+
export default new OAuthProvider<Env>({
|
|
108
|
+
apiRoute: '/mcp',
|
|
109
|
+
apiHandler: McpApiHandler,
|
|
110
|
+
defaultHandler,
|
|
255
111
|
|
|
256
|
-
|
|
112
|
+
authorizeEndpoint: '/authorize',
|
|
113
|
+
tokenEndpoint: '/oauth/token',
|
|
257
114
|
|
|
258
|
-
|
|
115
|
+
scopesSupported: ['mcp:read'],
|
|
259
116
|
|
|
260
|
-
|
|
117
|
+
resourceMetadata: {
|
|
118
|
+
resource: 'https://mcp.example.com/mcp',
|
|
119
|
+
authorization_servers: ['https://mcp.example.com'],
|
|
120
|
+
scopes_supported: ['mcp:read'],
|
|
121
|
+
resource_name: 'Example MCP server',
|
|
122
|
+
},
|
|
261
123
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
tokenExchangeCallback: async (options) => {
|
|
266
|
-
// options.grantType is either 'authorization_code' or 'refresh_token'
|
|
267
|
-
// options.props contains the current props
|
|
268
|
-
// options.clientId, options.userId, and options.scope are also available
|
|
269
|
-
|
|
270
|
-
if (options.grantType === 'authorization_code') {
|
|
271
|
-
// For authorization code exchange, might want to obtain upstream tokens
|
|
272
|
-
const upstreamTokens = await exchangeUpstreamToken(options.props.someCode);
|
|
273
|
-
|
|
274
|
-
return {
|
|
275
|
-
// Update the props stored in the access token
|
|
276
|
-
accessTokenProps: {
|
|
277
|
-
...options.props,
|
|
278
|
-
upstreamAccessToken: upstreamTokens.access_token,
|
|
279
|
-
},
|
|
280
|
-
// Update the props stored in the grant (for future token refreshes)
|
|
281
|
-
newProps: {
|
|
282
|
-
...options.props,
|
|
283
|
-
upstreamRefreshToken: upstreamTokens.refresh_token,
|
|
284
|
-
},
|
|
285
|
-
};
|
|
286
|
-
}
|
|
124
|
+
// Preferred for clients with no pre-existing relationship.
|
|
125
|
+
// Also requires global_fetch_strictly_public in wrangler.jsonc.
|
|
126
|
+
clientIdMetadataDocumentEnabled: true,
|
|
287
127
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const upstreamTokens = await refreshUpstreamToken(options.props.upstreamRefreshToken);
|
|
291
|
-
|
|
292
|
-
return {
|
|
293
|
-
accessTokenProps: {
|
|
294
|
-
...options.props,
|
|
295
|
-
upstreamAccessToken: upstreamTokens.access_token,
|
|
296
|
-
},
|
|
297
|
-
newProps: {
|
|
298
|
-
...options.props,
|
|
299
|
-
upstreamRefreshToken: upstreamTokens.refresh_token || options.props.upstreamRefreshToken,
|
|
300
|
-
},
|
|
301
|
-
// Optionally override the default access token TTL to match the upstream token
|
|
302
|
-
accessTokenTTL: upstreamTokens.expires_in,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
},
|
|
128
|
+
// Optional compatibility fallback. MCP 2026 deprecates DCR for new clients.
|
|
129
|
+
clientRegistrationEndpoint: '/oauth/register',
|
|
306
130
|
});
|
|
307
131
|
```
|
|
308
132
|
|
|
309
|
-
|
|
133
|
+
## Protecting routes
|
|
310
134
|
|
|
311
|
-
|
|
312
|
-
- Return only `accessTokenProps` to update just the current access token
|
|
313
|
-
- Return only `newProps` to update both the grant and access token (the access token inherits these props)
|
|
314
|
-
- Return `accessTokenTTL` to override the default TTL for this specific access token
|
|
315
|
-
- Return `refreshTokenTTL` to override the default TTL for this specific refresh token
|
|
316
|
-
- Return nothing to keep the original props unchanged
|
|
135
|
+
`apiRoute` and `apiHandler` protect one or more route prefixes with a single handler. Use `apiHandlers` when different prefixes need different handlers.
|
|
317
136
|
|
|
318
|
-
|
|
137
|
+
Before calling a protected handler, the provider reads the bearer token, rejects missing, invalid, or expired credentials, checks its audience, and exposes the authenticated application data through `ctx.props`. The handler does not need to parse or validate the token, but it must still enforce application permissions such as scope, ownership, and tenancy.
|
|
319
138
|
|
|
320
|
-
|
|
139
|
+
Requests outside the protected route prefixes go to `defaultHandler`. In the example above, that handler owns `/authorize`.
|
|
321
140
|
|
|
322
|
-
|
|
141
|
+
## How MCP authorization discovery works
|
|
323
142
|
|
|
324
|
-
|
|
143
|
+
An MCP client discovers authorization in two stages, following the [MCP authorization server discovery rules](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/authorization-server-discovery).
|
|
325
144
|
|
|
326
|
-
|
|
327
|
-
import { OAuthError, OAuthProvider } from '@cloudflare/workers-oauth-provider';
|
|
145
|
+
For an MCP endpoint at `https://mcp.example.com/mcp`:
|
|
328
146
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
tokenExchangeCallback: async (options) => {
|
|
332
|
-
if (options.grantType === 'refresh_token') {
|
|
333
|
-
return { newProps: await refreshUpstream(options.props) };
|
|
334
|
-
}
|
|
335
|
-
},
|
|
336
|
-
});
|
|
147
|
+
1. The client sends an unauthenticated request to `/mcp`.
|
|
148
|
+
2. The provider returns `401 Unauthorized` with a challenge similar to:
|
|
337
149
|
|
|
338
|
-
|
|
339
|
-
|
|
150
|
+
```http
|
|
151
|
+
WWW-Authenticate: Bearer realm="OAuth", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"
|
|
152
|
+
```
|
|
340
153
|
|
|
341
|
-
|
|
342
|
-
throw new OAuthError('invalid_grant', {
|
|
343
|
-
description: 'upstream refresh token is invalid',
|
|
344
|
-
});
|
|
345
|
-
}
|
|
154
|
+
3. The client fetches the protected resource metadata:
|
|
346
155
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
statusCode: 429,
|
|
351
|
-
headers: { 'Retry-After': res.headers.get('retry-after') ?? '60' },
|
|
352
|
-
});
|
|
353
|
-
}
|
|
156
|
+
```text
|
|
157
|
+
https://mcp.example.com/.well-known/oauth-protected-resource/mcp
|
|
158
|
+
```
|
|
354
159
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
```
|
|
160
|
+
4. That document identifies one or more authorization server issuers through `authorization_servers`.
|
|
161
|
+
5. The client fetches this provider's RFC 8414 authorization server metadata:
|
|
358
162
|
|
|
359
|
-
|
|
163
|
+
```text
|
|
164
|
+
https://mcp.example.com/.well-known/oauth-authorization-server
|
|
165
|
+
```
|
|
360
166
|
|
|
361
|
-
|
|
362
|
-
- `options.description` — human-readable text returned in `error_description`.
|
|
363
|
-
- `options.statusCode` — HTTP status code (default `400`).
|
|
364
|
-
- `options.headers` — additional response headers, such as `Retry-After` for transient failures. There is no implicit `Retry-After` default for callback-thrown errors.
|
|
167
|
+
6. The metadata tells the client where to authorize, exchange tokens, and register if registration is enabled.
|
|
365
168
|
|
|
366
|
-
|
|
169
|
+
Protected resource metadata and authorization server metadata serve different roles:
|
|
367
170
|
|
|
368
|
-
|
|
171
|
+
- Protected resource metadata describes the MCP server and identifies its authorization servers.
|
|
172
|
+
- Authorization server metadata describes OAuth endpoints and capabilities such as PKCE and CIMD.
|
|
369
173
|
|
|
370
|
-
|
|
174
|
+
### Protected resource metadata
|
|
371
175
|
|
|
372
|
-
|
|
373
|
-
new OAuthProvider({
|
|
374
|
-
// ... other options ...
|
|
375
|
-
resourceMetadata: { resource: 'https://mcp.example.com/mcp' },
|
|
376
|
-
enterpriseManagedAuthorization: {
|
|
377
|
-
trustedIssuers: async ({ iss }) =>
|
|
378
|
-
iss === 'https://idp.example.com'
|
|
379
|
-
? { issuer: iss, jwksUri: 'https://idp.example.com/.well-known/jwks.json', algorithms: ['RS256'] }
|
|
380
|
-
: null,
|
|
381
|
-
async mapClaims({ claims, requestedScope }) {
|
|
382
|
-
return {
|
|
383
|
-
// Opaque tokens use ':' as a separator — encode subjects that may contain it.
|
|
384
|
-
userId: `enterprise-${claims.sub}`,
|
|
385
|
-
scope: requestedScope,
|
|
386
|
-
metadata: { enterpriseIssuer: claims.iss, enterpriseSubject: claims.sub },
|
|
387
|
-
props: { enterprise: true, subject: claims.sub, email: claims.email },
|
|
388
|
-
};
|
|
389
|
-
},
|
|
390
|
-
},
|
|
391
|
-
});
|
|
392
|
-
```
|
|
176
|
+
The provider always serves RFC 9728 metadata at:
|
|
393
177
|
|
|
394
|
-
|
|
178
|
+
```text
|
|
179
|
+
/.well-known/oauth-protected-resource
|
|
180
|
+
```
|
|
395
181
|
|
|
396
|
-
|
|
397
|
-
2. Set `resourceMetadata.resource` to the MCP endpoint URL (required when EMA is enabled).
|
|
398
|
-
3. Implement `trustedIssuers` as a resolver — for multi-tenant deployments it can read `env` / `clientInfo` to look up per-tenant IdP config without redeploying.
|
|
182
|
+
It also supports path-specific metadata. A request to:
|
|
399
183
|
|
|
400
|
-
|
|
184
|
+
```text
|
|
185
|
+
/.well-known/oauth-protected-resource/public/mcp
|
|
186
|
+
```
|
|
401
187
|
|
|
402
|
-
|
|
188
|
+
produces `https://example.com/public/mcp` as the derived resource unless `resourceMetadata.resource` overrides it.
|
|
403
189
|
|
|
404
|
-
|
|
190
|
+
For MCP deployments, configure the canonical MCP endpoint explicitly:
|
|
405
191
|
|
|
406
192
|
```ts
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
193
|
+
resourceMetadata: {
|
|
194
|
+
resource: 'https://mcp.example.com/mcp',
|
|
195
|
+
authorization_servers: ['https://auth.example.com'],
|
|
196
|
+
scopes_supported: ['files:read'],
|
|
197
|
+
bearer_methods_supported: ['header'],
|
|
198
|
+
resource_name: 'Files MCP server',
|
|
410
199
|
}
|
|
411
200
|
```
|
|
412
201
|
|
|
413
|
-
|
|
202
|
+
`authorization_servers` may contain more than one issuer. The MCP client chooses an authorization server and must keep credentials and tokens separate for each issuer.
|
|
414
203
|
|
|
415
|
-
|
|
204
|
+
### Authorization server metadata
|
|
416
205
|
|
|
417
|
-
|
|
206
|
+
The provider publishes RFC 8414 metadata containing:
|
|
418
207
|
|
|
419
|
-
|
|
208
|
+
- `issuer`
|
|
209
|
+
- `authorization_endpoint`
|
|
210
|
+
- `token_endpoint`
|
|
211
|
+
- `registration_endpoint`, when DCR is enabled
|
|
212
|
+
- supported response and grant types
|
|
213
|
+
- token endpoint authentication methods
|
|
214
|
+
- PKCE methods
|
|
215
|
+
- revocation endpoint
|
|
216
|
+
- RFC 9207 issuer support
|
|
217
|
+
- CIMD support when it is enabled and safe to use
|
|
420
218
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
219
|
+
The package serves RFC 8414 metadata rather than OpenID Connect discovery. MCP authorization servers need to provide at least one of those mechanisms, so RFC 8414 is sufficient.
|
|
220
|
+
|
|
221
|
+
## Authorization endpoint
|
|
222
|
+
|
|
223
|
+
Your `authorizeEndpoint` belongs to the application's `defaultHandler` because user authentication and consent are application-specific. The provider is not an identity provider.
|
|
224
|
+
|
|
225
|
+
A typical flow has three steps:
|
|
226
|
+
|
|
227
|
+
1. Call `parseAuthRequest(request)` to validate the client, redirect URI, response type, resource, and PKCE restrictions.
|
|
228
|
+
2. Authenticate the user, show consent, and decide which scopes to grant.
|
|
229
|
+
3. Call `completeAuthorization()` and redirect to its returned `redirectTo` URL.
|
|
230
|
+
|
|
231
|
+
`completeAuthorization()` repeats response-type validation before writing a grant or revoking existing grants. The application remains responsible for rendering local authorization errors and for constructing any terminal OAuth error redirect only after client and redirect URI validation.
|
|
232
|
+
|
|
233
|
+
`completeAuthorization()` stores a new grant and, by default, revokes existing grants for the same user and client after the new grant is safely stored. Set `revokeExistingGrants: false` only when the application intentionally allows concurrent grants for the same user and client.
|
|
234
|
+
|
|
235
|
+
For users with many grants, `revokeExistingGrantsBatchSize` controls the KV page size used during that scan. It defaults to `50` and is capped at KV's maximum page size of `1000`.
|
|
429
236
|
|
|
430
|
-
|
|
237
|
+
### Authorization response issuer
|
|
238
|
+
|
|
239
|
+
RFC 9207 issuer identification is always enabled. Authorization server metadata advertises `authorization_response_iss_parameter_supported: true`, and successful authorization responses include `iss` automatically.
|
|
240
|
+
|
|
241
|
+
`parseAuthRequest()` returns the expected `issuer`. If the application creates a terminal OAuth error redirect, include that value:
|
|
431
242
|
|
|
432
243
|
```ts
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
// returning undefined (i.e. void) uses the default Response generation
|
|
440
|
-
},
|
|
441
|
-
});
|
|
244
|
+
const oauthRequest = await env.OAUTH_PROVIDER.parseAuthRequest(request);
|
|
245
|
+
const redirect = new URL(oauthRequest.redirectUri);
|
|
246
|
+
redirect.searchParams.set('error', 'access_denied');
|
|
247
|
+
redirect.searchParams.set('state', oauthRequest.state);
|
|
248
|
+
if (oauthRequest.issuer) redirect.searchParams.set('iss', oauthRequest.issuer);
|
|
249
|
+
return Response.redirect(redirect.toString(), 302);
|
|
442
250
|
```
|
|
443
251
|
|
|
444
|
-
|
|
252
|
+
Intermediate identity-provider redirects and local HTML error pages do not need the OAuth `iss` parameter.
|
|
253
|
+
|
|
254
|
+
## Client registration
|
|
255
|
+
|
|
256
|
+
[MCP client registration](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration) defines three ways for a client to obtain a client ID. Clients that support all three prefer pre-registration, then CIMD, then DCR.
|
|
445
257
|
|
|
446
|
-
|
|
258
|
+
### Pre-registered clients
|
|
447
259
|
|
|
448
|
-
|
|
260
|
+
Use `OAuthHelpers.createClient()` to create clients through application or administrative code. These clients are stored in KV and are not subject to `clientRegistrationTTL`.
|
|
261
|
+
|
|
262
|
+
### Client ID Metadata Documents
|
|
263
|
+
|
|
264
|
+
CIMD lets a client use an HTTPS URL with a non-root path as its `client_id`. That URL serves a JSON metadata document describing the client and its redirect URIs.
|
|
265
|
+
|
|
266
|
+
Enable it in both places:
|
|
449
267
|
|
|
450
268
|
```ts
|
|
451
|
-
|
|
452
|
-
//
|
|
269
|
+
new OAuthProvider({
|
|
270
|
+
// Other options...
|
|
271
|
+
clientIdMetadataDocumentEnabled: true,
|
|
453
272
|
});
|
|
273
|
+
```
|
|
454
274
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
async scheduled(event, env, ctx) {
|
|
460
|
-
const result = await oauthProvider.purgeExpiredData(env, { batchSize: 100 });
|
|
461
|
-
console.log(`Checked ${result.grantsChecked} grants, purged ${result.grantsPurged}`);
|
|
462
|
-
},
|
|
463
|
-
};
|
|
275
|
+
```jsonc
|
|
276
|
+
{
|
|
277
|
+
"compatibility_flags": ["global_fetch_strictly_public"],
|
|
278
|
+
}
|
|
464
279
|
```
|
|
465
280
|
|
|
466
|
-
The
|
|
281
|
+
The compatibility flag prevents outbound CIMD fetches from using legacy same-zone origin routing, which is necessary for SSRF protection. The provider advertises `client_id_metadata_document_supported: true` only when both settings are present.
|
|
282
|
+
|
|
283
|
+
CIMD validation includes:
|
|
467
284
|
|
|
468
|
-
|
|
469
|
-
|
|
285
|
+
- HTTPS URL with a non-root path.
|
|
286
|
+
- A document `client_id` exactly matching its URL.
|
|
287
|
+
- Non-empty `client_name` and `redirect_uris` fields.
|
|
288
|
+
- Exact authorization-request redirect URI validation, with RFC 8252 loopback port handling.
|
|
289
|
+
- A 5 KB response size limit and 10 second fetch timeout.
|
|
290
|
+
- Safe URI schemes for client metadata fields.
|
|
470
291
|
|
|
471
|
-
|
|
292
|
+
CIMD currently supports only `token_endpoint_auth_method: "none"`.
|
|
472
293
|
|
|
473
|
-
|
|
294
|
+
When a CIMD document cannot be fetched or validated, the token endpoint returns a generic `invalid_client` response and reports diagnostics through `onError.internal`. `OAuthHelpers` methods that resolve a CIMD client throw the exported `CimdFetchError`, allowing applications to distinguish an upstream metadata failure from a client that does not exist. See [Advanced configuration](https://github.com/cloudflare/workers-oauth-provider/blob/main/docs/advanced-configuration.md#cimd-fetch-errors) for an example.
|
|
474
295
|
|
|
475
|
-
|
|
296
|
+
### Dynamic Client Registration
|
|
297
|
+
|
|
298
|
+
Set `clientRegistrationEndpoint` to enable RFC 7591 Dynamic Client Registration:
|
|
476
299
|
|
|
477
300
|
```ts
|
|
478
|
-
|
|
479
|
-
// ... other options ...
|
|
480
|
-
resourceMetadata: {
|
|
481
|
-
resource: 'https://api.example.com',
|
|
482
|
-
authorization_servers: ['https://auth.example.com'],
|
|
483
|
-
scopes_supported: ['read', 'write'],
|
|
484
|
-
bearer_methods_supported: ['header'],
|
|
485
|
-
resource_name: 'My API',
|
|
486
|
-
},
|
|
487
|
-
});
|
|
301
|
+
clientRegistrationEndpoint: '/oauth/register';
|
|
488
302
|
```
|
|
489
303
|
|
|
490
|
-
|
|
304
|
+
MCP 2026-07-28 deprecates DCR for new implementations in favor of CIMD. The endpoint remains useful for compatibility with clients that do not support CIMD.
|
|
491
305
|
|
|
492
|
-
|
|
306
|
+
Registration accepts only authentication methods, grants, and response types implemented by the configured provider, and rejects inconsistent grant/response combinations before storage. Omitted metadata uses the RFC 7591 defaults: `client_secret_basic`, `grant_types: ["authorization_code"]`, and `response_types: ["code"]`.
|
|
493
307
|
|
|
494
|
-
|
|
495
|
-
- [OAuth 2.0 Authorization Server Metadata (RFC 8414)](https://datatracker.ietf.org/doc/html/rfc8414) — `/.well-known/oauth-authorization-server` discovery endpoint
|
|
496
|
-
- [OAuth 2.0 Protected Resource Metadata (RFC 9728)](https://datatracker.ietf.org/doc/html/rfc9728) — `/.well-known/oauth-protected-resource` discovery endpoint
|
|
497
|
-
- [OAuth 2.0 Dynamic Client Registration (RFC 7591)](https://datatracker.ietf.org/doc/html/rfc7591) — Dynamic client registration endpoint
|
|
498
|
-
- [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
|
|
499
|
-
- [MCP Enterprise-Managed Authorization extension](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization) — experimental ID-JAG JWT bearer grant support
|
|
308
|
+
Related options:
|
|
500
309
|
|
|
501
|
-
|
|
310
|
+
- `clientRegistrationTTL` controls the lifetime of dynamically registered clients. The default is 90 days.
|
|
311
|
+
- `disallowPublicClientRegistration` rejects DCR clients using `token_endpoint_auth_method: "none"`.
|
|
312
|
+
- `clientRegistrationCallback` can allow or reject registration based on application policy.
|
|
502
313
|
|
|
503
|
-
|
|
314
|
+
Clients created by `OAuthHelpers.createClient()` are not affected by the DCR TTL or public-registration restriction.
|
|
504
315
|
|
|
505
|
-
|
|
316
|
+
## PKCE and token lifecycle
|
|
506
317
|
|
|
507
|
-
|
|
318
|
+
Public clients must use PKCE with authorization code flow. PKCE challenges use only S256 by default. Confidential clients may still omit PKCE.
|
|
508
319
|
|
|
509
|
-
|
|
510
|
-
- The `props` associated with a grant (which are passed back to the application when API requests are performed) are stored encrypted with the secret token as key material. Hence, the contents of `props` are impossible to derive from storage unless a valid token is provided.
|
|
320
|
+
Legacy deployments with clients that cannot use S256 can opt back into plain PKCE:
|
|
511
321
|
|
|
512
|
-
|
|
322
|
+
```ts
|
|
323
|
+
allowPlainPKCE: true;
|
|
324
|
+
```
|
|
513
325
|
|
|
514
|
-
|
|
326
|
+
`allowImplicitFlow` defaults to `false`; leave it disabled for MCP and other new OAuth deployments.
|
|
515
327
|
|
|
516
|
-
|
|
328
|
+
The provider owns `tokenEndpoint`. It exchanges authorization codes for tokens, refreshes access tokens, and handles RFC 7009 revocation. Refresh tokens rotate on use. The immediately previous token remains valid until its replacement is first used, allowing a client to retry after losing a refresh response.
|
|
517
329
|
|
|
518
|
-
|
|
330
|
+
## Resources and token audiences
|
|
519
331
|
|
|
520
|
-
|
|
332
|
+
MCP clients must send the canonical MCP server URI as `resource` in authorization and token requests. The provider parses RFC 8707 resource indicators, stores the authorized resource on the grant, uses it as the access-token audience, and rejects resource expansion or audience mismatch.
|
|
521
333
|
|
|
522
|
-
|
|
334
|
+
Resource policy follows `resourceMetadata.resource`:
|
|
523
335
|
|
|
524
|
-
|
|
336
|
+
- When configured, authorization requests, token requests, and externally resolved tokens must use that one exact resource. `resourceMatchOriginOnly` cannot weaken this policy.
|
|
337
|
+
- When omitted, valid resources are accepted. Token requests may inherit the authorization resource. If the authorization request also omits it, the provider uses the request origin as the default and issues an origin-bound token.
|
|
525
338
|
|
|
526
|
-
|
|
339
|
+
Path-aware audiences use path-boundary prefix matching. A token for `https://example.com/mcp` can be used at `/mcp/tools`, but not at `/mcp-other`. Split deployments and deployments requiring path isolation should configure the canonical resource explicitly.
|
|
527
340
|
|
|
528
|
-
|
|
341
|
+
`resourceMatchOriginOnly` remains a migration option for grants created before path-aware resources were introduced. Do not enable it for a new deployment.
|
|
529
342
|
|
|
530
|
-
|
|
343
|
+
## Scopes and step-up authorization
|
|
531
344
|
|
|
532
|
-
|
|
533
|
-
new OAuthProvider({
|
|
534
|
-
// ... other options ...
|
|
535
|
-
clientIdMetadataDocumentEnabled: true,
|
|
536
|
-
});
|
|
537
|
-
```
|
|
345
|
+
`scopesSupported` is published only in authorization server metadata. Configure `resourceMetadata.scopes_supported` explicitly with the minimal scopes required for basic protected-resource functionality and baseline Bearer challenges.
|
|
538
346
|
|
|
539
|
-
|
|
347
|
+
The application decides which requested scopes to grant through `completeAuthorization({ scope })`. Token and refresh requests can only narrow those scopes.
|
|
540
348
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
349
|
+
The provider does not expose a standard effective-token authorization context to API handlers or enforce operation-level scope policy. Protected resource metadata supplies baseline scope guidance in Bearer challenges. Advanced integrations can provide operation-specific step-up guidance through external-token validation.
|
|
350
|
+
|
|
351
|
+
## Advanced features
|
|
352
|
+
|
|
353
|
+
The package also supports:
|
|
354
|
+
|
|
355
|
+
- External API keys and bearer credentials through `resolveExternalToken` as an advanced compatibility feature.
|
|
356
|
+
- Updating encrypted props, token scope, and token lifetimes with `tokenExchangeCallback`.
|
|
357
|
+
- OAuth 2.0 Token Exchange when `allowTokenExchangeGrant` is enabled.
|
|
358
|
+
- Structured callback errors through the exported `OAuthError` and `ExternalTokenError` classes.
|
|
359
|
+
- Custom error observation or responses through `onError`.
|
|
360
|
+
- Experimental MCP Enterprise-Managed Authorization using ID-JAG assertions.
|
|
361
|
+
- Multiple protected handlers through `apiHandlers`.
|
|
362
|
+
- Configurable access token, refresh token, and DCR client lifetimes.
|
|
546
363
|
|
|
547
|
-
|
|
364
|
+
See [Advanced configuration](https://github.com/cloudflare/workers-oauth-provider/blob/main/docs/advanced-configuration.md) for examples and security notes.
|
|
548
365
|
|
|
549
|
-
|
|
366
|
+
## KV storage and cleanup
|
|
550
367
|
|
|
551
|
-
|
|
368
|
+
Sensitive values are not stored in plaintext:
|
|
552
369
|
|
|
553
|
-
|
|
370
|
+
- Access tokens, refresh tokens, authorization codes, and client secrets are stored only by hash.
|
|
371
|
+
- `props` are encrypted with AES-GCM using key material wrapped by the corresponding secret token.
|
|
372
|
+
- Grant `userId` and `metadata` are not encrypted because applications use them to enumerate and revoke grants. Treat those fields as storage-visible metadata.
|
|
554
373
|
|
|
555
|
-
|
|
374
|
+
See [storage-schema.md](https://github.com/cloudflare/workers-oauth-provider/blob/main/storage-schema.md) for the complete KV layout.
|
|
556
375
|
|
|
557
|
-
|
|
376
|
+
KV TTLs remove expiring records automatically. `purgeExpiredData()` provides a manual sweep for orphaned or expired grants and tokens:
|
|
558
377
|
|
|
559
|
-
|
|
378
|
+
```ts
|
|
379
|
+
const provider = new OAuthProvider({
|
|
380
|
+
// Options...
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
export default {
|
|
384
|
+
fetch(request, env, ctx) {
|
|
385
|
+
return provider.fetch(request, env, ctx);
|
|
386
|
+
},
|
|
387
|
+
async scheduled(_event, env) {
|
|
388
|
+
const result = await provider.purgeExpiredData(env, { batchSize: 100 });
|
|
389
|
+
console.log(result);
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
The default batch size is 50. `result.done` reports whether both key spaces were scanned completely during that invocation.
|
|
395
|
+
|
|
396
|
+
Deleting a client through `OAuthHelpers.deleteClient()` also revokes its grants and associated tokens across users.
|
|
397
|
+
|
|
398
|
+
## Configuration reference
|
|
399
|
+
|
|
400
|
+
| Option | Purpose | Default |
|
|
401
|
+
| ---------------------------------- | -------------------------------------------------------- | ------------------------------------------- |
|
|
402
|
+
| `apiRoute` and `apiHandler` | Protect one or more route prefixes with one handler | Use these or `apiHandlers` |
|
|
403
|
+
| `apiHandlers` | Map protected route prefixes to different handlers | Use this or `apiRoute` plus `apiHandler` |
|
|
404
|
+
| `defaultHandler` | Handle authorization UI and other unprotected routes | Required |
|
|
405
|
+
| `authorizeEndpoint` | Application-owned authorization and consent endpoint | Required |
|
|
406
|
+
| `tokenEndpoint` | Provider-owned token and revocation endpoint | Required |
|
|
407
|
+
| `clientRegistrationEndpoint` | Enable RFC 7591 DCR | Disabled |
|
|
408
|
+
| `scopesSupported` | Publish authorization server scopes | Omitted |
|
|
409
|
+
| `resourceMetadata` | Configure RFC 9728 metadata | Derived from the request and token endpoint |
|
|
410
|
+
| `clientIdMetadataDocumentEnabled` | Enable CIMD lookup and advertisement | `false` |
|
|
411
|
+
| `allowPlainPKCE` | Permit the legacy plain PKCE method | `false` |
|
|
412
|
+
| `allowImplicitFlow` | Enable implicit token responses | `false` |
|
|
413
|
+
| `disallowPublicClientRegistration` | Reject public clients at DCR | `false` |
|
|
414
|
+
| `clientRegistrationCallback` | Apply application policy before storing a DCR client | None |
|
|
415
|
+
| `allowTokenExchangeGrant` | Enable RFC 8693 | `false` |
|
|
416
|
+
| `tokenExchangeCallback` | Update props, scopes, or lifetimes during token exchange | None |
|
|
417
|
+
| `resolveExternalToken` | Validate external bearer credentials (advanced) | None |
|
|
418
|
+
| `resourceMatchOriginOnly` | Migration mode for old origin-only resource grants | `false` |
|
|
419
|
+
| `enterpriseManagedAuthorization` | Enable experimental ID-JAG grant support | Disabled |
|
|
420
|
+
| `onError` | Observe or replace OAuth error responses | Logs a warning |
|
|
421
|
+
|
|
422
|
+
Consult the exported `OAuthProviderOptions`, callback interfaces, and JSDoc in [`src/oauth-provider.ts`](https://github.com/cloudflare/workers-oauth-provider/blob/main/src/oauth-provider.ts) for the complete typed API.
|
|
423
|
+
|
|
424
|
+
## OAuth helpers
|
|
425
|
+
|
|
426
|
+
Handlers receive `env.OAUTH_PROVIDER`, which implements `OAuthHelpers`. It can:
|
|
427
|
+
|
|
428
|
+
- Parse authorization requests and complete authorization.
|
|
429
|
+
- Look up, create, list, update, and delete clients.
|
|
430
|
+
- List and revoke grants for a user.
|
|
431
|
+
- Inspect internally issued tokens with `unwrapToken()`.
|
|
432
|
+
- Exchange access tokens when RFC 8693 is enabled.
|
|
433
|
+
- Purge expired and orphaned KV data.
|
|
434
|
+
|
|
435
|
+
`getOAuthApi(options, env)` provides the same helper API outside a fetch handler, including RPC methods and other Worker entrypoints.
|
|
436
|
+
|
|
437
|
+
## Standards
|
|
438
|
+
|
|
439
|
+
The package implements or supports the relevant portions of:
|
|
440
|
+
|
|
441
|
+
- [MCP authorization, 2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization)
|
|
442
|
+
- [OAuth 2.1, draft-ietf-oauth-v2-1-13](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13)
|
|
443
|
+
- [OAuth 2.0 Bearer Token Usage, RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750)
|
|
444
|
+
- [OAuth 2.0 Token Revocation, RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009)
|
|
445
|
+
- [OAuth 2.0 Dynamic Client Registration, RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591)
|
|
446
|
+
- [Proof Key for Code Exchange, RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)
|
|
447
|
+
- [OAuth 2.0 Authorization Server Metadata, RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414)
|
|
448
|
+
- [OAuth 2.0 Token Exchange, RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693)
|
|
449
|
+
- [Resource Indicators for OAuth 2.0, RFC 8707](https://datatracker.ietf.org/doc/html/rfc8707)
|
|
450
|
+
- [OAuth 2.0 Authorization Server Issuer Identification, RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207)
|
|
451
|
+
- [OAuth 2.0 Protected Resource Metadata, RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)
|
|
452
|
+
- [OAuth Client ID Metadata Documents](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00)
|
|
453
|
+
- [MCP Enterprise-Managed Authorization](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization), with experimental package support
|
|
454
|
+
|
|
455
|
+
## Development
|
|
456
|
+
|
|
457
|
+
Node 24 or newer is required.
|
|
458
|
+
|
|
459
|
+
```sh
|
|
460
|
+
npm install
|
|
461
|
+
npm run build
|
|
462
|
+
npm run check
|
|
463
|
+
npm run prettier
|
|
464
|
+
```
|
|
560
465
|
|
|
561
|
-
|
|
466
|
+
Changes that affect behavior or the public API need a Changeset. See [AGENTS.md](https://github.com/cloudflare/workers-oauth-provider/blob/main/AGENTS.md) for repository conventions and [SECURITY.md](https://github.com/cloudflare/workers-oauth-provider/blob/main/SECURITY.md) for vulnerability reporting.
|
|
562
467
|
|
|
563
|
-
|
|
468
|
+
## Project history
|
|
564
469
|
|
|
565
|
-
|
|
470
|
+
Kenton Varda's original account of how this library was created is preserved in [HISTORY.md](https://github.com/cloudflare/workers-oauth-provider/blob/main/HISTORY.md).
|