@cloudflare/workers-oauth-provider 0.1.0 → 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 +65 -46
- package/dist/oauth-provider.d.ts +563 -527
- package/dist/oauth-provider.js +1543 -1659
- package/package.json +11 -11
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
20
|
-
import { WorkerEntrypoint } from
|
|
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
|
-
|
|
33
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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: [
|
|
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 ==
|
|
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:
|
|
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:
|
|
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: [
|
|
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:
|
|
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(
|
|
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 ==
|
|
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(
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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
|
|
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.
|