@cloudflare/workers-oauth-provider 0.0.0-9587b58 → 0.0.0-9d4b595

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
@@ -38,7 +38,7 @@ export default new OAuthProvider({
38
38
  // You can provide either an object with a fetch method (ExportedHandler)
39
39
  // or a class extending WorkerEntrypoint.
40
40
  apiHandler: ApiHandler, // Using a WorkerEntrypoint class
41
-
41
+
42
42
  // For multi-handler setups, you can use apiHandlers instead of apiRoute+apiHandler.
43
43
  // This allows you to use different handlers for different API routes.
44
44
  // Note: You must use either apiRoute+apiHandler (single-handler) OR apiHandlers (multi-handler), not both.
@@ -87,7 +87,13 @@ export default new OAuthProvider({
87
87
  // Note: Creating public clients via the OAuthHelpers.createClient() method
88
88
  // is always allowed regardless of this setting.
89
89
  // Defaults to false.
90
- disallowPublicClientRegistration: false
90
+ disallowPublicClientRegistration: false,
91
+
92
+ // Optional: Time-to-live for refresh tokens in seconds.
93
+ // If not specified, refresh tokens do not expire.
94
+ // Set to 0 to disable refresh tokens (only access tokens will be issued).
95
+ // For example: 3600 = 1 hour, 86400 = 1 day, 2592000 = 30 days
96
+ refreshTokenTTL: 2592000 // 30 days
91
97
  });
92
98
 
93
99
  // The default handler object - the OAuthProvider will pass through HTTP requests to this object's fetch method
@@ -261,6 +267,7 @@ The callback can:
261
267
  - Return only `accessTokenProps` to update just the current access token
262
268
  - Return only `newProps` to update both the grant and access token (the access token inherits these props)
263
269
  - Return `accessTokenTTL` to override the default TTL for this specific access token
270
+ - Return `refreshTokenTTL` to override the default TTL for this specific refresh token
264
271
  - Return nothing to keep the original props unchanged
265
272
 
266
273
  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.
@@ -33,6 +33,13 @@ interface TokenExchangeCallbackResult {
33
33
  * Value should be in seconds.
34
34
  */
35
35
  accessTokenTTL?: number;
36
+ /**
37
+ * Override the default refresh token TTL (time-to-live) for this specific grant.
38
+ * Value should be in seconds.
39
+ * Note: This is only honored during authorization code exchange. If returned during
40
+ * refresh token exchange, it will be ignored.
41
+ */
42
+ refreshTokenTTL?: number;
36
43
  }
37
44
  /**
38
45
  * Options for token exchange callback functions
@@ -116,6 +123,12 @@ interface OAuthProviderOptions {
116
123
  * Defaults to 1 hour (3600 seconds) if not specified.
117
124
  */
118
125
  accessTokenTTL?: number;
126
+ /**
127
+ * Time-to-live for refresh tokens in seconds.
128
+ * If not specified, refresh tokens do not expire.
129
+ * For example: 3600 = 1 hour, 2592000 = 30 days
130
+ */
131
+ refreshTokenTTL?: number;
119
132
  /**
120
133
  * List of scopes supported by this OAuth provider.
121
134
  * If not provided, the 'scopes_supported' field will be omitted from the OAuth metadata.
@@ -381,6 +394,10 @@ interface Grant {
381
394
  * Unix timestamp when the grant was created
382
395
  */
383
396
  createdAt: number;
397
+ /**
398
+ * Unix timestamp when the grant expires (if TTL is configured)
399
+ */
400
+ expiresAt?: number;
384
401
  /**
385
402
  * The hash of the current refresh token associated with this grant
386
403
  */
@@ -523,6 +540,10 @@ interface GrantSummary {
523
540
  * Unix timestamp when the grant was created
524
541
  */
525
542
  createdAt: number;
543
+ /**
544
+ * Unix timestamp when the grant expires (if TTL is configured)
545
+ */
546
+ expiresAt?: number;
526
547
  }
527
548
  /**
528
549
  * OAuth 2.0 Provider implementation for Cloudflare Workers
@@ -478,12 +478,10 @@ var OAuthProviderImpl = class {
478
478
  }
479
479
  }
480
480
  const accessTokenSecret = generateRandomString(TOKEN_LENGTH);
481
- const refreshTokenSecret = generateRandomString(TOKEN_LENGTH);
482
481
  const accessToken = `${userId}:${grantId}:${accessTokenSecret}`;
483
- const refreshToken = `${userId}:${grantId}:${refreshTokenSecret}`;
484
482
  const accessTokenId = await generateTokenId(accessToken);
485
- const refreshTokenId = await generateTokenId(refreshToken);
486
483
  let accessTokenTTL = this.options.accessTokenTTL;
484
+ let refreshTokenTTL = this.options.refreshTokenTTL;
487
485
  const encryptionKey = await unwrapKeyWithToken(code, grantData.authCodeWrappedKey);
488
486
  let grantEncryptionKey = encryptionKey;
489
487
  let accessTokenEncryptionKey = encryptionKey;
@@ -513,6 +511,9 @@ var OAuthProviderImpl = class {
513
511
  if (callbackResult.accessTokenTTL !== void 0) {
514
512
  accessTokenTTL = callbackResult.accessTokenTTL;
515
513
  }
514
+ if ("refreshTokenTTL" in callbackResult) {
515
+ refreshTokenTTL = callbackResult.refreshTokenTTL;
516
+ }
516
517
  }
517
518
  const grantResult = await encryptProps(grantProps);
518
519
  grantData.encryptedProps = grantResult.encryptedData;
@@ -528,17 +529,26 @@ var OAuthProviderImpl = class {
528
529
  }
529
530
  const now = Math.floor(Date.now() / 1e3);
530
531
  const accessTokenExpiresAt = now + accessTokenTTL;
532
+ const useRefreshToken = refreshTokenTTL !== 0;
531
533
  const accessTokenWrappedKey = await wrapKeyWithToken(accessToken, accessTokenEncryptionKey);
532
- const refreshTokenWrappedKey = await wrapKeyWithToken(refreshToken, grantEncryptionKey);
533
534
  delete grantData.authCodeId;
534
535
  delete grantData.codeChallenge;
535
536
  delete grantData.codeChallengeMethod;
536
537
  delete grantData.authCodeWrappedKey;
537
- grantData.refreshTokenId = refreshTokenId;
538
- grantData.refreshTokenWrappedKey = refreshTokenWrappedKey;
539
- grantData.previousRefreshTokenId = void 0;
540
- grantData.previousRefreshTokenWrappedKey = void 0;
541
- await env.OAUTH_KV.put(grantKey, JSON.stringify(grantData));
538
+ let refreshToken;
539
+ if (useRefreshToken) {
540
+ const refreshTokenSecret = generateRandomString(TOKEN_LENGTH);
541
+ refreshToken = `${userId}:${grantId}:${refreshTokenSecret}`;
542
+ const refreshTokenId = await generateTokenId(refreshToken);
543
+ const refreshTokenWrappedKey = await wrapKeyWithToken(refreshToken, grantEncryptionKey);
544
+ const expiresAt = refreshTokenTTL !== void 0 ? now + refreshTokenTTL : void 0;
545
+ grantData.refreshTokenId = refreshTokenId;
546
+ grantData.refreshTokenWrappedKey = refreshTokenWrappedKey;
547
+ grantData.previousRefreshTokenId = void 0;
548
+ grantData.previousRefreshTokenWrappedKey = void 0;
549
+ grantData.expiresAt = expiresAt;
550
+ }
551
+ await this.saveGrantWithTTL(env, grantKey, grantData, now);
542
552
  const accessTokenData = {
543
553
  id: accessTokenId,
544
554
  grantId,
@@ -555,18 +565,18 @@ var OAuthProviderImpl = class {
555
565
  await env.OAUTH_KV.put(`token:${userId}:${grantId}:${accessTokenId}`, JSON.stringify(accessTokenData), {
556
566
  expirationTtl: accessTokenTTL
557
567
  });
558
- return new Response(
559
- JSON.stringify({
560
- access_token: accessToken,
561
- token_type: "bearer",
562
- expires_in: accessTokenTTL,
563
- refresh_token: refreshToken,
564
- scope: grantData.scope.join(" ")
565
- }),
566
- {
567
- headers: { "Content-Type": "application/json" }
568
- }
569
- );
568
+ const tokenResponse = {
569
+ access_token: accessToken,
570
+ token_type: "bearer",
571
+ expires_in: accessTokenTTL,
572
+ scope: grantData.scope.join(" ")
573
+ };
574
+ if (refreshToken) {
575
+ tokenResponse.refresh_token = refreshToken;
576
+ }
577
+ return new Response(JSON.stringify(tokenResponse), {
578
+ headers: { "Content-Type": "application/json" }
579
+ });
570
580
  }
571
581
  /**
572
582
  * Handles the refresh token grant type
@@ -600,12 +610,15 @@ var OAuthProviderImpl = class {
600
610
  if (grantData.clientId !== clientInfo.clientId) {
601
611
  return this.createErrorResponse("invalid_grant", "Client ID mismatch");
602
612
  }
613
+ if (grantData.expiresAt !== void 0) {
614
+ const now2 = Math.floor(Date.now() / 1e3);
615
+ if (now2 >= grantData.expiresAt) {
616
+ return this.createErrorResponse("invalid_grant", "Refresh token has expired");
617
+ }
618
+ }
603
619
  const accessTokenSecret = generateRandomString(TOKEN_LENGTH);
604
620
  const newAccessToken = `${userId}:${grantId}:${accessTokenSecret}`;
605
621
  const accessTokenId = await generateTokenId(newAccessToken);
606
- const refreshTokenSecret = generateRandomString(TOKEN_LENGTH);
607
- const newRefreshToken = `${userId}:${grantId}:${refreshTokenSecret}`;
608
- const newRefreshTokenId = await generateTokenId(newRefreshToken);
609
622
  let accessTokenTTL = this.options.accessTokenTTL;
610
623
  let wrappedKeyToUse;
611
624
  if (isCurrentToken) {
@@ -617,6 +630,7 @@ var OAuthProviderImpl = class {
617
630
  let grantEncryptionKey = encryptionKey;
618
631
  let accessTokenEncryptionKey = encryptionKey;
619
632
  let encryptedAccessTokenProps = grantData.encryptedProps;
633
+ let grantPropsChanged = false;
620
634
  if (this.options.tokenExchangeCallback) {
621
635
  const decryptedProps = await decryptProps(encryptionKey, grantData.encryptedProps);
622
636
  let grantProps = decryptedProps;
@@ -629,7 +643,6 @@ var OAuthProviderImpl = class {
629
643
  props: decryptedProps
630
644
  };
631
645
  const callbackResult = await Promise.resolve(this.options.tokenExchangeCallback(callbackOptions));
632
- let grantPropsChanged = false;
633
646
  if (callbackResult) {
634
647
  if (callbackResult.newProps) {
635
648
  grantProps = callbackResult.newProps;
@@ -644,6 +657,12 @@ var OAuthProviderImpl = class {
644
657
  if (callbackResult.accessTokenTTL !== void 0) {
645
658
  accessTokenTTL = callbackResult.accessTokenTTL;
646
659
  }
660
+ if ("refreshTokenTTL" in callbackResult) {
661
+ return this.createErrorResponse(
662
+ "invalid_request",
663
+ "refreshTokenTTL cannot be changed during refresh token exchange"
664
+ );
665
+ }
647
666
  }
648
667
  if (grantPropsChanged) {
649
668
  const grantResult = await encryptProps(grantProps);
@@ -665,14 +684,23 @@ var OAuthProviderImpl = class {
665
684
  }
666
685
  }
667
686
  const now = Math.floor(Date.now() / 1e3);
687
+ if (grantData.expiresAt !== void 0) {
688
+ const remainingRefreshTokenLifetime = grantData.expiresAt - now;
689
+ if (remainingRefreshTokenLifetime > 0) {
690
+ accessTokenTTL = Math.min(accessTokenTTL, remainingRefreshTokenLifetime);
691
+ }
692
+ }
668
693
  const accessTokenExpiresAt = now + accessTokenTTL;
669
694
  const accessTokenWrappedKey = await wrapKeyWithToken(newAccessToken, accessTokenEncryptionKey);
695
+ const refreshTokenSecret = generateRandomString(TOKEN_LENGTH);
696
+ const newRefreshToken = `${userId}:${grantId}:${refreshTokenSecret}`;
697
+ const newRefreshTokenId = await generateTokenId(newRefreshToken);
670
698
  const newRefreshTokenWrappedKey = await wrapKeyWithToken(newRefreshToken, grantEncryptionKey);
671
699
  grantData.previousRefreshTokenId = providedTokenHash;
672
700
  grantData.previousRefreshTokenWrappedKey = wrappedKeyToUse;
673
701
  grantData.refreshTokenId = newRefreshTokenId;
674
702
  grantData.refreshTokenWrappedKey = newRefreshTokenWrappedKey;
675
- await env.OAUTH_KV.put(grantKey, JSON.stringify(grantData));
703
+ await this.saveGrantWithTTL(env, grantKey, grantData, now);
676
704
  const accessTokenData = {
677
705
  id: accessTokenId,
678
706
  grantId,
@@ -689,18 +717,16 @@ var OAuthProviderImpl = class {
689
717
  await env.OAUTH_KV.put(`token:${userId}:${grantId}:${accessTokenId}`, JSON.stringify(accessTokenData), {
690
718
  expirationTtl: accessTokenTTL
691
719
  });
692
- return new Response(
693
- JSON.stringify({
694
- access_token: newAccessToken,
695
- token_type: "bearer",
696
- expires_in: accessTokenTTL,
697
- refresh_token: newRefreshToken,
698
- scope: grantData.scope.join(" ")
699
- }),
700
- {
701
- headers: { "Content-Type": "application/json" }
702
- }
703
- );
720
+ const tokenResponse = {
721
+ access_token: newAccessToken,
722
+ token_type: "bearer",
723
+ expires_in: accessTokenTTL,
724
+ refresh_token: newRefreshToken,
725
+ scope: grantData.scope.join(" ")
726
+ };
727
+ return new Response(JSON.stringify(tokenResponse), {
728
+ headers: { "Content-Type": "application/json" }
729
+ });
704
730
  }
705
731
  /**
706
732
  * Handles OAuth 2.0 token revocation requests (RFC 7009)
@@ -736,7 +762,7 @@ var OAuthProviderImpl = class {
736
762
  } else if (isRefreshToken) {
737
763
  await this.createOAuthHelpers(env).revokeGrant(grantId, userId);
738
764
  }
739
- return new Response("", { status: 500 });
765
+ return new Response("", { status: 200 });
740
766
  }
741
767
  /**
742
768
  * Revokes a specific access token without affecting the refresh token
@@ -962,6 +988,17 @@ var OAuthProviderImpl = class {
962
988
  createOAuthHelpers(env) {
963
989
  return new OAuthHelpersImpl(env, this);
964
990
  }
991
+ /**
992
+ * Saves a grant to KV with appropriate TTL based on expiration
993
+ * @param env - The environment bindings
994
+ * @param grantKey - The KV key for the grant
995
+ * @param grantData - The grant data to save
996
+ * @param now - Current timestamp in seconds
997
+ */
998
+ async saveGrantWithTTL(env, grantKey, grantData, now) {
999
+ const kvOptions = grantData.expiresAt !== void 0 ? { expiration: grantData.expiresAt } : {};
1000
+ await env.OAUTH_KV.put(grantKey, JSON.stringify(grantData), kvOptions);
1001
+ }
965
1002
  /**
966
1003
  * Fetches client information from KV storage
967
1004
  * This method is not private because `OAuthHelpers` needs to call it. Note that since
@@ -1444,7 +1481,8 @@ var OAuthHelpersImpl = class {
1444
1481
  userId: grantData.userId,
1445
1482
  scope: grantData.scope,
1446
1483
  metadata: grantData.metadata,
1447
- createdAt: grantData.createdAt
1484
+ createdAt: grantData.createdAt,
1485
+ expiresAt: grantData.expiresAt
1448
1486
  };
1449
1487
  grantSummaries.push(summary);
1450
1488
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/workers-oauth-provider",
3
- "version": "0.0.0-9587b58",
3
+ "version": "0.0.0-9d4b595",
4
4
  "description": "OAuth provider for Cloudflare Workers",
5
5
  "main": "dist/oauth-provider.js",
6
6
  "types": "dist/oauth-provider.d.ts",
@@ -26,11 +26,12 @@
26
26
  },
27
27
  "devDependencies": {
28
28
  "@changesets/changelog-github": "^0.5.1",
29
- "@changesets/cli": "^2.29.5",
29
+ "@changesets/cli": "^2.29.7",
30
30
  "@cloudflare/workers-types": "^4.20250807.0",
31
+ "pkg-pr-new": "^0.0.59",
31
32
  "prettier": "^3.6.2",
32
33
  "tsup": "^8.5.0",
33
- "tsx": "^4.20.3",
34
+ "tsx": "^4.20.5",
34
35
  "typescript": "^5.9.2",
35
36
  "vitest": "^3.2.4"
36
37
  },