Skip to content

Password reset

Kilat implements a self-service password reset flow in src/server/routes/auth.routes.ts with hashed reset tokens and pluggable email delivery.

  1. GET /forgot-password — renders the email form.
  2. POST /forgot-password — if the email exists, creates a reset token and emails a link. Always renders { status: "sent" } regardless of whether the account exists — no user enumeration.
  3. GET /reset-password?email=…&token=… — renders the new-password form, pre-filled from the query string.
  4. POST /reset-password — validates the token, checks password confirmation, hashes the new password, clears all reset tokens for that email, and redirects to /login?notice=password_reset.
const token = await createPasswordReset(user.email);
const link = `${config.appUrl}/reset-password?email=${encodeURIComponent(user.email)}&token=${token}`;
await sendMail({
to: user.email,
subject: "Reset your password",
text: `Reset your password:\n${link}\n\nThis link expires in 60 minutes.`,
html: `Reset your password: ${link} — expires in 60 minutes.`,
});

Reset tokens are 256-bit random and hashed at rest — the password_resets table stores only SHA-256(token), never the raw token. The raw token travels only in the email link.

export const RESET_TOKEN_TTL_MS = 60 * 60 * 1000; // 1 hour
Property Value
Token length 256-bit random, hex-encoded
Stored as SHA-256 hash (token_hash PK)
Expiry 60 minutes (RESET_TOKEN_TTL_MS)
Cleanup All tokens for the email cleared after reset

verifyPasswordReset checks both the hash match and the expiry timestamp. Expired or invalid tokens return a 422 with “This reset link is invalid or has expired.”

Reset emails are sent via the mailer — see Mailer for driver configuration (log, resend, mailtrap), secrets, and production setup.