@cloudflare/workers-oauth-provider 0.0.2 → 0.0.4-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -5
- package/dist/oauth-provider.d.ts +31 -2
- package/dist/oauth-provider.js +114 -62
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
-
##
|
|
5
|
+
## Beta
|
|
6
6
|
|
|
7
|
-
As of March, 2025, this library is very new
|
|
7
|
+
As of March, 2025, this library is very new, prerelease software. The API is still subject to change.
|
|
8
8
|
|
|
9
9
|
## Benefits of this library
|
|
10
10
|
|
|
@@ -43,6 +43,16 @@ export default new OAuthProvider({
|
|
|
43
43
|
// You can provide either an object with a fetch method (ExportedHandler)
|
|
44
44
|
// or a class extending WorkerEntrypoint.
|
|
45
45
|
apiHandler: ApiHandler, // Using a WorkerEntrypoint class
|
|
46
|
+
|
|
47
|
+
// For multi-handler setups, you can use apiHandlers instead of apiRoute+apiHandler.
|
|
48
|
+
// This allows you to use different handlers for different API routes.
|
|
49
|
+
// Note: You must use either apiRoute+apiHandler (single-handler) OR apiHandlers (multi-handler), not both.
|
|
50
|
+
// Example:
|
|
51
|
+
// apiHandlers: {
|
|
52
|
+
// "/api/users/": UsersApiHandler,
|
|
53
|
+
// "/api/documents/": DocumentsApiHandler,
|
|
54
|
+
// "https://api.example.com/": ExternalApiHandler,
|
|
55
|
+
// },
|
|
46
56
|
|
|
47
57
|
// Any requests which aren't API request will be passed to the default handler instead.
|
|
48
58
|
// Again, this can be either an object or a WorkerEntrypoint.
|
|
@@ -262,11 +272,34 @@ The `accessTokenTTL` override is particularly useful when the application is als
|
|
|
262
272
|
|
|
263
273
|
The `props` values are end-to-end encrypted, so they can safely contain sensitive information.
|
|
264
274
|
|
|
265
|
-
##
|
|
275
|
+
## Custom Error Responses
|
|
266
276
|
|
|
267
|
-
|
|
277
|
+
By using the `onError` option, you can emit notifications or take other actions when an error response was to be emitted:
|
|
268
278
|
|
|
269
|
-
|
|
279
|
+
```ts
|
|
280
|
+
new OAuthProvider({
|
|
281
|
+
// ... other options ...
|
|
282
|
+
onError({ code, description, status, headers }) {
|
|
283
|
+
Sentry.captureMessage(/* ... */)
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
By returning a `Response` you can also override what the OAuthProvider returns to your users:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
new OAuthProvider({
|
|
292
|
+
// ... other options ...
|
|
293
|
+
onError({ code, description, status, headers }) {
|
|
294
|
+
if (code === 'unsupported_grant_type') {
|
|
295
|
+
return new Response('...', { status, headers })
|
|
296
|
+
}
|
|
297
|
+
// returning undefined (i.e. void) uses the default Response generation
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
By default, the `onError` callback is set to ``({ status, code, description }) => console.warn(`OAuth error response: ${status} ${code} - ${description}`)``.
|
|
270
303
|
|
|
271
304
|
## Implementation Notes
|
|
272
305
|
|
|
@@ -286,3 +319,17 @@ OAuth 2.1 requires that refresh tokens are either "cryptographically bound" to t
|
|
|
286
319
|
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.
|
|
287
320
|
|
|
288
321
|
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.
|
|
322
|
+
|
|
323
|
+
## Written using Claude
|
|
324
|
+
|
|
325
|
+
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.
|
|
326
|
+
|
|
327
|
+
**"NOOOOOOOO!!!! You can't just use an LLM to write an auth library!"**
|
|
328
|
+
|
|
329
|
+
"haha gpus go brrr"
|
|
330
|
+
|
|
331
|
+
In all seriousness, two months ago (January 2025), I ([@kentonv](https://github.com/kentonv)) would have agreed. I was an AI skeptic. I thoughts 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.
|
|
332
|
+
|
|
333
|
+
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.
|
|
334
|
+
|
|
335
|
+
Again, please check out the commit history -- especially early commits -- to understand how this went.
|
package/dist/oauth-provider.d.ts
CHANGED
|
@@ -67,14 +67,31 @@ interface OAuthProviderOptions {
|
|
|
67
67
|
* URL(s) for API routes. Requests with URLs starting with any of these prefixes
|
|
68
68
|
* will be treated as API requests and require a valid access token.
|
|
69
69
|
* Can be a single route or an array of routes. Each route can be a full URL or just a path.
|
|
70
|
+
*
|
|
71
|
+
* Used with `apiHandler` for the single-handler configuration. This is incompatible with
|
|
72
|
+
* the `apiHandlers` property. You must use either `apiRoute` + `apiHandler` OR `apiHandlers`, not both.
|
|
70
73
|
*/
|
|
71
|
-
apiRoute
|
|
74
|
+
apiRoute?: string | string[];
|
|
72
75
|
/**
|
|
73
76
|
* Handler for API requests that have a valid access token.
|
|
74
77
|
* This handler will receive the authenticated user properties in ctx.props.
|
|
75
78
|
* Can be either an ExportedHandler object with a fetch method or a class extending WorkerEntrypoint.
|
|
79
|
+
*
|
|
80
|
+
* Used with `apiRoute` for the single-handler configuration. This is incompatible with
|
|
81
|
+
* the `apiHandlers` property. You must use either `apiRoute` + `apiHandler` OR `apiHandlers`, not both.
|
|
76
82
|
*/
|
|
77
|
-
apiHandler
|
|
83
|
+
apiHandler?: ExportedHandlerWithFetch | (new (ctx: ExecutionContext, env: any) => WorkerEntrypointWithFetch);
|
|
84
|
+
/**
|
|
85
|
+
* Map of API routes to their corresponding handlers for the multi-handler configuration.
|
|
86
|
+
* The keys are the API routes (strings only, not arrays), and the values are the handlers.
|
|
87
|
+
* Each route can be a full URL or just a path, and each handler can be either an ExportedHandler
|
|
88
|
+
* object with a fetch method or a class extending WorkerEntrypoint.
|
|
89
|
+
*
|
|
90
|
+
* This is incompatible with the `apiRoute` and `apiHandler` properties. You must use either
|
|
91
|
+
* `apiRoute` + `apiHandler` (single-handler configuration) OR `apiHandlers` (multi-handler
|
|
92
|
+
* configuration), not both.
|
|
93
|
+
*/
|
|
94
|
+
apiHandlers?: Record<string, ExportedHandlerWithFetch | (new (ctx: ExecutionContext, env: any) => WorkerEntrypointWithFetch)>;
|
|
78
95
|
/**
|
|
79
96
|
* Handler for all non-API requests or API requests without a valid token.
|
|
80
97
|
* Can be either an ExportedHandler object with a fetch method or a class extending WorkerEntrypoint.
|
|
@@ -128,6 +145,18 @@ interface OAuthProviderOptions {
|
|
|
128
145
|
* If the callback returns nothing or undefined for a props field, the original props will be used.
|
|
129
146
|
*/
|
|
130
147
|
tokenExchangeCallback?: (options: TokenExchangeCallbackOptions) => Promise<TokenExchangeCallbackResult | void> | TokenExchangeCallbackResult | void;
|
|
148
|
+
/**
|
|
149
|
+
* Optional callback function that is called whenever the OAuthProvider returns an error response
|
|
150
|
+
* This allows the client to emit notifications or perform other actions when an error occurs.
|
|
151
|
+
*
|
|
152
|
+
* If the function returns a Response, that will be used in place of the OAuthProvider's default one.
|
|
153
|
+
*/
|
|
154
|
+
onError?: (error: {
|
|
155
|
+
code: string;
|
|
156
|
+
description: string;
|
|
157
|
+
status: number;
|
|
158
|
+
headers: Record<string, string>;
|
|
159
|
+
}) => Response | void;
|
|
131
160
|
}
|
|
132
161
|
/**
|
|
133
162
|
* Helper methods for OAuth operations provided to handler functions
|
package/dist/oauth-provider.js
CHANGED
|
@@ -37,14 +37,36 @@ var OAuthProviderImpl = class {
|
|
|
37
37
|
* @param options - Configuration options for the provider
|
|
38
38
|
*/
|
|
39
39
|
constructor(options) {
|
|
40
|
-
this.
|
|
40
|
+
this.typedApiHandlers = /* @__PURE__ */ new Map();
|
|
41
|
+
const hasSingleHandlerConfig = !!(options.apiRoute && options.apiHandler);
|
|
42
|
+
const hasMultiHandlerConfig = !!options.apiHandlers;
|
|
43
|
+
if (hasSingleHandlerConfig && hasMultiHandlerConfig) {
|
|
44
|
+
throw new TypeError(
|
|
45
|
+
"Cannot use both apiRoute/apiHandler and apiHandlers. Use either apiRoute + apiHandler OR apiHandlers, not both."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (!hasSingleHandlerConfig && !hasMultiHandlerConfig) {
|
|
49
|
+
throw new TypeError(
|
|
50
|
+
"Must provide either apiRoute + apiHandler OR apiHandlers. No API route configuration provided."
|
|
51
|
+
);
|
|
52
|
+
}
|
|
41
53
|
this.typedDefaultHandler = this.validateHandler(options.defaultHandler, "defaultHandler");
|
|
42
|
-
if (
|
|
43
|
-
options.
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
if (hasSingleHandlerConfig) {
|
|
55
|
+
const apiHandler = this.validateHandler(options.apiHandler, "apiHandler");
|
|
56
|
+
if (Array.isArray(options.apiRoute)) {
|
|
57
|
+
options.apiRoute.forEach((route, index) => {
|
|
58
|
+
this.validateEndpoint(route, `apiRoute[${index}]`);
|
|
59
|
+
this.typedApiHandlers.set(route, apiHandler);
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
this.validateEndpoint(options.apiRoute, "apiRoute");
|
|
63
|
+
this.typedApiHandlers.set(options.apiRoute, apiHandler);
|
|
64
|
+
}
|
|
46
65
|
} else {
|
|
47
|
-
|
|
66
|
+
for (const [route, handler] of Object.entries(options.apiHandlers)) {
|
|
67
|
+
this.validateEndpoint(route, `apiHandlers key: ${route}`);
|
|
68
|
+
this.typedApiHandlers.set(route, this.validateHandler(handler, `apiHandlers[${route}]`));
|
|
69
|
+
}
|
|
48
70
|
}
|
|
49
71
|
this.validateEndpoint(options.authorizeEndpoint, "authorizeEndpoint");
|
|
50
72
|
this.validateEndpoint(options.tokenEndpoint, "tokenEndpoint");
|
|
@@ -52,8 +74,9 @@ var OAuthProviderImpl = class {
|
|
|
52
74
|
this.validateEndpoint(options.clientRegistrationEndpoint, "clientRegistrationEndpoint");
|
|
53
75
|
}
|
|
54
76
|
this.options = {
|
|
55
|
-
|
|
56
|
-
|
|
77
|
+
accessTokenTTL: DEFAULT_ACCESS_TOKEN_TTL,
|
|
78
|
+
onError: ({ status, code, description }) => console.warn(`OAuth error response: ${status} ${code} - ${description}`),
|
|
79
|
+
...options
|
|
57
80
|
};
|
|
58
81
|
}
|
|
59
82
|
/**
|
|
@@ -203,11 +226,25 @@ var OAuthProviderImpl = class {
|
|
|
203
226
|
* @returns True if the URL matches any of the API routes
|
|
204
227
|
*/
|
|
205
228
|
isApiRequest(url) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
229
|
+
for (const route of this.typedApiHandlers.keys()) {
|
|
230
|
+
if (this.matchApiRoute(url, route)) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Finds the appropriate API handler for a URL
|
|
238
|
+
* @param url - The URL to find a handler for
|
|
239
|
+
* @returns The TypedHandler for the URL, or undefined if no handler matches
|
|
240
|
+
*/
|
|
241
|
+
findApiHandlerForUrl(url) {
|
|
242
|
+
for (const [route, handler] of this.typedApiHandlers.entries()) {
|
|
243
|
+
if (this.matchApiRoute(url, route)) {
|
|
244
|
+
return handler;
|
|
245
|
+
}
|
|
210
246
|
}
|
|
247
|
+
return void 0;
|
|
211
248
|
}
|
|
212
249
|
/**
|
|
213
250
|
* Gets the full URL for an endpoint, using the provided request URL's
|
|
@@ -298,12 +335,12 @@ var OAuthProviderImpl = class {
|
|
|
298
335
|
*/
|
|
299
336
|
async handleTokenRequest(request, env) {
|
|
300
337
|
if (request.method !== "POST") {
|
|
301
|
-
return createErrorResponse("invalid_request", "Method not allowed", 405);
|
|
338
|
+
return this.createErrorResponse("invalid_request", "Method not allowed", 405);
|
|
302
339
|
}
|
|
303
340
|
let contentType = request.headers.get("Content-Type") || "";
|
|
304
341
|
let body = {};
|
|
305
342
|
if (!contentType.includes("application/x-www-form-urlencoded")) {
|
|
306
|
-
return createErrorResponse("invalid_request", "Content-Type must be application/x-www-form-urlencoded", 400);
|
|
343
|
+
return this.createErrorResponse("invalid_request", "Content-Type must be application/x-www-form-urlencoded", 400);
|
|
307
344
|
}
|
|
308
345
|
const formData = await request.formData();
|
|
309
346
|
for (const [key, value] of formData.entries()) {
|
|
@@ -322,19 +359,19 @@ var OAuthProviderImpl = class {
|
|
|
322
359
|
clientSecret = body.client_secret || "";
|
|
323
360
|
}
|
|
324
361
|
if (!clientId) {
|
|
325
|
-
return createErrorResponse("invalid_client", "Client ID is required", 401);
|
|
362
|
+
return this.createErrorResponse("invalid_client", "Client ID is required", 401);
|
|
326
363
|
}
|
|
327
364
|
const clientInfo = await this.getClient(env, clientId);
|
|
328
365
|
if (!clientInfo) {
|
|
329
|
-
return createErrorResponse("invalid_client", "Client not found", 401);
|
|
366
|
+
return this.createErrorResponse("invalid_client", "Client not found", 401);
|
|
330
367
|
}
|
|
331
368
|
const isPublicClient = clientInfo.tokenEndpointAuthMethod === "none";
|
|
332
369
|
if (!isPublicClient) {
|
|
333
370
|
if (!clientSecret) {
|
|
334
|
-
return createErrorResponse("invalid_client", "Client authentication failed: missing client_secret", 401);
|
|
371
|
+
return this.createErrorResponse("invalid_client", "Client authentication failed: missing client_secret", 401);
|
|
335
372
|
}
|
|
336
373
|
if (!clientInfo.clientSecret) {
|
|
337
|
-
return createErrorResponse(
|
|
374
|
+
return this.createErrorResponse(
|
|
338
375
|
"invalid_client",
|
|
339
376
|
"Client authentication failed: client has no registered secret",
|
|
340
377
|
401
|
|
@@ -342,7 +379,7 @@ var OAuthProviderImpl = class {
|
|
|
342
379
|
}
|
|
343
380
|
const providedSecretHash = await hashSecret(clientSecret);
|
|
344
381
|
if (providedSecretHash !== clientInfo.clientSecret) {
|
|
345
|
-
return createErrorResponse("invalid_client", "Client authentication failed: invalid client_secret", 401);
|
|
382
|
+
return this.createErrorResponse("invalid_client", "Client authentication failed: invalid client_secret", 401);
|
|
346
383
|
}
|
|
347
384
|
}
|
|
348
385
|
const grantType = body.grant_type;
|
|
@@ -351,7 +388,7 @@ var OAuthProviderImpl = class {
|
|
|
351
388
|
} else if (grantType === "refresh_token") {
|
|
352
389
|
return this.handleRefreshTokenGrant(body, clientInfo, env);
|
|
353
390
|
} else {
|
|
354
|
-
return createErrorResponse("unsupported_grant_type", "Grant type not supported");
|
|
391
|
+
return this.createErrorResponse("unsupported_grant_type", "Grant type not supported");
|
|
355
392
|
}
|
|
356
393
|
}
|
|
357
394
|
/**
|
|
@@ -367,38 +404,38 @@ var OAuthProviderImpl = class {
|
|
|
367
404
|
const redirectUri = body.redirect_uri;
|
|
368
405
|
const codeVerifier = body.code_verifier;
|
|
369
406
|
if (!code) {
|
|
370
|
-
return createErrorResponse("invalid_request", "Authorization code is required");
|
|
407
|
+
return this.createErrorResponse("invalid_request", "Authorization code is required");
|
|
371
408
|
}
|
|
372
409
|
const codeParts = code.split(":");
|
|
373
410
|
if (codeParts.length !== 3) {
|
|
374
|
-
return createErrorResponse("invalid_grant", "Invalid authorization code format");
|
|
411
|
+
return this.createErrorResponse("invalid_grant", "Invalid authorization code format");
|
|
375
412
|
}
|
|
376
413
|
const [userId, grantId, _] = codeParts;
|
|
377
414
|
const grantKey = `grant:${userId}:${grantId}`;
|
|
378
415
|
const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
|
|
379
416
|
if (!grantData) {
|
|
380
|
-
return createErrorResponse("invalid_grant", "Grant not found or authorization code expired");
|
|
417
|
+
return this.createErrorResponse("invalid_grant", "Grant not found or authorization code expired");
|
|
381
418
|
}
|
|
382
419
|
if (!grantData.authCodeId) {
|
|
383
|
-
return createErrorResponse("invalid_grant", "Authorization code already used");
|
|
420
|
+
return this.createErrorResponse("invalid_grant", "Authorization code already used");
|
|
384
421
|
}
|
|
385
422
|
const codeHash = await hashSecret(code);
|
|
386
423
|
if (codeHash !== grantData.authCodeId) {
|
|
387
|
-
return createErrorResponse("invalid_grant", "Invalid authorization code");
|
|
424
|
+
return this.createErrorResponse("invalid_grant", "Invalid authorization code");
|
|
388
425
|
}
|
|
389
426
|
if (grantData.clientId !== clientInfo.clientId) {
|
|
390
|
-
return createErrorResponse("invalid_grant", "Client ID mismatch");
|
|
427
|
+
return this.createErrorResponse("invalid_grant", "Client ID mismatch");
|
|
391
428
|
}
|
|
392
429
|
const isPkceEnabled = !!grantData.codeChallenge;
|
|
393
430
|
if (!redirectUri && !isPkceEnabled) {
|
|
394
|
-
return createErrorResponse("invalid_request", "redirect_uri is required when not using PKCE");
|
|
431
|
+
return this.createErrorResponse("invalid_request", "redirect_uri is required when not using PKCE");
|
|
395
432
|
}
|
|
396
433
|
if (redirectUri && !clientInfo.redirectUris.includes(redirectUri)) {
|
|
397
|
-
return createErrorResponse("invalid_grant", "Invalid redirect URI");
|
|
434
|
+
return this.createErrorResponse("invalid_grant", "Invalid redirect URI");
|
|
398
435
|
}
|
|
399
436
|
if (isPkceEnabled) {
|
|
400
437
|
if (!codeVerifier) {
|
|
401
|
-
return createErrorResponse("invalid_request", "code_verifier is required for PKCE");
|
|
438
|
+
return this.createErrorResponse("invalid_request", "code_verifier is required for PKCE");
|
|
402
439
|
}
|
|
403
440
|
let calculatedChallenge;
|
|
404
441
|
if (grantData.codeChallengeMethod === "S256") {
|
|
@@ -411,7 +448,7 @@ var OAuthProviderImpl = class {
|
|
|
411
448
|
calculatedChallenge = codeVerifier;
|
|
412
449
|
}
|
|
413
450
|
if (calculatedChallenge !== grantData.codeChallenge) {
|
|
414
|
-
return createErrorResponse("invalid_grant", "Invalid PKCE code_verifier");
|
|
451
|
+
return this.createErrorResponse("invalid_grant", "Invalid PKCE code_verifier");
|
|
415
452
|
}
|
|
416
453
|
}
|
|
417
454
|
const accessTokenSecret = generateRandomString(TOKEN_LENGTH);
|
|
@@ -516,26 +553,26 @@ var OAuthProviderImpl = class {
|
|
|
516
553
|
async handleRefreshTokenGrant(body, clientInfo, env) {
|
|
517
554
|
const refreshToken = body.refresh_token;
|
|
518
555
|
if (!refreshToken) {
|
|
519
|
-
return createErrorResponse("invalid_request", "Refresh token is required");
|
|
556
|
+
return this.createErrorResponse("invalid_request", "Refresh token is required");
|
|
520
557
|
}
|
|
521
558
|
const tokenParts = refreshToken.split(":");
|
|
522
559
|
if (tokenParts.length !== 3) {
|
|
523
|
-
return createErrorResponse("invalid_grant", "Invalid token format");
|
|
560
|
+
return this.createErrorResponse("invalid_grant", "Invalid token format");
|
|
524
561
|
}
|
|
525
562
|
const [userId, grantId, _] = tokenParts;
|
|
526
563
|
const providedTokenHash = await generateTokenId(refreshToken);
|
|
527
564
|
const grantKey = `grant:${userId}:${grantId}`;
|
|
528
565
|
const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
|
|
529
566
|
if (!grantData) {
|
|
530
|
-
return createErrorResponse("invalid_grant", "Grant not found");
|
|
567
|
+
return this.createErrorResponse("invalid_grant", "Grant not found");
|
|
531
568
|
}
|
|
532
569
|
const isCurrentToken = grantData.refreshTokenId === providedTokenHash;
|
|
533
570
|
const isPreviousToken = grantData.previousRefreshTokenId === providedTokenHash;
|
|
534
571
|
if (!isCurrentToken && !isPreviousToken) {
|
|
535
|
-
return createErrorResponse("invalid_grant", "Invalid refresh token");
|
|
572
|
+
return this.createErrorResponse("invalid_grant", "Invalid refresh token");
|
|
536
573
|
}
|
|
537
574
|
if (grantData.clientId !== clientInfo.clientId) {
|
|
538
|
-
return createErrorResponse("invalid_grant", "Client ID mismatch");
|
|
575
|
+
return this.createErrorResponse("invalid_grant", "Client ID mismatch");
|
|
539
576
|
}
|
|
540
577
|
const accessTokenSecret = generateRandomString(TOKEN_LENGTH);
|
|
541
578
|
const newAccessToken = `${userId}:${grantId}:${accessTokenSecret}`;
|
|
@@ -647,24 +684,24 @@ var OAuthProviderImpl = class {
|
|
|
647
684
|
*/
|
|
648
685
|
async handleClientRegistration(request, env) {
|
|
649
686
|
if (!this.options.clientRegistrationEndpoint) {
|
|
650
|
-
return createErrorResponse("not_implemented", "Client registration is not enabled", 501);
|
|
687
|
+
return this.createErrorResponse("not_implemented", "Client registration is not enabled", 501);
|
|
651
688
|
}
|
|
652
689
|
if (request.method !== "POST") {
|
|
653
|
-
return createErrorResponse("invalid_request", "Method not allowed", 405);
|
|
690
|
+
return this.createErrorResponse("invalid_request", "Method not allowed", 405);
|
|
654
691
|
}
|
|
655
692
|
const contentLength = parseInt(request.headers.get("Content-Length") || "0", 10);
|
|
656
693
|
if (contentLength > 1048576) {
|
|
657
|
-
return createErrorResponse("invalid_request", "Request payload too large, must be under 1 MiB", 413);
|
|
694
|
+
return this.createErrorResponse("invalid_request", "Request payload too large, must be under 1 MiB", 413);
|
|
658
695
|
}
|
|
659
696
|
let clientMetadata;
|
|
660
697
|
try {
|
|
661
698
|
const text = await request.text();
|
|
662
699
|
if (text.length > 1048576) {
|
|
663
|
-
return createErrorResponse("invalid_request", "Request payload too large, must be under 1 MiB", 413);
|
|
700
|
+
return this.createErrorResponse("invalid_request", "Request payload too large, must be under 1 MiB", 413);
|
|
664
701
|
}
|
|
665
702
|
clientMetadata = JSON.parse(text);
|
|
666
703
|
} catch (error) {
|
|
667
|
-
return createErrorResponse("invalid_request", "Invalid JSON payload", 400);
|
|
704
|
+
return this.createErrorResponse("invalid_request", "Invalid JSON payload", 400);
|
|
668
705
|
}
|
|
669
706
|
const validateStringField = (field) => {
|
|
670
707
|
if (field === void 0) {
|
|
@@ -692,7 +729,7 @@ var OAuthProviderImpl = class {
|
|
|
692
729
|
const authMethod = validateStringField(clientMetadata.token_endpoint_auth_method) || "client_secret_basic";
|
|
693
730
|
const isPublicClient = authMethod === "none";
|
|
694
731
|
if (isPublicClient && this.options.disallowPublicClientRegistration) {
|
|
695
|
-
return createErrorResponse("invalid_client_metadata", "Public client registration is not allowed");
|
|
732
|
+
return this.createErrorResponse("invalid_client_metadata", "Public client registration is not allowed");
|
|
696
733
|
}
|
|
697
734
|
const clientId = generateRandomString(16);
|
|
698
735
|
let clientSecret;
|
|
@@ -726,7 +763,7 @@ var OAuthProviderImpl = class {
|
|
|
726
763
|
clientInfo.clientSecret = hashedSecret;
|
|
727
764
|
}
|
|
728
765
|
} catch (error) {
|
|
729
|
-
return createErrorResponse(
|
|
766
|
+
return this.createErrorResponse(
|
|
730
767
|
"invalid_client_metadata",
|
|
731
768
|
error instanceof Error ? error.message : "Invalid client metadata"
|
|
732
769
|
);
|
|
@@ -766,14 +803,14 @@ var OAuthProviderImpl = class {
|
|
|
766
803
|
async handleApiRequest(request, env, ctx) {
|
|
767
804
|
const authHeader = request.headers.get("Authorization");
|
|
768
805
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
769
|
-
return createErrorResponse("invalid_token", "Missing or invalid access token", 401, {
|
|
806
|
+
return this.createErrorResponse("invalid_token", "Missing or invalid access token", 401, {
|
|
770
807
|
"WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token", error_description="Missing or invalid access token"'
|
|
771
808
|
});
|
|
772
809
|
}
|
|
773
810
|
const accessToken = authHeader.substring(7);
|
|
774
811
|
const tokenParts = accessToken.split(":");
|
|
775
812
|
if (tokenParts.length !== 3) {
|
|
776
|
-
return createErrorResponse("invalid_token", "Invalid token format", 401, {
|
|
813
|
+
return this.createErrorResponse("invalid_token", "Invalid token format", 401, {
|
|
777
814
|
"WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
|
|
778
815
|
});
|
|
779
816
|
}
|
|
@@ -782,13 +819,13 @@ var OAuthProviderImpl = class {
|
|
|
782
819
|
const tokenKey = `token:${userId}:${grantId}:${accessTokenId}`;
|
|
783
820
|
const tokenData = await env.OAUTH_KV.get(tokenKey, { type: "json" });
|
|
784
821
|
if (!tokenData) {
|
|
785
|
-
return createErrorResponse("invalid_token", "Invalid access token", 401, {
|
|
822
|
+
return this.createErrorResponse("invalid_token", "Invalid access token", 401, {
|
|
786
823
|
"WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
|
|
787
824
|
});
|
|
788
825
|
}
|
|
789
826
|
const now = Math.floor(Date.now() / 1e3);
|
|
790
827
|
if (tokenData.expiresAt < now) {
|
|
791
|
-
return createErrorResponse("invalid_token", "Access token expired", 401, {
|
|
828
|
+
return this.createErrorResponse("invalid_token", "Access token expired", 401, {
|
|
792
829
|
"WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
|
|
793
830
|
});
|
|
794
831
|
}
|
|
@@ -798,10 +835,15 @@ var OAuthProviderImpl = class {
|
|
|
798
835
|
if (!env.OAUTH_PROVIDER) {
|
|
799
836
|
env.OAUTH_PROVIDER = this.createOAuthHelpers(env);
|
|
800
837
|
}
|
|
801
|
-
|
|
802
|
-
|
|
838
|
+
const url = new URL(request.url);
|
|
839
|
+
const apiHandler = this.findApiHandlerForUrl(url);
|
|
840
|
+
if (!apiHandler) {
|
|
841
|
+
return this.createErrorResponse("invalid_request", "No handler found for API route", 404);
|
|
842
|
+
}
|
|
843
|
+
if (apiHandler.type === 0 /* EXPORTED_HANDLER */) {
|
|
844
|
+
return apiHandler.handler.fetch(request, env, ctx);
|
|
803
845
|
} else {
|
|
804
|
-
const handler = new
|
|
846
|
+
const handler = new apiHandler.handler(ctx, env);
|
|
805
847
|
return handler.fetch(request);
|
|
806
848
|
}
|
|
807
849
|
}
|
|
@@ -827,22 +869,32 @@ var OAuthProviderImpl = class {
|
|
|
827
869
|
const clientKey = `client:${clientId}`;
|
|
828
870
|
return env.OAUTH_KV.get(clientKey, { type: "json" });
|
|
829
871
|
}
|
|
872
|
+
/**
|
|
873
|
+
* Helper function to create OAuth error responses
|
|
874
|
+
* @param code - OAuth error code (e.g., 'invalid_request', 'invalid_token')
|
|
875
|
+
* @param description - Human-readable error description
|
|
876
|
+
* @param status - HTTP status code (default: 400)
|
|
877
|
+
* @param headers - Additional headers to include
|
|
878
|
+
* @returns A Response object with the error
|
|
879
|
+
*/
|
|
880
|
+
createErrorResponse(code, description, status = 400, headers = {}) {
|
|
881
|
+
const customErrorResponse = this.options.onError?.({ code, description, status, headers });
|
|
882
|
+
if (customErrorResponse) return customErrorResponse;
|
|
883
|
+
const body = JSON.stringify({
|
|
884
|
+
error: code,
|
|
885
|
+
error_description: description
|
|
886
|
+
});
|
|
887
|
+
return new Response(body, {
|
|
888
|
+
status,
|
|
889
|
+
headers: {
|
|
890
|
+
"Content-Type": "application/json",
|
|
891
|
+
...headers
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
}
|
|
830
895
|
};
|
|
831
896
|
var DEFAULT_ACCESS_TOKEN_TTL = 60 * 60;
|
|
832
897
|
var TOKEN_LENGTH = 32;
|
|
833
|
-
function createErrorResponse(code, description, status = 400, headers = {}) {
|
|
834
|
-
const body = JSON.stringify({
|
|
835
|
-
error: code,
|
|
836
|
-
error_description: description
|
|
837
|
-
});
|
|
838
|
-
return new Response(body, {
|
|
839
|
-
status,
|
|
840
|
-
headers: {
|
|
841
|
-
"Content-Type": "application/json",
|
|
842
|
-
...headers
|
|
843
|
-
}
|
|
844
|
-
});
|
|
845
|
-
}
|
|
846
898
|
async function hashSecret(secret) {
|
|
847
899
|
return generateTokenId(secret);
|
|
848
900
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudflare/workers-oauth-provider",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4-0",
|
|
4
4
|
"description": "OAuth provider for Cloudflare Workers",
|
|
5
5
|
"main": "dist/oauth-provider.js",
|
|
6
6
|
"types": "dist/oauth-provider.d.ts",
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsup",
|
|
18
|
+
"build:watch": "tsup --watch",
|
|
18
19
|
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
19
21
|
"prepublishOnly": "npm run build",
|
|
20
22
|
"prettier": "prettier -w ."
|
|
21
23
|
},
|