@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 +9 -2
- package/dist/oauth-provider.d.ts +21 -0
- package/dist/oauth-provider.js +78 -40
- package/package.json +4 -3
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.
|
package/dist/oauth-provider.d.ts
CHANGED
|
@@ -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
|
package/dist/oauth-provider.js
CHANGED
|
@@ -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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
{
|
|
701
|
-
|
|
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:
|
|
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-
|
|
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.
|
|
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.
|
|
34
|
+
"tsx": "^4.20.5",
|
|
34
35
|
"typescript": "^5.9.2",
|
|
35
36
|
"vitest": "^3.2.4"
|
|
36
37
|
},
|