@cloudflare/workers-oauth-provider 0.0.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/LICENSE.txt +21 -0
- package/README.md +222 -0
- package/dist/oauth-provider.d.ts +459 -0
- package/dist/oauth-provider.js +1339 -0
- package/package.json +29 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Cloudflare, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# OAuth 2.1 Provider Framework for Cloudflare Workers
|
|
2
|
+
|
|
3
|
+
This is a TypeScript library that implements the provider side of the OAuth 2.1 protocol with PKCE support. The library is intended to be used on Cloudflare Workers.
|
|
4
|
+
|
|
5
|
+
## EXPERIMENTAL
|
|
6
|
+
|
|
7
|
+
As of March, 2025, this library is very new. Please use with caution as additional security reviews are still in progress.
|
|
8
|
+
|
|
9
|
+
## Benefits of this library
|
|
10
|
+
|
|
11
|
+
* The library acts as a wrapper around your Worker code, which adds authorization for your API endpoints.
|
|
12
|
+
* All token management is handled automatically.
|
|
13
|
+
* 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.
|
|
14
|
+
* The library is agnostic to how you manage and authenticate users.
|
|
15
|
+
* 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.
|
|
16
|
+
* The library's storage does not store any secrets, only hashes of them.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
A Worker that uses the library might look like this:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { OAuthProvider } from "my-oauth";
|
|
24
|
+
import type { ExportedHandler } from "@cloudflare/workers-types";
|
|
25
|
+
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
26
|
+
|
|
27
|
+
// We export the OAuthProvider instance as the entrypoint to our Worker. This means it
|
|
28
|
+
// implements the `fetch()` handler, receiving all HTTP requests.
|
|
29
|
+
export default new OAuthProvider({
|
|
30
|
+
// Configure API routes. Any requests whose URL starts with any of these prefixes will be
|
|
31
|
+
// considered API requests. The OAuth provider will check the access token on these requests,
|
|
32
|
+
// and then, if the token is valid, send the request to the API handler.
|
|
33
|
+
// You can provide:
|
|
34
|
+
// - A single route (string) or multiple routes (array)
|
|
35
|
+
// - Full URLs (which will match the hostname) or just paths (which will match any hostname)
|
|
36
|
+
apiRoute: [
|
|
37
|
+
"/api/", // Path only - will match any hostname
|
|
38
|
+
"https://api.example.com/" // Full URL - will check hostname
|
|
39
|
+
],
|
|
40
|
+
|
|
41
|
+
// When the OAuth system receives an API request with a valid access token, it passes the request
|
|
42
|
+
// to this handler object's fetch method.
|
|
43
|
+
// You can provide either an object with a fetch method (ExportedHandler)
|
|
44
|
+
// or a class extending WorkerEntrypoint.
|
|
45
|
+
apiHandler: ApiHandler, // Using a WorkerEntrypoint class
|
|
46
|
+
|
|
47
|
+
// Any requests which aren't API request will be passed to the default handler instead.
|
|
48
|
+
// Again, this can be either an object or a WorkerEntrypoint.
|
|
49
|
+
defaultHandler: defaultHandler, // Using an object with a fetch method
|
|
50
|
+
|
|
51
|
+
// This specifies the URL of the OAuth authorization flow UI. This UI is NOT implemented by
|
|
52
|
+
// the OAuthProvider. It is up to the application to implement a UI here. The only reason why
|
|
53
|
+
// this URL is given to the OAuthProvider is so that it can implement the RFC-8414 metadata
|
|
54
|
+
// discovery endpoint, i.e. `.well-known/oauth-authorization-server`.
|
|
55
|
+
// Can also be specified as just a path (e.g., "/authorize").
|
|
56
|
+
authorizeEndpoint: "https://example.com/authorize",
|
|
57
|
+
|
|
58
|
+
// This specifies the OAuth 2 token exchange endpoint. The OAuthProvider will implement this
|
|
59
|
+
// endpoint (by directly responding to requests with a matching URL).
|
|
60
|
+
// Can also be specified as just a path (e.g., "/oauth/token").
|
|
61
|
+
tokenEndpoint: "https://example.com/oauth/token",
|
|
62
|
+
|
|
63
|
+
// This specifies the RFC-7591 dynamic client registration endpoint. This setting is optional,
|
|
64
|
+
// but if provided, the OAuthProvider will implement this endpoint to allow dynamic client
|
|
65
|
+
// registration.
|
|
66
|
+
// Can also be specified as just a path (e.g., "/oauth/register").
|
|
67
|
+
clientRegistrationEndpoint: "https://example.com/oauth/register",
|
|
68
|
+
|
|
69
|
+
// Optional list of scopes supported by this OAuth provider.
|
|
70
|
+
// If provided, this will be included in the RFC 8414 metadata as 'scopes_supported'.
|
|
71
|
+
// If not provided, the 'scopes_supported' field will be omitted from the metadata.
|
|
72
|
+
scopesSupported: ["document.read", "document.write", "profile"],
|
|
73
|
+
|
|
74
|
+
// Optional: Controls whether the OAuth implicit flow is allowed.
|
|
75
|
+
// The implicit flow is discouraged in OAuth 2.1 but may be needed for some clients.
|
|
76
|
+
// Defaults to false.
|
|
77
|
+
allowImplicitFlow: false,
|
|
78
|
+
|
|
79
|
+
// Optional: Controls whether public clients (clients without a secret, like SPAs)
|
|
80
|
+
// can register via the dynamic client registration endpoint.
|
|
81
|
+
// When true, only confidential clients can register.
|
|
82
|
+
// Note: Creating public clients via the OAuthHelpers.createClient() method
|
|
83
|
+
// is always allowed regardless of this setting.
|
|
84
|
+
// Defaults to false.
|
|
85
|
+
disallowPublicClientRegistration: false
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// The default handler object - the OAuthProvider will pass through HTTP requests to this object's fetch method
|
|
89
|
+
// if they aren't API requests or do not have a valid access token
|
|
90
|
+
const defaultHandler = {
|
|
91
|
+
// This fetch method works just like a standard Cloudflare Workers fetch handler
|
|
92
|
+
//
|
|
93
|
+
// The `request`, `env`, and `ctx` parameters are the same as for a normal Cloudflare Workers fetch
|
|
94
|
+
// handler, and are exactly the objects that the `OAuthProvider` itself received from the Workers
|
|
95
|
+
// runtime.
|
|
96
|
+
//
|
|
97
|
+
// The `env.OAUTH_PROVIDER` provides an API by which the application can call back to the
|
|
98
|
+
// OAuthProvider.
|
|
99
|
+
async fetch(request: Request, env, ctx) {
|
|
100
|
+
let url = new URL(request.url);
|
|
101
|
+
|
|
102
|
+
if (url.pathname == "/authorize") {
|
|
103
|
+
// This is a request for our OAuth authorization flow UI. It is up to the application to
|
|
104
|
+
// implement this. However, the OAuthProvider library provides some helpers to assist.
|
|
105
|
+
|
|
106
|
+
// `env.OAUTH_PROVIDER.parseAuthRequest()` parses the OAuth authorization request to extract the parameters
|
|
107
|
+
// required by the OAuth 2 standard, namely response_type, client_id, redirect_uri, scope, and
|
|
108
|
+
// state. It returns an object containing all these (using idiomatic camelCase naming).
|
|
109
|
+
let oauthReqInfo = await env.OAUTH_PROVIDER.parseAuthRequest(request);
|
|
110
|
+
|
|
111
|
+
// `env.OAUTH_PROVIDER.lookupClient()` looks up metadata about the client, as definetd by RFC-7591. This
|
|
112
|
+
// includes things like redirect_uris, client_name, logo_uri, etc.
|
|
113
|
+
let clientInfo = await env.OAUTH_PROVIDER.lookupClient(oauthReqInfo.clientId);
|
|
114
|
+
|
|
115
|
+
// At this point, the application should use `oauthReqInfo` and `clientInfo` to render an
|
|
116
|
+
// authorization consent UI to the user. The details of this are up to the app so are not
|
|
117
|
+
// shown here.
|
|
118
|
+
|
|
119
|
+
// After the user has granted consent, the application calls `env.OAUTH_PROVIDER.completeAuthorization()` to
|
|
120
|
+
// grant the authorization.
|
|
121
|
+
let {redirectTo} = await env.OAUTH_PROVIDER.completeAuthorization({
|
|
122
|
+
// The application passes back the original OAuth request info that was returned by
|
|
123
|
+
// `parseAuthRequest()` earlier.
|
|
124
|
+
request: oauthReqInfo,
|
|
125
|
+
|
|
126
|
+
// The application must specify the user's ID, which is some sort of string. This is needed
|
|
127
|
+
// so that the application can later query the OAuthProvider to enumerate all grants
|
|
128
|
+
// belonging to a particular user, e.g. to implement an audit and revocation UI.
|
|
129
|
+
userId: "1234",
|
|
130
|
+
|
|
131
|
+
// The application can specify some arbitary metadata which describes this grant. The
|
|
132
|
+
// metadata can contain any JSON-serializable content. This metadata is not used by the
|
|
133
|
+
// OAuthProvider, but the application can read back the metadata attached to specific
|
|
134
|
+
// grants when enumerating them later, again e.g. to implement an udit and revocation UI.
|
|
135
|
+
metadata: {label: "foo"},
|
|
136
|
+
|
|
137
|
+
// The application specifies the list of OAuth scope identifiers that were granted. This
|
|
138
|
+
// may or may not be the same as was requested in `oauthReqInfo.scope`.
|
|
139
|
+
scope: ["document.read", "document.write"],
|
|
140
|
+
|
|
141
|
+
// `props` is an arbitrary JSON-serializable object which will be passed back to the API
|
|
142
|
+
// handler for every request authorized by this grant.
|
|
143
|
+
props: {
|
|
144
|
+
userId: 1234,
|
|
145
|
+
username: "Bob"
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// `completeAuthorization()` will have returned the URL to which the user should be redirected
|
|
150
|
+
// in order to complete the authorization flow. This is the requesting client's OAuth
|
|
151
|
+
// redirect_uri with the appropriate query parameters added to complete the flow and obtain
|
|
152
|
+
// tokens.
|
|
153
|
+
return Response.redirect(redirectTo, 302);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ... the application can implement other non-API HTTP endpoints here ...
|
|
157
|
+
|
|
158
|
+
return new Response("Not found", {status: 404});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// The API handler object - the OAuthProivder will pass authorized API requests to this object's fetch method
|
|
163
|
+
// (because we provided it as the `apiHandler` setting, above). This is ONLY called for API requests
|
|
164
|
+
// that had a valid access token.
|
|
165
|
+
class ApiHandler extends WorkerEntrypoint {
|
|
166
|
+
// This fetch method works just like any other WorkerEntrypoint fetch method. The `request` is
|
|
167
|
+
// passed as a parameter, while `env` and `ctx` are available as `this.env` and `this.ctx`.
|
|
168
|
+
//
|
|
169
|
+
// The `this.env.OAUTH_PROVIDER` is available just like in the default handler.
|
|
170
|
+
//
|
|
171
|
+
// The `this.ctx.props` property contains the `props` value that was passed to
|
|
172
|
+
// `env.OAUTH_PROVIDER.completeAuthorization()` during the authorization flow that authorized this client.
|
|
173
|
+
fetch(request: Request) {
|
|
174
|
+
// The application can implement its API endpoints like normal. This app implements a single
|
|
175
|
+
// endpoint, `/api/whoami`, which returns the user's authenticated identity.
|
|
176
|
+
|
|
177
|
+
let url = new URL(request.url);
|
|
178
|
+
if (url.pathname == "/api/whoami") {
|
|
179
|
+
// Since the username is embedded in `ctx.props`, which came from the access token that the
|
|
180
|
+
// OAuthProivder already verified, we don't need to do any other authentication steps.
|
|
181
|
+
return new Response(`You are authenticated as: ${this.ctx.props.username}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return new Response("Not found", {status: 404});
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
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.
|
|
190
|
+
|
|
191
|
+
The `env.OAUTH_PROVIDER` object available to the fetch handlers provides some methods to query the storage, including:
|
|
192
|
+
|
|
193
|
+
* Create, list, modify, and delete client_id registrations (in addition to `lookupClient()`, already shown in the example code).
|
|
194
|
+
* List all active authorization grants for a particular user.
|
|
195
|
+
* Revoke (delete) an authorization grant.
|
|
196
|
+
|
|
197
|
+
See the `OAuthHelpers` interface definition for full API details.
|
|
198
|
+
|
|
199
|
+
## Written by Claude
|
|
200
|
+
|
|
201
|
+
This library (including the schema documentation) was largely written by [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.
|
|
202
|
+
|
|
203
|
+
(@kentonv, the lead engineer, was actually an AI skeptic, and started this project with the intent to prove that LLMs cannot code. He ended up deciding he had proven himself wrong.)
|
|
204
|
+
|
|
205
|
+
## Implementation Notes
|
|
206
|
+
|
|
207
|
+
### End-to-end encryption
|
|
208
|
+
|
|
209
|
+
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:
|
|
210
|
+
|
|
211
|
+
* 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.
|
|
212
|
+
* 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.
|
|
213
|
+
|
|
214
|
+
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.
|
|
215
|
+
|
|
216
|
+
### Single-use refresh tokens?
|
|
217
|
+
|
|
218
|
+
OAuth 2.1 requires that refresh tokens are either "cryptographically bound" to the client, or are single-use. This library currently does not implement any cryptographic binding, thus seemingly requiring single-use tokens. Under this requirement, every token refresh request invalidates the old refresh token and issues a new one.
|
|
219
|
+
|
|
220
|
+
This requirement is seemingly fundamentally flawed as it assumes that every refresh request will complete with no errors. In the real world, a transient network error, machine failure, or software fault could mean that the client fails to store the new refresh token after a refresh request. In this case, the client would be permanently unable to make any further requests, as the only token it has is no longer valid.
|
|
221
|
+
|
|
222
|
+
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.
|