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:
Meta Vens 2026-10-19 14:34:43 +02:00
commit 09ea233592
2 changed files with 30 additions and 4 deletions

View file

@ -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"] {

View file

@ -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,