fix(oauth-provider): normalize auth_time timestamps (#8761)

This commit is contained in:
Gustavo Valverde
2026-03-24 22:22:35 +00:00
committed by GitHub
parent e0c8d853bf
commit 2d56c6af68
3 changed files with 140 additions and 3 deletions

View File

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

View File

@@ -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<string, unknown>).createdAt) ??
normalizeTimestampValue((value as Record<string, unknown>).created_at);
if (direct) {
return direct;
}
const nested = (value as Record<string, unknown>).session;
if (!nested || typeof nested !== "object") {
return undefined;
}
return (
normalizeTimestampValue((nested as Record<string, unknown>).createdAt) ??
normalizeTimestampValue((nested as Record<string, unknown>).created_at)
);
}
const cachedTrustedClients = new TTLCache<string, SchemaClient<Scope[]>>();
export async function verifyOAuthQueryParams(

View File

@@ -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();
});
});