diff --git a/packages/oauth-provider/src/token.ts b/packages/oauth-provider/src/token.ts index 7d3d493edc..dbafe68347 100644 --- a/packages/oauth-provider/src/token.ts +++ b/packages/oauth-provider/src/token.ts @@ -21,7 +21,9 @@ import { getJwtPlugin, getStoredToken, isPKCERequired, + normalizeTimestampValue, parseClientMetadata, + resolveSessionAuthTime, resolveSubjectIdentifier, storeToken, validateClientCredentials, @@ -759,8 +761,8 @@ async function handleAuthorizationCodeGrant( const authTime = verificationValue.authTime != null - ? new Date(verificationValue.authTime) - : new Date(session.createdAt); + ? normalizeTimestampValue(verificationValue.authTime) + : resolveSessionAuthTime(session); return createUserTokens( ctx, @@ -1063,7 +1065,9 @@ async function handleRefreshTokenGrant( } const authTime = - refreshToken.authTime != null ? new Date(refreshToken.authTime) : undefined; + refreshToken.authTime != null + ? normalizeTimestampValue(refreshToken.authTime) + : undefined; // Generate new tokens return createUserTokens( diff --git a/packages/oauth-provider/src/utils/index.ts b/packages/oauth-provider/src/utils/index.ts index 67a7e82048..b460e989a5 100644 --- a/packages/oauth-provider/src/utils/index.ts +++ b/packages/oauth-provider/src/utils/index.ts @@ -60,6 +60,80 @@ export const getJwtPlugin = (ctx: AuthContext) => { return plugin; }; +/** + * Normalizes timestamp-like values returned by adapters. + * + * Accepts Date instances, epoch milliseconds as numbers, and strings that are + * either ISO dates or numeric millisecond values such as "1774295570569.0". + */ +export function normalizeTimestampValue(value: unknown): Date | undefined { + if (value == null) { + return undefined; + } + + if (value instanceof Date) { + return Number.isFinite(value.getTime()) ? value : undefined; + } + + if (typeof value === "number") { + if (!Number.isFinite(value)) { + return undefined; + } + + const parsed = new Date(value); + return Number.isFinite(parsed.getTime()) ? parsed : undefined; + } + + if (typeof value === "string") { + const trimmed = value.trim(); + if (!trimmed.length) { + return undefined; + } + + const numeric = Number(trimmed); + if (Number.isFinite(numeric)) { + const parsed = new Date(numeric); + return Number.isFinite(parsed.getTime()) ? parsed : undefined; + } + + const parsed = new Date(trimmed); + return Number.isFinite(parsed.getTime()) ? parsed : undefined; + } + + return undefined; +} + +/** + * Resolves a session auth time from common adapter return shapes. + */ +export function resolveSessionAuthTime(value: unknown): Date | undefined { + if (value instanceof Date) { + return normalizeTimestampValue(value); + } + + if (!value || typeof value !== "object") { + return normalizeTimestampValue(value); + } + + const direct = + normalizeTimestampValue((value as Record).createdAt) ?? + normalizeTimestampValue((value as Record).created_at); + + if (direct) { + return direct; + } + + const nested = (value as Record).session; + if (!nested || typeof nested !== "object") { + return undefined; + } + + return ( + normalizeTimestampValue((nested as Record).createdAt) ?? + normalizeTimestampValue((nested as Record).created_at) + ); +} + const cachedTrustedClients = new TTLCache>(); export async function verifyOAuthQueryParams( diff --git a/packages/oauth-provider/src/utils/timestamps.test.ts b/packages/oauth-provider/src/utils/timestamps.test.ts new file mode 100644 index 0000000000..f92f58c819 --- /dev/null +++ b/packages/oauth-provider/src/utils/timestamps.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; + +import { normalizeTimestampValue, resolveSessionAuthTime } from "./index"; + +describe("normalizeTimestampValue", () => { + it("parses epoch-millis text values", () => { + const result = normalizeTimestampValue("1774295570569.0"); + + expect(result).toBeInstanceOf(Date); + expect(result?.getTime()).toBe(1774295570569); + }); + + it("returns undefined for invalid values", () => { + expect(normalizeTimestampValue("not-a-date")).toBeUndefined(); + expect(normalizeTimestampValue(Number.NaN)).toBeUndefined(); + expect(normalizeTimestampValue(9e15)).toBeUndefined(); + expect(normalizeTimestampValue("9e15")).toBeUndefined(); + }); +}); + +describe("resolveSessionAuthTime", () => { + it("reads createdAt from direct session objects", () => { + const result = resolveSessionAuthTime({ + createdAt: "1774295570569.0", + }); + + expect(result?.getTime()).toBe(1774295570569); + }); + + it("reads created_at from nested session payloads", () => { + const result = resolveSessionAuthTime({ + session: { + created_at: 1774295570569, + }, + }); + + expect(result?.getTime()).toBe(1774295570569); + }); + + it("normalizes direct Date values", () => { + const date = new Date(1774295570569); + + expect(resolveSessionAuthTime(date)?.getTime()).toBe(1774295570569); + }); + + it("does not fall back to updatedAt timestamps", () => { + const directResult = resolveSessionAuthTime({ + updatedAt: 1774295570569, + }); + const nestedResult = resolveSessionAuthTime({ + session: { + updated_at: "1774295570569.0", + }, + }); + + expect(directResult).toBeUndefined(); + expect(nestedResult).toBeUndefined(); + }); +});