@cloudflare/workers-oauth-provider 0.0.13 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,20 +4,20 @@ This is a TypeScript library that implements the provider side of the OAuth 2.1
4
4
 
5
5
  ## Benefits of this library
6
6
 
7
- * The library acts as a wrapper around your Worker code, which adds authorization for your API endpoints.
8
- * All token management is handled automatically.
9
- * Your API handler is written like a regular fetch handler, but receives the already-authenticated user details as a parameter. No need to perform any checks of your own.
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
+ - The library acts as a wrapper around your Worker code, which adds authorization for your API endpoints.
8
+ - All token management is handled automatically.
9
+ - Your API handler is written like a regular fetch handler, but receives the already-authenticated user details as a parameter. No need to perform any checks of your own.
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.
13
13
 
14
14
  ## Usage
15
15
 
16
16
  A Worker that uses the library might look like this:
17
17
 
18
18
  ```ts
19
- import { OAuthProvider } from "@cloudflare/workers-oauth-provider";
20
- import { WorkerEntrypoint } from "cloudflare:workers";
19
+ import { OAuthProvider } from '@cloudflare/workers-oauth-provider';
20
+ import { WorkerEntrypoint } from 'cloudflare:workers';
21
21
 
22
22
  // We export the OAuthProvider instance as the entrypoint to our Worker. This means it
23
23
  // implements the `fetch()` handler, receiving all HTTP requests.
@@ -29,8 +29,8 @@ export default new OAuthProvider({
29
29
  // - A single route (string) or multiple routes (array)
30
30
  // - Full URLs (which will match the hostname) or just paths (which will match any hostname)
31
31
  apiRoute: [
32
- "/api/", // Path only - will match any hostname
33
- "https://api.example.com/" // Full URL - will check hostname
32
+ '/api/', // Path only - will match any hostname
33
+ 'https://api.example.com/', // Full URL - will check hostname
34
34
  ],
35
35
 
36
36
  // When the OAuth system receives an API request with a valid access token, it passes the request
@@ -58,23 +58,23 @@ export default new OAuthProvider({
58
58
  // this URL is given to the OAuthProvider is so that it can implement the RFC-8414 metadata
59
59
  // discovery endpoint, i.e. `.well-known/oauth-authorization-server`.
60
60
  // Can also be specified as just a path (e.g., "/authorize").
61
- authorizeEndpoint: "https://example.com/authorize",
61
+ authorizeEndpoint: 'https://example.com/authorize',
62
62
 
63
63
  // This specifies the OAuth 2 token exchange endpoint. The OAuthProvider will implement this
64
64
  // endpoint (by directly responding to requests with a matching URL).
65
65
  // Can also be specified as just a path (e.g., "/oauth/token").
66
- tokenEndpoint: "https://example.com/oauth/token",
66
+ tokenEndpoint: 'https://example.com/oauth/token',
67
67
 
68
68
  // This specifies the RFC-7591 dynamic client registration endpoint. This setting is optional,
69
69
  // but if provided, the OAuthProvider will implement this endpoint to allow dynamic client
70
70
  // registration.
71
71
  // Can also be specified as just a path (e.g., "/oauth/register").
72
- clientRegistrationEndpoint: "https://example.com/oauth/register",
72
+ clientRegistrationEndpoint: 'https://example.com/oauth/register',
73
73
 
74
74
  // Optional list of scopes supported by this OAuth provider.
75
75
  // If provided, this will be included in the RFC 8414 metadata as 'scopes_supported'.
76
76
  // If not provided, the 'scopes_supported' field will be omitted from the metadata.
77
- scopesSupported: ["document.read", "document.write", "profile"],
77
+ scopesSupported: ['document.read', 'document.write', 'profile'],
78
78
 
79
79
  // Optional: Controls whether the OAuth implicit flow is allowed.
80
80
  // The implicit flow is discouraged in OAuth 2.1 but may be needed for some clients.
@@ -93,7 +93,7 @@ export default new OAuthProvider({
93
93
  // If not specified, refresh tokens do not expire.
94
94
  // Set to 0 to disable refresh tokens (only access tokens will be issued).
95
95
  // For example: 3600 = 1 hour, 86400 = 1 day, 2592000 = 30 days
96
- refreshTokenTTL: 2592000 // 30 days
96
+ refreshTokenTTL: 2592000, // 30 days
97
97
  });
98
98
 
99
99
  // The default handler object - the OAuthProvider will pass through HTTP requests to this object's fetch method
@@ -110,7 +110,7 @@ const defaultHandler = {
110
110
  async fetch(request: Request, env, ctx) {
111
111
  let url = new URL(request.url);
112
112
 
113
- if (url.pathname == "/authorize") {
113
+ if (url.pathname == '/authorize') {
114
114
  // This is a request for our OAuth authorization flow UI. It is up to the application to
115
115
  // implement this. However, the OAuthProvider library provides some helpers to assist.
116
116
 
@@ -129,7 +129,7 @@ const defaultHandler = {
129
129
 
130
130
  // After the user has granted consent, the application calls `env.OAUTH_PROVIDER.completeAuthorization()` to
131
131
  // grant the authorization.
132
- let {redirectTo} = await env.OAUTH_PROVIDER.completeAuthorization({
132
+ let { redirectTo } = await env.OAUTH_PROVIDER.completeAuthorization({
133
133
  // The application passes back the original OAuth request info that was returned by
134
134
  // `parseAuthRequest()` earlier.
135
135
  request: oauthReqInfo,
@@ -137,24 +137,24 @@ const defaultHandler = {
137
137
  // The application must specify the user's ID, which is some sort of string. This is needed
138
138
  // so that the application can later query the OAuthProvider to enumerate all grants
139
139
  // belonging to a particular user, e.g. to implement an audit and revocation UI.
140
- userId: "1234",
140
+ userId: '1234',
141
141
 
142
142
  // The application can specify some arbitary metadata which describes this grant. The
143
143
  // metadata can contain any JSON-serializable content. This metadata is not used by the
144
144
  // OAuthProvider, but the application can read back the metadata attached to specific
145
145
  // grants when enumerating them later, again e.g. to implement an udit and revocation UI.
146
- metadata: {label: "foo"},
146
+ metadata: { label: 'foo' },
147
147
 
148
148
  // The application specifies the list of OAuth scope identifiers that were granted. This
149
149
  // may or may not be the same as was requested in `oauthReqInfo.scope`.
150
- scope: ["document.read", "document.write"],
150
+ scope: ['document.read', 'document.write'],
151
151
 
152
152
  // `props` is an arbitrary JSON-serializable object which will be passed back to the API
153
153
  // handler for every request authorized by this grant.
154
154
  props: {
155
155
  userId: 1234,
156
- username: "Bob"
157
- }
156
+ username: 'Bob',
157
+ },
158
158
  });
159
159
 
160
160
  // `completeAuthorization()` will have returned the URL to which the user should be redirected
@@ -166,8 +166,8 @@ const defaultHandler = {
166
166
 
167
167
  // ... the application can implement other non-API HTTP endpoints here ...
168
168
 
169
- return new Response("Not found", {status: 404});
170
- }
169
+ return new Response('Not found', { status: 404 });
170
+ },
171
171
  };
172
172
 
173
173
  // The API handler object - the OAuthProivder will pass authorized API requests to this object's fetch method
@@ -186,24 +186,24 @@ class ApiHandler extends WorkerEntrypoint {
186
186
  // endpoint, `/api/whoami`, which returns the user's authenticated identity.
187
187
 
188
188
  let url = new URL(request.url);
189
- if (url.pathname == "/api/whoami") {
189
+ if (url.pathname == '/api/whoami') {
190
190
  // Since the username is embedded in `ctx.props`, which came from the access token that the
191
191
  // OAuthProivder already verified, we don't need to do any other authentication steps.
192
192
  return new Response(`You are authenticated as: ${this.ctx.props.username}`);
193
193
  }
194
194
 
195
- return new Response("Not found", {status: 404});
195
+ return new Response('Not found', { status: 404 });
196
196
  }
197
- };
197
+ }
198
198
  ```
199
199
 
200
200
  This implementation requires that your worker is configured with a Workers KV namespace binding called `OAUTH_KV`, which is used to store token information. See the file `storage-schema.md` for details on the schema of this namespace.
201
201
 
202
202
  The `env.OAUTH_PROVIDER` object available to the fetch handlers provides some methods to query the storage, including:
203
203
 
204
- * Create, list, modify, and delete client_id registrations (in addition to `lookupClient()`, already shown in the example code).
205
- * List all active authorization grants for a particular user.
206
- * Revoke (delete) an authorization grant.
204
+ - Create, list, modify, and delete client_id registrations (in addition to `lookupClient()`, already shown in the example code).
205
+ - List all active authorization grants for a particular user.
206
+ - Revoke (delete) an authorization grant.
207
207
 
208
208
  See the `OAuthHelpers` interface definition for full API details.
209
209
 
@@ -231,13 +231,13 @@ new OAuthProvider({
231
231
  // Update the props stored in the access token
232
232
  accessTokenProps: {
233
233
  ...options.props,
234
- upstreamAccessToken: upstreamTokens.access_token
234
+ upstreamAccessToken: upstreamTokens.access_token,
235
235
  },
236
236
  // Update the props stored in the grant (for future token refreshes)
237
237
  newProps: {
238
238
  ...options.props,
239
- upstreamRefreshToken: upstreamTokens.refresh_token
240
- }
239
+ upstreamRefreshToken: upstreamTokens.refresh_token,
240
+ },
241
241
  };
242
242
  }
243
243
 
@@ -248,21 +248,22 @@ new OAuthProvider({
248
248
  return {
249
249
  accessTokenProps: {
250
250
  ...options.props,
251
- upstreamAccessToken: upstreamTokens.access_token
251
+ upstreamAccessToken: upstreamTokens.access_token,
252
252
  },
253
253
  newProps: {
254
254
  ...options.props,
255
- upstreamRefreshToken: upstreamTokens.refresh_token || options.props.upstreamRefreshToken
255
+ upstreamRefreshToken: upstreamTokens.refresh_token || options.props.upstreamRefreshToken,
256
256
  },
257
257
  // Optionally override the default access token TTL to match the upstream token
258
- accessTokenTTL: upstreamTokens.expires_in
258
+ accessTokenTTL: upstreamTokens.expires_in,
259
259
  };
260
260
  }
261
- }
261
+ },
262
262
  });
263
263
  ```
264
264
 
265
265
  The callback can:
266
+
266
267
  - Return both `accessTokenProps` and `newProps` to update both
267
268
  - Return only `accessTokenProps` to update just the current access token
268
269
  - Return only `newProps` to update both the grant and access token (the access token inherits these props)
@@ -282,9 +283,9 @@ By using the `onError` option, you can emit notifications or take other actions
282
283
  new OAuthProvider({
283
284
  // ... other options ...
284
285
  onError({ code, description, status, headers }) {
285
- Sentry.captureMessage(/* ... */)
286
- }
287
- })
286
+ Sentry.captureMessage(/* ... */);
287
+ },
288
+ });
288
289
  ```
289
290
 
290
291
  By returning a `Response` you can also override what the OAuthProvider returns to your users:
@@ -294,11 +295,11 @@ new OAuthProvider({
294
295
  // ... other options ...
295
296
  onError({ code, description, status, headers }) {
296
297
  if (code === 'unsupported_grant_type') {
297
- return new Response('...', { status, headers })
298
+ return new Response('...', { status, headers });
298
299
  }
299
300
  // returning undefined (i.e. void) uses the default Response generation
300
- }
301
- })
301
+ },
302
+ });
302
303
  ```
303
304
 
304
305
  By default, the `onError` callback is set to ``({ status, code, description }) => console.warn(`OAuth error response: ${status} ${code} - ${description}`)``.
@@ -309,8 +310,8 @@ By default, the `onError` callback is set to ``({ status, code, description }) =
309
310
 
310
311
  This library stores records about authorization tokens in KV. The storage schema is carefully designed such that a complete leak of the storage only reveals mundane metadata about what has been granted. In particular:
311
312
 
312
- * Secrets (including access tokens, refresh tokens, authorization codes, and client secrets) are stored only by hash. Hence, such secrets cannot be derived from the storage alone.
313
- * 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.
313
+ - Secrets (including access tokens, refresh tokens, authorization codes, and client secrets) are stored only by hash. Hence, such secrets cannot be derived from the storage alone.
314
+ - 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.
314
315
 
315
316
  Note that the `userId` and the `metadata` associated with each grant are not encrypted, because the purpose of these values is to allow grants to be enumerated for audit and revocation purposes. However, these values are completely opaque to the library. An application is free to omit them or apply its own encryption to them before passing them into the library, if it desires.
316
317
 
@@ -322,6 +323,24 @@ This requirement is seemingly fundamentally flawed as it assumes that every refr
322
323
 
323
324
  This library implements a compromise: At any particular time, a grant may have two valid refresh tokens. When the client uses one of them, the other one is invalidated, and a new one is generated and returned. Thus, if the client correctly uses the new refresh token each time, then older refresh tokens are continuously invalidated. But if a transient failure prevents the client from updating its token, it can always retry the request with the token it used previously.
324
325
 
326
+ ## Client ID Metadata Document (CIMD) Support
327
+
328
+ This library supports [Client ID Metadata Documents](https://www.ietf.org/archive/id/draft-parecki-oauth-client-id-metadata-document-03.html), which allow clients to use HTTPS URLs as their `client_id`. When a client presents an HTTPS URL with a non-root path as its `client_id`, the library will fetch and validate the metadata document from that URL.
329
+
330
+ ### Enabling CIMD
331
+
332
+ To enable CIMD support, you must add the `global_fetch_strictly_public` compatibility flag to your `wrangler.jsonc`:
333
+
334
+ ```jsonc
335
+ {
336
+ "compatibility_flags": ["global_fetch_strictly_public"],
337
+ }
338
+ ```
339
+
340
+ This flag is required for SSRF (Server-Side Request Forgery) protection. Due to a legacy quirk, `fetch()` requests to URLs within your zone's domain are sent directly to the origin server, bypassing Cloudflare. The `global_fetch_strictly_public` flag disables this behavior. See [Cloudflare's blog post](https://blog.cloudflare.com/workers-environment-live-object-bindings/) and [documentation](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#global-fetch-strictly-public) for more details.
341
+
342
+ When this flag is not enabled, the OAuth metadata endpoint will report `client_id_metadata_document_supported: false` and MCP Clients should use DCR instead.
343
+
325
344
  ## Written using Claude
326
345
 
327
346
  This library (including the schema documentation) was largely written with the help of [Claude](https://claude.ai), the AI model by Anthropic. Claude's output was thoroughly reviewed by Cloudflare engineers with careful attention paid to security and compliance with standards. Many improvements were made on the initial output, mostly again by prompting Claude (and reviewing the results). Check out the commit history to see how Claude was prompted and what code it produced.
@@ -332,6 +351,6 @@ This library (including the schema documentation) was largely written with the h
332
351
 
333
352
  In all seriousness, two months ago (January 2025), I ([@kentonv](https://github.com/kentonv)) would have agreed. I was an AI skeptic. I thought LLMs were glorified Markov chain generators that didn't actually understand code and couldn't produce anything novel. I started this project on a lark, fully expecting the AI to produce terrible code for me to laugh at. And then, uh... the code actually looked pretty good. Not perfect, but I just told the AI to fix things, and it did. I was shocked.
334
353
 
335
- To emphasize, **this is not "vibe coded"**. Every line was thoroughly reviewed and cross-referenced with relevant RFCs, by security experts with previous experience with those RFCs. I was *trying* to validate my skepticism. I ended up proving myself wrong.
354
+ To emphasize, **this is not "vibe coded"**. Every line was thoroughly reviewed and cross-referenced with relevant RFCs, by security experts with previous experience with those RFCs. I was _trying_ to validate my skepticism. I ended up proving myself wrong.
336
355
 
337
356
  Again, please check out the commit history -- especially early commits -- to understand how this went.