@cloudflare/workers-oauth-provider 0.0.1 → 0.0.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
@@ -196,6 +196,72 @@ The `env.OAUTH_PROVIDER` object available to the fetch handlers provides some me
196
196
 
197
197
  See the `OAuthHelpers` interface definition for full API details.
198
198
 
199
+ ## Token Exchange Callback
200
+
201
+ This library allows you to update the `props` value during token exchanges by configuring a callback function. This is useful for scenarios where the application needs to perform additional processing when tokens are issued or refreshed.
202
+
203
+ For example, if your application is also a client to some other OAuth API, you might want to perform an equivalent upstream token exchange and store the result in the `props`. The callback can be used to update the props for both the grant record and specific access tokens.
204
+
205
+ To use this feature, provide a `tokenExchangeCallback` in your OAuthProvider options:
206
+
207
+ ```ts
208
+ new OAuthProvider({
209
+ // ... other options ...
210
+ tokenExchangeCallback: async (options) => {
211
+ // options.grantType is either 'authorization_code' or 'refresh_token'
212
+ // options.props contains the current props
213
+ // options.clientId, options.userId, and options.scope are also available
214
+
215
+ if (options.grantType === 'authorization_code') {
216
+ // For authorization code exchange, might want to obtain upstream tokens
217
+ const upstreamTokens = await exchangeUpstreamToken(options.props.someCode);
218
+
219
+ return {
220
+ // Update the props stored in the access token
221
+ accessTokenProps: {
222
+ ...options.props,
223
+ upstreamAccessToken: upstreamTokens.access_token
224
+ },
225
+ // Update the props stored in the grant (for future token refreshes)
226
+ newProps: {
227
+ ...options.props,
228
+ upstreamRefreshToken: upstreamTokens.refresh_token
229
+ }
230
+ };
231
+ }
232
+
233
+ if (options.grantType === 'refresh_token') {
234
+ // For refresh token exchanges, might want to refresh upstream tokens too
235
+ const upstreamTokens = await refreshUpstreamToken(options.props.upstreamRefreshToken);
236
+
237
+ return {
238
+ accessTokenProps: {
239
+ ...options.props,
240
+ upstreamAccessToken: upstreamTokens.access_token
241
+ },
242
+ newProps: {
243
+ ...options.props,
244
+ upstreamRefreshToken: upstreamTokens.refresh_token || options.props.upstreamRefreshToken
245
+ },
246
+ // Optionally override the default access token TTL to match the upstream token
247
+ accessTokenTTL: upstreamTokens.expires_in
248
+ };
249
+ }
250
+ }
251
+ });
252
+ ```
253
+
254
+ The callback can:
255
+ - Return both `accessTokenProps` and `newProps` to update both
256
+ - Return only `accessTokenProps` to update just the current access token
257
+ - Return only `newProps` to update both the grant and access token (the access token inherits these props)
258
+ - Return `accessTokenTTL` to override the default TTL for this specific access token
259
+ - Return nothing to keep the original props unchanged
260
+
261
+ The `accessTokenTTL` override is particularly useful when the application is also an OAuth client to another service and wants to match its access token TTL to the upstream access token TTL. This helps prevent situations where the downstream token is still valid but the upstream token has expired.
262
+
263
+ The `props` values are end-to-end encrypted, so they can safely contain sensitive information.
264
+
199
265
  ## Written by Claude
200
266
 
201
267
  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.
@@ -9,6 +9,59 @@ type WorkerEntrypointWithFetch = WorkerEntrypoint & Pick<Required<WorkerEntrypoi
9
9
  /**
10
10
  * Configuration options for the OAuth Provider
11
11
  */
12
+ /**
13
+ * Result of a token exchange callback function.
14
+ * Allows updating the props stored in both the access token and the grant.
15
+ */
16
+ interface TokenExchangeCallbackResult {
17
+ /**
18
+ * New props to be stored specifically with the access token.
19
+ * If not provided but newProps is, the access token will use newProps.
20
+ * If neither is provided, the original props will be used.
21
+ */
22
+ accessTokenProps?: any;
23
+ /**
24
+ * New props to replace the props stored in the grant itself.
25
+ * These props will be used for all future token refreshes.
26
+ * If accessTokenProps is not provided, these props will also be used for the current access token.
27
+ * If not provided, the original props will be used.
28
+ */
29
+ newProps?: any;
30
+ /**
31
+ * Override the default access token TTL (time-to-live) for this specific token.
32
+ * This is especially useful when the application is also an OAuth client to another service
33
+ * and wants to match its access token TTL to the upstream access token TTL.
34
+ * Value should be in seconds.
35
+ */
36
+ accessTokenTTL?: number;
37
+ }
38
+ /**
39
+ * Options for token exchange callback functions
40
+ */
41
+ interface TokenExchangeCallbackOptions {
42
+ /**
43
+ * The type of grant being processed.
44
+ * 'authorization_code' for initial code exchange,
45
+ * 'refresh_token' for refresh token exchange.
46
+ */
47
+ grantType: 'authorization_code' | 'refresh_token';
48
+ /**
49
+ * Client that received this grant
50
+ */
51
+ clientId: string;
52
+ /**
53
+ * User who authorized this grant
54
+ */
55
+ userId: string;
56
+ /**
57
+ * List of scopes that were granted
58
+ */
59
+ scope: string[];
60
+ /**
61
+ * Application-specific properties currently associated with this grant
62
+ */
63
+ props: any;
64
+ }
12
65
  interface OAuthProviderOptions {
13
66
  /**
14
67
  * URL(s) for API routes. Requests with URLs starting with any of these prefixes
@@ -65,6 +118,16 @@ interface OAuthProviderOptions {
65
118
  * Defaults to false.
66
119
  */
67
120
  disallowPublicClientRegistration?: boolean;
121
+ /**
122
+ * Optional callback function that is called during token exchange.
123
+ * This allows updating the props stored in both the access token and the grant.
124
+ * For example, if the application itself is also a client to some other OAuth API,
125
+ * it may want to perform the equivalent upstream token exchange, and store the result in the props.
126
+ *
127
+ * The callback can return new props values that will be stored with the token or grant.
128
+ * If the callback returns nothing or undefined for a props field, the original props will be used.
129
+ */
130
+ tokenExchangeCallback?: (options: TokenExchangeCallbackOptions) => Promise<TokenExchangeCallbackResult | void> | TokenExchangeCallbackResult | void;
68
131
  }
69
132
  /**
70
133
  * Helper methods for OAuth operations provided to handler functions
@@ -456,4 +519,4 @@ declare class OAuthProvider {
456
519
  fetch(request: Request, env: any, ctx: ExecutionContext): Promise<Response>;
457
520
  }
458
521
 
459
- export { type AuthRequest, type ClientInfo, type CompleteAuthorizationOptions, type Grant, type GrantSummary, type ListOptions, type ListResult, type OAuthHelpers, OAuthProvider, type OAuthProviderOptions, type Token, OAuthProvider as default };
522
+ export { type AuthRequest, type ClientInfo, type CompleteAuthorizationOptions, type Grant, type GrantSummary, type ListOptions, type ListResult, type OAuthHelpers, OAuthProvider, type OAuthProviderOptions, type Token, type TokenExchangeCallbackOptions, type TokenExchangeCallbackResult, OAuthProvider as default };
@@ -89,7 +89,9 @@ var OAuthProviderImpl = class {
89
89
  if (typeof handler === "function" && handler.prototype instanceof WorkerEntrypoint) {
90
90
  return { type: 1 /* WORKER_ENTRYPOINT */, handler };
91
91
  }
92
- throw new TypeError(`${name} must be either an ExportedHandler object with a fetch method or a class extending WorkerEntrypoint`);
92
+ throw new TypeError(
93
+ `${name} must be either an ExportedHandler object with a fetch method or a class extending WorkerEntrypoint`
94
+ );
93
95
  }
94
96
  /**
95
97
  * Main fetch handler for the Worker
@@ -132,7 +134,11 @@ var OAuthProviderImpl = class {
132
134
  env.OAUTH_PROVIDER = this.createOAuthHelpers(env);
133
135
  }
134
136
  if (this.typedDefaultHandler.type === 0 /* EXPORTED_HANDLER */) {
135
- return this.typedDefaultHandler.handler.fetch(request, env, ctx);
137
+ return this.typedDefaultHandler.handler.fetch(
138
+ request,
139
+ env,
140
+ ctx
141
+ );
136
142
  } else {
137
143
  const handler = new this.typedDefaultHandler.handler(ctx, env);
138
144
  return handler.fetch(request);
@@ -292,20 +298,12 @@ var OAuthProviderImpl = class {
292
298
  */
293
299
  async handleTokenRequest(request, env) {
294
300
  if (request.method !== "POST") {
295
- return createErrorResponse(
296
- "invalid_request",
297
- "Method not allowed",
298
- 405
299
- );
301
+ return createErrorResponse("invalid_request", "Method not allowed", 405);
300
302
  }
301
303
  let contentType = request.headers.get("Content-Type") || "";
302
304
  let body = {};
303
305
  if (!contentType.includes("application/x-www-form-urlencoded")) {
304
- return createErrorResponse(
305
- "invalid_request",
306
- "Content-Type must be application/x-www-form-urlencoded",
307
- 400
308
- );
306
+ return createErrorResponse("invalid_request", "Content-Type must be application/x-www-form-urlencoded", 400);
309
307
  }
310
308
  const formData = await request.formData();
311
309
  for (const [key, value] of formData.entries()) {
@@ -324,28 +322,16 @@ var OAuthProviderImpl = class {
324
322
  clientSecret = body.client_secret || "";
325
323
  }
326
324
  if (!clientId) {
327
- return createErrorResponse(
328
- "invalid_client",
329
- "Client ID is required",
330
- 401
331
- );
325
+ return createErrorResponse("invalid_client", "Client ID is required", 401);
332
326
  }
333
327
  const clientInfo = await this.getClient(env, clientId);
334
328
  if (!clientInfo) {
335
- return createErrorResponse(
336
- "invalid_client",
337
- "Client not found",
338
- 401
339
- );
329
+ return createErrorResponse("invalid_client", "Client not found", 401);
340
330
  }
341
331
  const isPublicClient = clientInfo.tokenEndpointAuthMethod === "none";
342
332
  if (!isPublicClient) {
343
333
  if (!clientSecret) {
344
- return createErrorResponse(
345
- "invalid_client",
346
- "Client authentication failed: missing client_secret",
347
- 401
348
- );
334
+ return createErrorResponse("invalid_client", "Client authentication failed: missing client_secret", 401);
349
335
  }
350
336
  if (!clientInfo.clientSecret) {
351
337
  return createErrorResponse(
@@ -356,11 +342,7 @@ var OAuthProviderImpl = class {
356
342
  }
357
343
  const providedSecretHash = await hashSecret(clientSecret);
358
344
  if (providedSecretHash !== clientInfo.clientSecret) {
359
- return createErrorResponse(
360
- "invalid_client",
361
- "Client authentication failed: invalid client_secret",
362
- 401
363
- );
345
+ return createErrorResponse("invalid_client", "Client authentication failed: invalid client_secret", 401);
364
346
  }
365
347
  }
366
348
  const grantType = body.grant_type;
@@ -369,10 +351,7 @@ var OAuthProviderImpl = class {
369
351
  } else if (grantType === "refresh_token") {
370
352
  return this.handleRefreshTokenGrant(body, clientInfo, env);
371
353
  } else {
372
- return createErrorResponse(
373
- "unsupported_grant_type",
374
- "Grant type not supported"
375
- );
354
+ return createErrorResponse("unsupported_grant_type", "Grant type not supported");
376
355
  }
377
356
  }
378
357
  /**
@@ -388,65 +367,38 @@ var OAuthProviderImpl = class {
388
367
  const redirectUri = body.redirect_uri;
389
368
  const codeVerifier = body.code_verifier;
390
369
  if (!code) {
391
- return createErrorResponse(
392
- "invalid_request",
393
- "Authorization code is required"
394
- );
370
+ return createErrorResponse("invalid_request", "Authorization code is required");
395
371
  }
396
372
  const codeParts = code.split(":");
397
373
  if (codeParts.length !== 3) {
398
- return createErrorResponse(
399
- "invalid_grant",
400
- "Invalid authorization code format"
401
- );
374
+ return createErrorResponse("invalid_grant", "Invalid authorization code format");
402
375
  }
403
376
  const [userId, grantId, _] = codeParts;
404
377
  const grantKey = `grant:${userId}:${grantId}`;
405
378
  const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
406
379
  if (!grantData) {
407
- return createErrorResponse(
408
- "invalid_grant",
409
- "Grant not found or authorization code expired"
410
- );
380
+ return createErrorResponse("invalid_grant", "Grant not found or authorization code expired");
411
381
  }
412
382
  if (!grantData.authCodeId) {
413
- return createErrorResponse(
414
- "invalid_grant",
415
- "Authorization code already used"
416
- );
383
+ return createErrorResponse("invalid_grant", "Authorization code already used");
417
384
  }
418
385
  const codeHash = await hashSecret(code);
419
386
  if (codeHash !== grantData.authCodeId) {
420
- return createErrorResponse(
421
- "invalid_grant",
422
- "Invalid authorization code"
423
- );
387
+ return createErrorResponse("invalid_grant", "Invalid authorization code");
424
388
  }
425
389
  if (grantData.clientId !== clientInfo.clientId) {
426
- return createErrorResponse(
427
- "invalid_grant",
428
- "Client ID mismatch"
429
- );
390
+ return createErrorResponse("invalid_grant", "Client ID mismatch");
430
391
  }
431
392
  const isPkceEnabled = !!grantData.codeChallenge;
432
393
  if (!redirectUri && !isPkceEnabled) {
433
- return createErrorResponse(
434
- "invalid_request",
435
- "redirect_uri is required when not using PKCE"
436
- );
394
+ return createErrorResponse("invalid_request", "redirect_uri is required when not using PKCE");
437
395
  }
438
396
  if (redirectUri && !clientInfo.redirectUris.includes(redirectUri)) {
439
- return createErrorResponse(
440
- "invalid_grant",
441
- "Invalid redirect URI"
442
- );
397
+ return createErrorResponse("invalid_grant", "Invalid redirect URI");
443
398
  }
444
399
  if (isPkceEnabled) {
445
400
  if (!codeVerifier) {
446
- return createErrorResponse(
447
- "invalid_request",
448
- "code_verifier is required for PKCE"
449
- );
401
+ return createErrorResponse("invalid_request", "code_verifier is required for PKCE");
450
402
  }
451
403
  let calculatedChallenge;
452
404
  if (grantData.codeChallengeMethod === "S256") {
@@ -459,10 +411,7 @@ var OAuthProviderImpl = class {
459
411
  calculatedChallenge = codeVerifier;
460
412
  }
461
413
  if (calculatedChallenge !== grantData.codeChallenge) {
462
- return createErrorResponse(
463
- "invalid_grant",
464
- "Invalid PKCE code_verifier"
465
- );
414
+ return createErrorResponse("invalid_grant", "Invalid PKCE code_verifier");
466
415
  }
467
416
  }
468
417
  const accessTokenSecret = generateRandomString(TOKEN_LENGTH);
@@ -471,11 +420,53 @@ var OAuthProviderImpl = class {
471
420
  const refreshToken = `${userId}:${grantId}:${refreshTokenSecret}`;
472
421
  const accessTokenId = await generateTokenId(accessToken);
473
422
  const refreshTokenId = await generateTokenId(refreshToken);
474
- const now = Math.floor(Date.now() / 1e3);
475
- const accessTokenExpiresAt = now + this.options.accessTokenTTL;
423
+ let accessTokenTTL = this.options.accessTokenTTL;
476
424
  const encryptionKey = await unwrapKeyWithToken(code, grantData.authCodeWrappedKey);
477
- const accessTokenWrappedKey = await wrapKeyWithToken(accessToken, encryptionKey);
478
- const refreshTokenWrappedKey = await wrapKeyWithToken(refreshToken, encryptionKey);
425
+ let grantEncryptionKey = encryptionKey;
426
+ let accessTokenEncryptionKey = encryptionKey;
427
+ let encryptedAccessTokenProps = grantData.encryptedProps;
428
+ if (this.options.tokenExchangeCallback) {
429
+ const decryptedProps = await decryptProps(encryptionKey, grantData.encryptedProps);
430
+ let grantProps = decryptedProps;
431
+ let accessTokenProps = decryptedProps;
432
+ const callbackOptions = {
433
+ grantType: "authorization_code",
434
+ clientId: clientInfo.clientId,
435
+ userId,
436
+ scope: grantData.scope,
437
+ props: decryptedProps
438
+ };
439
+ const callbackResult = await Promise.resolve(this.options.tokenExchangeCallback(callbackOptions));
440
+ if (callbackResult) {
441
+ if (callbackResult.newProps) {
442
+ grantProps = callbackResult.newProps;
443
+ if (!callbackResult.accessTokenProps) {
444
+ accessTokenProps = callbackResult.newProps;
445
+ }
446
+ }
447
+ if (callbackResult.accessTokenProps) {
448
+ accessTokenProps = callbackResult.accessTokenProps;
449
+ }
450
+ if (callbackResult.accessTokenTTL !== void 0) {
451
+ accessTokenTTL = callbackResult.accessTokenTTL;
452
+ }
453
+ }
454
+ const grantResult = await encryptProps(grantProps);
455
+ grantData.encryptedProps = grantResult.encryptedData;
456
+ grantEncryptionKey = grantResult.key;
457
+ if (accessTokenProps !== grantProps) {
458
+ const tokenResult = await encryptProps(accessTokenProps);
459
+ encryptedAccessTokenProps = tokenResult.encryptedData;
460
+ accessTokenEncryptionKey = tokenResult.key;
461
+ } else {
462
+ encryptedAccessTokenProps = grantData.encryptedProps;
463
+ accessTokenEncryptionKey = grantEncryptionKey;
464
+ }
465
+ }
466
+ const now = Math.floor(Date.now() / 1e3);
467
+ const accessTokenExpiresAt = now + accessTokenTTL;
468
+ const accessTokenWrappedKey = await wrapKeyWithToken(accessToken, accessTokenEncryptionKey);
469
+ const refreshTokenWrappedKey = await wrapKeyWithToken(refreshToken, grantEncryptionKey);
479
470
  delete grantData.authCodeId;
480
471
  delete grantData.codeChallenge;
481
472
  delete grantData.codeChallengeMethod;
@@ -495,23 +486,24 @@ var OAuthProviderImpl = class {
495
486
  grant: {
496
487
  clientId: grantData.clientId,
497
488
  scope: grantData.scope,
498
- encryptedProps: grantData.encryptedProps
489
+ encryptedProps: encryptedAccessTokenProps
499
490
  }
500
491
  };
501
- await env.OAUTH_KV.put(
502
- `token:${userId}:${grantId}:${accessTokenId}`,
503
- JSON.stringify(accessTokenData),
504
- { expirationTtl: this.options.accessTokenTTL }
505
- );
506
- return new Response(JSON.stringify({
507
- access_token: accessToken,
508
- token_type: "bearer",
509
- expires_in: this.options.accessTokenTTL,
510
- refresh_token: refreshToken,
511
- scope: grantData.scope.join(" ")
512
- }), {
513
- headers: { "Content-Type": "application/json" }
492
+ await env.OAUTH_KV.put(`token:${userId}:${grantId}:${accessTokenId}`, JSON.stringify(accessTokenData), {
493
+ expirationTtl: accessTokenTTL
514
494
  });
495
+ return new Response(
496
+ JSON.stringify({
497
+ access_token: accessToken,
498
+ token_type: "bearer",
499
+ expires_in: accessTokenTTL,
500
+ refresh_token: refreshToken,
501
+ scope: grantData.scope.join(" ")
502
+ }),
503
+ {
504
+ headers: { "Content-Type": "application/json" }
505
+ }
506
+ );
515
507
  }
516
508
  /**
517
509
  * Handles the refresh token grant type
@@ -524,41 +516,26 @@ var OAuthProviderImpl = class {
524
516
  async handleRefreshTokenGrant(body, clientInfo, env) {
525
517
  const refreshToken = body.refresh_token;
526
518
  if (!refreshToken) {
527
- return createErrorResponse(
528
- "invalid_request",
529
- "Refresh token is required"
530
- );
519
+ return createErrorResponse("invalid_request", "Refresh token is required");
531
520
  }
532
521
  const tokenParts = refreshToken.split(":");
533
522
  if (tokenParts.length !== 3) {
534
- return createErrorResponse(
535
- "invalid_grant",
536
- "Invalid token format"
537
- );
523
+ return createErrorResponse("invalid_grant", "Invalid token format");
538
524
  }
539
525
  const [userId, grantId, _] = tokenParts;
540
526
  const providedTokenHash = await generateTokenId(refreshToken);
541
527
  const grantKey = `grant:${userId}:${grantId}`;
542
528
  const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
543
529
  if (!grantData) {
544
- return createErrorResponse(
545
- "invalid_grant",
546
- "Grant not found"
547
- );
530
+ return createErrorResponse("invalid_grant", "Grant not found");
548
531
  }
549
532
  const isCurrentToken = grantData.refreshTokenId === providedTokenHash;
550
533
  const isPreviousToken = grantData.previousRefreshTokenId === providedTokenHash;
551
534
  if (!isCurrentToken && !isPreviousToken) {
552
- return createErrorResponse(
553
- "invalid_grant",
554
- "Invalid refresh token"
555
- );
535
+ return createErrorResponse("invalid_grant", "Invalid refresh token");
556
536
  }
557
537
  if (grantData.clientId !== clientInfo.clientId) {
558
- return createErrorResponse(
559
- "invalid_grant",
560
- "Client ID mismatch"
561
- );
538
+ return createErrorResponse("invalid_grant", "Client ID mismatch");
562
539
  }
563
540
  const accessTokenSecret = generateRandomString(TOKEN_LENGTH);
564
541
  const newAccessToken = `${userId}:${grantId}:${accessTokenSecret}`;
@@ -566,8 +543,7 @@ var OAuthProviderImpl = class {
566
543
  const refreshTokenSecret = generateRandomString(TOKEN_LENGTH);
567
544
  const newRefreshToken = `${userId}:${grantId}:${refreshTokenSecret}`;
568
545
  const newRefreshTokenId = await generateTokenId(newRefreshToken);
569
- const now = Math.floor(Date.now() / 1e3);
570
- const accessTokenExpiresAt = now + this.options.accessTokenTTL;
546
+ let accessTokenTTL = this.options.accessTokenTTL;
571
547
  let wrappedKeyToUse;
572
548
  if (isCurrentToken) {
573
549
  wrappedKeyToUse = grantData.refreshTokenWrappedKey;
@@ -575,8 +551,60 @@ var OAuthProviderImpl = class {
575
551
  wrappedKeyToUse = grantData.previousRefreshTokenWrappedKey;
576
552
  }
577
553
  const encryptionKey = await unwrapKeyWithToken(refreshToken, wrappedKeyToUse);
578
- const accessTokenWrappedKey = await wrapKeyWithToken(newAccessToken, encryptionKey);
579
- const newRefreshTokenWrappedKey = await wrapKeyWithToken(newRefreshToken, encryptionKey);
554
+ let grantEncryptionKey = encryptionKey;
555
+ let accessTokenEncryptionKey = encryptionKey;
556
+ let encryptedAccessTokenProps = grantData.encryptedProps;
557
+ if (this.options.tokenExchangeCallback) {
558
+ const decryptedProps = await decryptProps(encryptionKey, grantData.encryptedProps);
559
+ let grantProps = decryptedProps;
560
+ let accessTokenProps = decryptedProps;
561
+ const callbackOptions = {
562
+ grantType: "refresh_token",
563
+ clientId: clientInfo.clientId,
564
+ userId,
565
+ scope: grantData.scope,
566
+ props: decryptedProps
567
+ };
568
+ const callbackResult = await Promise.resolve(this.options.tokenExchangeCallback(callbackOptions));
569
+ let grantPropsChanged = false;
570
+ if (callbackResult) {
571
+ if (callbackResult.newProps) {
572
+ grantProps = callbackResult.newProps;
573
+ grantPropsChanged = true;
574
+ if (!callbackResult.accessTokenProps) {
575
+ accessTokenProps = callbackResult.newProps;
576
+ }
577
+ }
578
+ if (callbackResult.accessTokenProps) {
579
+ accessTokenProps = callbackResult.accessTokenProps;
580
+ }
581
+ if (callbackResult.accessTokenTTL !== void 0) {
582
+ accessTokenTTL = callbackResult.accessTokenTTL;
583
+ }
584
+ }
585
+ if (grantPropsChanged) {
586
+ const grantResult = await encryptProps(grantProps);
587
+ grantData.encryptedProps = grantResult.encryptedData;
588
+ if (grantResult.key !== encryptionKey) {
589
+ grantEncryptionKey = grantResult.key;
590
+ wrappedKeyToUse = await wrapKeyWithToken(refreshToken, grantEncryptionKey);
591
+ } else {
592
+ grantEncryptionKey = grantResult.key;
593
+ }
594
+ }
595
+ if (accessTokenProps !== grantProps) {
596
+ const tokenResult = await encryptProps(accessTokenProps);
597
+ encryptedAccessTokenProps = tokenResult.encryptedData;
598
+ accessTokenEncryptionKey = tokenResult.key;
599
+ } else {
600
+ encryptedAccessTokenProps = grantData.encryptedProps;
601
+ accessTokenEncryptionKey = grantEncryptionKey;
602
+ }
603
+ }
604
+ const now = Math.floor(Date.now() / 1e3);
605
+ const accessTokenExpiresAt = now + accessTokenTTL;
606
+ const accessTokenWrappedKey = await wrapKeyWithToken(newAccessToken, accessTokenEncryptionKey);
607
+ const newRefreshTokenWrappedKey = await wrapKeyWithToken(newRefreshToken, grantEncryptionKey);
580
608
  grantData.previousRefreshTokenId = providedTokenHash;
581
609
  grantData.previousRefreshTokenWrappedKey = wrappedKeyToUse;
582
610
  grantData.refreshTokenId = newRefreshTokenId;
@@ -592,23 +620,24 @@ var OAuthProviderImpl = class {
592
620
  grant: {
593
621
  clientId: grantData.clientId,
594
622
  scope: grantData.scope,
595
- encryptedProps: grantData.encryptedProps
623
+ encryptedProps: encryptedAccessTokenProps
596
624
  }
597
625
  };
598
- await env.OAUTH_KV.put(
599
- `token:${userId}:${grantId}:${accessTokenId}`,
600
- JSON.stringify(accessTokenData),
601
- { expirationTtl: this.options.accessTokenTTL }
602
- );
603
- return new Response(JSON.stringify({
604
- access_token: newAccessToken,
605
- token_type: "bearer",
606
- expires_in: this.options.accessTokenTTL,
607
- refresh_token: newRefreshToken,
608
- scope: grantData.scope.join(" ")
609
- }), {
610
- headers: { "Content-Type": "application/json" }
626
+ await env.OAUTH_KV.put(`token:${userId}:${grantId}:${accessTokenId}`, JSON.stringify(accessTokenData), {
627
+ expirationTtl: accessTokenTTL
611
628
  });
629
+ return new Response(
630
+ JSON.stringify({
631
+ access_token: newAccessToken,
632
+ token_type: "bearer",
633
+ expires_in: accessTokenTTL,
634
+ refresh_token: newRefreshToken,
635
+ scope: grantData.scope.join(" ")
636
+ }),
637
+ {
638
+ headers: { "Content-Type": "application/json" }
639
+ }
640
+ );
612
641
  }
613
642
  /**
614
643
  * Handles the dynamic client registration endpoint (RFC 7591)
@@ -618,44 +647,24 @@ var OAuthProviderImpl = class {
618
647
  */
619
648
  async handleClientRegistration(request, env) {
620
649
  if (!this.options.clientRegistrationEndpoint) {
621
- return createErrorResponse(
622
- "not_implemented",
623
- "Client registration is not enabled",
624
- 501
625
- );
650
+ return createErrorResponse("not_implemented", "Client registration is not enabled", 501);
626
651
  }
627
652
  if (request.method !== "POST") {
628
- return createErrorResponse(
629
- "invalid_request",
630
- "Method not allowed",
631
- 405
632
- );
653
+ return createErrorResponse("invalid_request", "Method not allowed", 405);
633
654
  }
634
655
  const contentLength = parseInt(request.headers.get("Content-Length") || "0", 10);
635
656
  if (contentLength > 1048576) {
636
- return createErrorResponse(
637
- "invalid_request",
638
- "Request payload too large, must be under 1 MiB",
639
- 413
640
- );
657
+ return createErrorResponse("invalid_request", "Request payload too large, must be under 1 MiB", 413);
641
658
  }
642
659
  let clientMetadata;
643
660
  try {
644
661
  const text = await request.text();
645
662
  if (text.length > 1048576) {
646
- return createErrorResponse(
647
- "invalid_request",
648
- "Request payload too large, must be under 1 MiB",
649
- 413
650
- );
663
+ return createErrorResponse("invalid_request", "Request payload too large, must be under 1 MiB", 413);
651
664
  }
652
665
  clientMetadata = JSON.parse(text);
653
666
  } catch (error) {
654
- return createErrorResponse(
655
- "invalid_request",
656
- "Invalid JSON payload",
657
- 400
658
- );
667
+ return createErrorResponse("invalid_request", "Invalid JSON payload", 400);
659
668
  }
660
669
  const validateStringField = (field) => {
661
670
  if (field === void 0) {
@@ -683,10 +692,7 @@ var OAuthProviderImpl = class {
683
692
  const authMethod = validateStringField(clientMetadata.token_endpoint_auth_method) || "client_secret_basic";
684
693
  const isPublicClient = authMethod === "none";
685
694
  if (isPublicClient && this.options.disallowPublicClientRegistration) {
686
- return createErrorResponse(
687
- "invalid_client_metadata",
688
- "Public client registration is not allowed"
689
- );
695
+ return createErrorResponse("invalid_client_metadata", "Public client registration is not allowed");
690
696
  }
691
697
  const clientId = generateRandomString(16);
692
698
  let clientSecret;
@@ -760,49 +766,34 @@ var OAuthProviderImpl = class {
760
766
  async handleApiRequest(request, env, ctx) {
761
767
  const authHeader = request.headers.get("Authorization");
762
768
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
763
- return createErrorResponse(
764
- "invalid_token",
765
- "Missing or invalid access token",
766
- 401,
767
- { "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token", error_description="Missing or invalid access token"' }
768
- );
769
+ return createErrorResponse("invalid_token", "Missing or invalid access token", 401, {
770
+ "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token", error_description="Missing or invalid access token"'
771
+ });
769
772
  }
770
773
  const accessToken = authHeader.substring(7);
771
774
  const tokenParts = accessToken.split(":");
772
775
  if (tokenParts.length !== 3) {
773
- return createErrorResponse(
774
- "invalid_token",
775
- "Invalid token format",
776
- 401,
777
- { "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"' }
778
- );
776
+ return createErrorResponse("invalid_token", "Invalid token format", 401, {
777
+ "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
778
+ });
779
779
  }
780
780
  const [userId, grantId, _] = tokenParts;
781
781
  const accessTokenId = await generateTokenId(accessToken);
782
782
  const tokenKey = `token:${userId}:${grantId}:${accessTokenId}`;
783
783
  const tokenData = await env.OAUTH_KV.get(tokenKey, { type: "json" });
784
784
  if (!tokenData) {
785
- return createErrorResponse(
786
- "invalid_token",
787
- "Invalid access token",
788
- 401,
789
- { "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"' }
790
- );
785
+ return createErrorResponse("invalid_token", "Invalid access token", 401, {
786
+ "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
787
+ });
791
788
  }
792
789
  const now = Math.floor(Date.now() / 1e3);
793
790
  if (tokenData.expiresAt < now) {
794
- return createErrorResponse(
795
- "invalid_token",
796
- "Access token expired",
797
- 401,
798
- { "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"' }
799
- );
791
+ return createErrorResponse("invalid_token", "Access token expired", 401, {
792
+ "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
793
+ });
800
794
  }
801
795
  const encryptionKey = await unwrapKeyWithToken(accessToken, tokenData.wrappedEncryptionKey);
802
- const decryptedProps = await decryptProps(
803
- encryptionKey,
804
- tokenData.grant.encryptedProps
805
- );
796
+ const decryptedProps = await decryptProps(encryptionKey, tokenData.grant.encryptedProps);
806
797
  ctx.props = decryptedProps;
807
798
  if (!env.OAUTH_PROVIDER) {
808
799
  env.OAUTH_PROVIDER = this.createOAuthHelpers(env);
@@ -972,11 +963,7 @@ async function deriveKeyFromToken(tokenStr) {
972
963
  false,
973
964
  ["sign"]
974
965
  );
975
- const hmacResult = await crypto.subtle.sign(
976
- "HMAC",
977
- hmacKey,
978
- encoder.encode(tokenStr)
979
- );
966
+ const hmacResult = await crypto.subtle.sign("HMAC", hmacKey, encoder.encode(tokenStr));
980
967
  return await crypto.subtle.importKey(
981
968
  "raw",
982
969
  hmacResult,
@@ -988,12 +975,7 @@ async function deriveKeyFromToken(tokenStr) {
988
975
  }
989
976
  async function wrapKeyWithToken(tokenStr, keyToWrap) {
990
977
  const wrappingKey = await deriveKeyFromToken(tokenStr);
991
- const wrappedKeyBuffer = await crypto.subtle.wrapKey(
992
- "raw",
993
- keyToWrap,
994
- wrappingKey,
995
- { name: "AES-KW" }
996
- );
978
+ const wrappedKeyBuffer = await crypto.subtle.wrapKey("raw", keyToWrap, wrappingKey, { name: "AES-KW" });
997
979
  return arrayBufferToBase64(wrappedKeyBuffer);
998
980
  }
999
981
  async function unwrapKeyWithToken(tokenStr, wrappedKeyBase64) {
@@ -1319,9 +1301,11 @@ var OAuthHelpersImpl = class {
1319
1301
  }
1320
1302
  const result = await this.env.OAUTH_KV.list(listOptions);
1321
1303
  if (result.keys.length > 0) {
1322
- await Promise.all(result.keys.map((key) => {
1323
- return this.env.OAUTH_KV.delete(key.name);
1324
- }));
1304
+ await Promise.all(
1305
+ result.keys.map((key) => {
1306
+ return this.env.OAUTH_KV.delete(key.name);
1307
+ })
1308
+ );
1325
1309
  }
1326
1310
  if (result.list_complete) {
1327
1311
  allTokensDeleted = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/workers-oauth-provider",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "OAuth provider for Cloudflare Workers",
5
5
  "main": "dist/oauth-provider.js",
6
6
  "types": "dist/oauth-provider.d.ts",
@@ -11,19 +11,21 @@
11
11
  ],
12
12
  "type": "module",
13
13
  "publishConfig": {
14
- "access": "restricted"
14
+ "access": "public"
15
15
  },
16
16
  "scripts": {
17
17
  "build": "tsup",
18
- "test": "vitest",
19
- "prepublishOnly": "npm run build"
18
+ "test": "vitest run",
19
+ "prepublishOnly": "npm run build",
20
+ "prettier": "prettier -w ."
20
21
  },
21
22
  "dependencies": {
22
23
  "@cloudflare/workers-types": "^4.20250311.0"
23
24
  },
24
25
  "devDependencies": {
26
+ "prettier": "^3.5.3",
25
27
  "tsup": "^8.4.0",
26
28
  "typescript": "^5.8.2",
27
- "vitest": "^1.2.2"
29
+ "vitest": "^3.0.8"
28
30
  }
29
31
  }