mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-23 15:42:09 -05:00
fix(oauth-provider): normalize auth_time timestamps (#8761)
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
59
packages/oauth-provider/src/utils/timestamps.test.ts
Normal file
59
packages/oauth-provider/src/utils/timestamps.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user