Token answers: an expires_in beyond a year is unusable, so the default lifetime applies
A value like 3e11 overflowed the Date the expiry is stored as and surfaced as an untyped error. usableExpiresIn now caps a usable value at 365 days; the refresh path is pinned on memory and live Postgres. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e1ac84fb70
commit
09ea233592
2 changed files with 30 additions and 4 deletions
13
src/oauth.ts
13
src/oauth.ts
|
|
@ -228,10 +228,19 @@ export async function requestProviderTokens(
|
|||
return tokenSet;
|
||||
}
|
||||
|
||||
/** A positive finite number, or a string that is a positive integer. */
|
||||
/** The longest access token lifetime taken at its word: a year. Anything above is not a lifetime. */
|
||||
const MAX_USABLE_EXPIRES_IN_SECONDS = 365 * 24 * 60 * 60;
|
||||
|
||||
/**
|
||||
* A positive finite number of seconds, or a string that is a positive integer,
|
||||
* no longer than a year. A larger value would overflow the Date the expiry is
|
||||
* stored as, so it is unusable and the descriptor's default lifetime applies.
|
||||
*/
|
||||
function usableExpiresIn(value: unknown): number | undefined {
|
||||
const seconds = typeof value === "string" && /^\s*\d+\s*$/.test(value) ? Number(value) : value;
|
||||
return typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0 ? seconds : undefined;
|
||||
return typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0 && seconds <= MAX_USABLE_EXPIRES_IN_SECONDS
|
||||
? seconds
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function sharedRuntimeExchange(provider: Provider): OAuthProviderRuntime["exchangeAuthorizationCode"] {
|
||||
|
|
|
|||
|
|
@ -65,10 +65,23 @@ type Mode =
|
|||
| "no_expires_in"
|
||||
| "unusable_expires_in"
|
||||
| "string_expires_in"
|
||||
| "huge_expires_in"
|
||||
| "huge_string_expires_in"
|
||||
| "hang";
|
||||
|
||||
function expiresInFor(mode: Mode): unknown {
|
||||
return mode === "unusable_expires_in" ? "soon" : mode === "string_expires_in" ? "7200" : 3600;
|
||||
switch (mode) {
|
||||
case "unusable_expires_in":
|
||||
return "soon";
|
||||
case "string_expires_in":
|
||||
return "7200";
|
||||
case "huge_expires_in":
|
||||
return 3e11;
|
||||
case "huge_string_expires_in":
|
||||
return "300000000000";
|
||||
default:
|
||||
return 3600;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -122,7 +135,9 @@ function installTokenEndpoint(provider: Provider, firstRefreshToken: string) {
|
|||
case "normal":
|
||||
case "no_expires_in":
|
||||
case "unusable_expires_in":
|
||||
case "string_expires_in": {
|
||||
case "string_expires_in":
|
||||
case "huge_expires_in":
|
||||
case "huge_string_expires_in": {
|
||||
if (state.installationRevoked || presented !== state.live) {
|
||||
state.installationRevoked = true;
|
||||
return new Response(JSON.stringify({ error: "invalid_grant", error_description: "reused CANARY" }), { status: 400 });
|
||||
|
|
@ -487,6 +502,8 @@ for (const { kind, live } of rigKinds) describe.skipIf(live && !adminUrl)(`manag
|
|||
["no expires_in", "no_expires_in", 3600],
|
||||
["an unusable expires_in", "unusable_expires_in", 3600],
|
||||
["a numeric-string expires_in", "string_expires_in", 7200],
|
||||
["a numeric expires_in beyond a year", "huge_expires_in", 3600],
|
||||
["a numeric-string expires_in beyond a year", "huge_string_expires_in", 3600],
|
||||
] as const)(
|
||||
"refresh-once-per-token: an answer with %s stores now plus %s s, not the old due expiry, and resolve refreshes again only once it passes",
|
||||
timeout,
|
||||
|
|
|
|||
Loading…
Reference in a new issue