diff --git a/lib/db.ts b/lib/db.ts index aa8c32a..5c3df38 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -931,8 +931,14 @@ export async function accountForResetToken(token: string): Promise datetime('now')`, - args: [token], + // reset_expires is stored as an ISO-8601 string (Date#toISOString, e.g. + // "2026-07-20T20:53:43.696Z"); compare it against an ISO "now" so the check + // is a like-for-like lexical comparison. Comparing against SQLite's + // datetime('now') ("2026-07-20 21:23:43") instead makes the "T" separator + // (0x54) always sort after the space (0x20), so an expired token would read + // as still valid for the rest of the UTC day. + sql: `SELECT id FROM accounts WHERE reset_token = ? AND reset_expires > ?`, + args: [token, new Date().toISOString()], }); return res.rows[0] ? String(res.rows[0].id) : null; } diff --git a/tests/reset-token-expiry.test.mjs b/tests/reset-token-expiry.test.mjs new file mode 100644 index 0000000..9e377b1 --- /dev/null +++ b/tests/reset-token-expiry.test.mjs @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +// In-memory libSQL so the token-expiry query runs against a real database. +process.env.TURSO_DATABASE_URL = ":memory:"; +const { ensureSchema, createAccount, setResetToken, accountForResetToken } = + await import("../lib/db.ts"); + +test("reset tokens are rejected once expired", async () => { + await ensureSchema(); + const acct = await createAccount({ + email: "reset@example.com", + passwordHash: "h", + passwordSalt: "s", + domain: "example.com", + }); + + // Valid (future) token is accepted. + const future = new Date(Date.now() + 60 * 60 * 1000).toISOString(); + await setResetToken("reset@example.com", "valid-token", future); + assert.equal(await accountForResetToken("valid-token"), acct.id); + + // Token that expired 30 minutes ago (same UTC day) must be rejected. A string + // comparison against SQLite datetime('now') would wrongly accept it, because + // the ISO "T" separator always sorts after datetime('now')'s space. + const expired = new Date(Date.now() - 30 * 60 * 1000).toISOString(); + await setResetToken("reset@example.com", "expired-token", expired); + assert.equal(await accountForResetToken("expired-token"), null); +});