fix(auth): read a remembered browser's expiry on the throttle's clock

knownSignInSource compared a trusted device's expiry with new Date()
while the throttle around it times its waits with Date.now(). The test
mocks Date.now() only, so it set the device to expire 30 days after its
fixed clock, which the real date passed on 2026-10-26 at noon UTC; from
then on the remembered browser read as expired and the test failed on
every run. One clock decides both now, and the test's expiry is fixed
against it. A further case pins that an expired device gets no
exemption.
This commit is contained in:
Marc-André Bombeck committed 2026-10-26 15:08:06 +01:00
1 parent 460a4fa8f9
commit b180d09fae
2 files changed
+23 -10

No files matched your search

+20 -9
View File
@@ -125,6 +125,15 @@ describe("waitAfter", () => {
});
});
/** The owner's browser, remembered as a trusted device until `expiresAt`. */
function rememberBrowser(expiresAt: Date) {
deviceCookie = "device-token";
vi.mocked(prisma.trustedDevice.findUnique).mockResolvedValue({
userId: "owner-1",
expiresAt,
} as never);
}
describe("somebody guessing from two addresses", () => {
async function tenGuesses() {
const answers: string[] = [];
@@ -159,18 +168,20 @@ describe("somebody guessing from two addresses", () => {
it("the owner's remembered browser still gets through", async () => {
await tenGuesses();
deviceCookie = "device-token";
// The device's expiry is compared against the wall clock, not the mocked
// rate-limit clock `now`, so it is anchored to the wall clock too: an
// expiry fixed relative to `now` expired for real once the calendar
// passed it.
vi.mocked(prisma.trustedDevice.findUnique).mockResolvedValue({
userId: "owner-1",
expiresAt: new Date(Date.now() + 30 * 86_400_000),
} as never);
// The device's expiry is read against the same clock as the wait, the
// mocked `now`. It used to be compared with `new Date()`, which the mock
// does not reach, so this expiry ran out for real on 2026-10-26 at noon
// UTC, 30 days after `now`, and the test failed from then on.
rememberBrowser(new Date(now + 30 * 86_400_000));
expect(await guess("203.0.113.51", { right: true })).toBe("checked");
});
it("a remembered browser whose device has expired is throttled like anyone", async () => {
await tenGuesses();
rememberBrowser(new Date(now - 1));
expect(await guess("203.0.113.52", { right: true })).toBe("waiting");
});
it("an address the account signed in from is no exemption: a guesser can claim it", async () => {
// A session of the owner's from this address. The guesser shares the
// network, or names the address in a forwarding header; either way the
+3 -1
View File
@@ -67,7 +67,9 @@ export async function knownSignInSource(
userId: string,
request: Request,
): Promise<KnownSource | null> {
const now = new Date();
// The same clock as the throttle's own wait (`Date.now()`), so one instant
// decides both.
const now = new Date(Date.now());
let deviceToken: string | undefined;
try {