@cloudflare/workers-oauth-provider 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/oauth-provider.js +32 -5
- package/package.json +1 -1
package/dist/oauth-provider.js
CHANGED
|
@@ -639,9 +639,11 @@ function validateEmaMapperResult(result) {
|
|
|
639
639
|
* TOCTOU window between claim validation and token mint.
|
|
640
640
|
*/
|
|
641
641
|
function computeEmaAccessTokenTTL(input) {
|
|
642
|
-
const { configuredDefaultSeconds, assertionExp, mapperTtl, now } = input;
|
|
642
|
+
const { configuredDefaultSeconds, assertionExp, mapperTtl, now, minTtlSeconds } = input;
|
|
643
643
|
if (assertionExp - now <= 0) return err({ reason: "assertion_expired_after_processing" });
|
|
644
|
-
|
|
644
|
+
const ttl = mapperTtl ?? configuredDefaultSeconds;
|
|
645
|
+
if (ttl < minTtlSeconds) return err({ reason: "invalid_mapped_ttl" });
|
|
646
|
+
return ok(ttl);
|
|
645
647
|
}
|
|
646
648
|
function readRequiredString(claims, claimName) {
|
|
647
649
|
const value = claims[claimName];
|
|
@@ -785,6 +787,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
785
787
|
onError: ({ status, code, description }) => console.warn(`OAuth error response: ${status} ${code} - ${description}`),
|
|
786
788
|
...options
|
|
787
789
|
};
|
|
790
|
+
if (!Number.isInteger(this.options.accessTokenTTL) || this.options.accessTokenTTL < KV_MIN_EXPIRATION_TTL_SECONDS) throw new TypeError(`accessTokenTTL must be an integer of at least ${KV_MIN_EXPIRATION_TTL_SECONDS} seconds (Cloudflare KV's minimum expiration window).`);
|
|
788
791
|
this.validateEmaOptions(this.options.enterpriseManagedAuthorization);
|
|
789
792
|
if (this.options.enterpriseManagedAuthorization) {
|
|
790
793
|
this.jwksProvider = createDefaultJwksProvider({ cacheTtlSeconds: this.options.enterpriseManagedAuthorization.jwksCacheTtlSeconds });
|
|
@@ -1375,7 +1378,8 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1375
1378
|
if (!isCurrentToken && !isPreviousToken) return this.createErrorResponse("invalid_grant", { description: "Invalid refresh token" });
|
|
1376
1379
|
if (grantData.clientId !== clientInfo.clientId) return this.createErrorResponse("invalid_grant", { description: "Client ID mismatch" });
|
|
1377
1380
|
if (grantData.expiresAt !== void 0) {
|
|
1378
|
-
|
|
1381
|
+
const now$1 = Math.floor(Date.now() / 1e3);
|
|
1382
|
+
if (grantData.expiresAt - now$1 < KV_MIN_EXPIRATION_TTL_SECONDS) return this.createErrorResponse("invalid_grant", { description: "Refresh token has expired" });
|
|
1379
1383
|
}
|
|
1380
1384
|
const newAccessToken = `${userId}:${grantId}:${generateRandomString(TOKEN_LENGTH)}`;
|
|
1381
1385
|
const accessTokenId = await generateTokenId(newAccessToken);
|
|
@@ -1432,10 +1436,12 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1432
1436
|
}
|
|
1433
1437
|
}
|
|
1434
1438
|
const now = Math.floor(Date.now() / 1e3);
|
|
1439
|
+
if (grantData.expiresAt !== void 0 && grantData.expiresAt - now < KV_MIN_EXPIRATION_TTL_SECONDS) return this.createErrorResponse("invalid_grant", { description: "Refresh token has expired" });
|
|
1435
1440
|
if (grantData.expiresAt !== void 0) {
|
|
1436
1441
|
const remainingRefreshTokenLifetime = grantData.expiresAt - now;
|
|
1437
1442
|
if (remainingRefreshTokenLifetime > 0) accessTokenTTL = Math.min(accessTokenTTL, remainingRefreshTokenLifetime);
|
|
1438
1443
|
}
|
|
1444
|
+
if (accessTokenTTL < KV_MIN_EXPIRATION_TTL_SECONDS) return this.createErrorResponse("invalid_request", { description: "Requested token lifetime must be at least 60 seconds" });
|
|
1439
1445
|
const accessTokenExpiresAt = now + accessTokenTTL;
|
|
1440
1446
|
const accessTokenWrappedKey = await wrapKeyWithToken(newAccessToken, accessTokenEncryptionKey);
|
|
1441
1447
|
const newRefreshToken = `${userId}:${grantId}:${generateRandomString(TOKEN_LENGTH)}`;
|
|
@@ -1524,6 +1530,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1524
1530
|
}
|
|
1525
1531
|
const now = Math.floor(Date.now() / 1e3);
|
|
1526
1532
|
const subjectTokenRemainingLifetime = tokenSummary.expiresAt - now;
|
|
1533
|
+
if (subjectTokenRemainingLifetime < KV_MIN_EXPIRATION_TTL_SECONDS) throw new OAuthError("invalid_grant", { description: "Subject token is too close to expiry to exchange" });
|
|
1527
1534
|
let accessTokenTTL = this.options.accessTokenTTL ?? DEFAULT_ACCESS_TOKEN_TTL;
|
|
1528
1535
|
if (expiresIn !== void 0) {
|
|
1529
1536
|
if (expiresIn <= 0) throw new OAuthError("invalid_request", { description: "Invalid expires_in parameter" });
|
|
@@ -1561,6 +1568,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1561
1568
|
if (callbackResult.accessTokenScope) tokenScopes = this.downscope(callbackResult.accessTokenScope, grantData.scope);
|
|
1562
1569
|
}
|
|
1563
1570
|
}
|
|
1571
|
+
if (accessTokenTTL < KV_MIN_EXPIRATION_TTL_SECONDS) throw new OAuthError("invalid_request", { description: "Requested token lifetime must be at least 60 seconds" });
|
|
1564
1572
|
const tokenResponse = {
|
|
1565
1573
|
access_token: await this.createAccessToken({
|
|
1566
1574
|
userId: tokenSummary.userId,
|
|
@@ -1737,7 +1745,8 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1737
1745
|
configuredDefaultSeconds: this.options.accessTokenTTL ?? DEFAULT_ACCESS_TOKEN_TTL,
|
|
1738
1746
|
assertionExp: claims.value.claims.exp,
|
|
1739
1747
|
mapperTtl: mapped.value.accessTokenTTL,
|
|
1740
|
-
now: issueNow
|
|
1748
|
+
now: issueNow,
|
|
1749
|
+
minTtlSeconds: KV_MIN_EXPIRATION_TTL_SECONDS
|
|
1741
1750
|
});
|
|
1742
1751
|
if (!ttl.ok) return ttl;
|
|
1743
1752
|
return ok(await this.issueEmaAccessToken({
|
|
@@ -2114,7 +2123,8 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
2114
2123
|
* @param now - Current timestamp in seconds
|
|
2115
2124
|
*/
|
|
2116
2125
|
async saveGrantWithTTL(env, grantKey, grantData, now) {
|
|
2117
|
-
const
|
|
2126
|
+
const minExpiration = now + KV_MIN_EXPIRATION_TTL_SECONDS + KV_EXPIRATION_CLAMP_MARGIN_SECONDS;
|
|
2127
|
+
const kvOptions = grantData.expiresAt !== void 0 ? { expiration: Math.max(grantData.expiresAt, minExpiration) } : {};
|
|
2118
2128
|
try {
|
|
2119
2129
|
await env.OAUTH_KV.put(grantKey, JSON.stringify(grantData), kvOptions);
|
|
2120
2130
|
} catch (error) {
|
|
@@ -2171,6 +2181,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
2171
2181
|
*/
|
|
2172
2182
|
async createAccessToken(params) {
|
|
2173
2183
|
const { userId, grantId, clientId, scope, encryptedProps, encryptionKey, expiresIn, audience, env } = params;
|
|
2184
|
+
if (expiresIn < KV_MIN_EXPIRATION_TTL_SECONDS) throw new OAuthError("invalid_request", { description: "Requested token lifetime must be at least 60 seconds" });
|
|
2174
2185
|
const accessToken = `${userId}:${grantId}:${generateRandomString(TOKEN_LENGTH)}`;
|
|
2175
2186
|
const now = Math.floor(Date.now() / 1e3);
|
|
2176
2187
|
const accessTokenId = await generateTokenId(accessToken);
|
|
@@ -2522,6 +2533,22 @@ const DEFAULT_REFRESH_TOKEN_TTL = 720 * 60 * 60;
|
|
|
2522
2533
|
*/
|
|
2523
2534
|
const DEFAULT_CLIENT_REGISTRATION_TTL = 2160 * 60 * 60;
|
|
2524
2535
|
/**
|
|
2536
|
+
* Minimum number of seconds an absolute KV expiration must be in the future.
|
|
2537
|
+
* Cloudflare KV rejects `put` calls whose `expiration` is less than 60 seconds
|
|
2538
|
+
* away with "400 Invalid expiration ... Expiration times must be at least 60
|
|
2539
|
+
* seconds in the future." We use this to treat near-expiry grants as expired and
|
|
2540
|
+
* to clamp absolute expirations when writing grants back to KV.
|
|
2541
|
+
*/
|
|
2542
|
+
const KV_MIN_EXPIRATION_TTL_SECONDS = 60;
|
|
2543
|
+
/**
|
|
2544
|
+
* Safety margin (seconds) added on top of `KV_MIN_EXPIRATION_TTL_SECONDS` when clamping an
|
|
2545
|
+
* absolute KV expiration. Absolute expirations are validated against KV's clock at the
|
|
2546
|
+
* moment the write is processed, so writing exactly `now + 60` can be rejected once
|
|
2547
|
+
* worker→KV latency or minor clock skew is accounted for. The margin keeps clamped writes
|
|
2548
|
+
* comfortably above KV's hard minimum without meaningfully extending a grant's lifetime.
|
|
2549
|
+
*/
|
|
2550
|
+
const KV_EXPIRATION_CLAMP_MARGIN_SECONDS = 5;
|
|
2551
|
+
/**
|
|
2525
2552
|
* Default batch size for purgeExpiredData. Conservative to stay within
|
|
2526
2553
|
* Cloudflare's 1000 subrequest limit per invocation.
|
|
2527
2554
|
*/
|