CREATE TABLE password_reset_tokens (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX idx_reset_tokens_hash ON password_reset_tokens (token_hash);
CREATE INDEX idx_reset_tokens_user ON password_reset_tokens (user_id);
import { randomBytes, createHash } from "crypto";
import { Pool } from "pg";
const TTL_MS = 1000 * 60 * 30; // 30 minutes
function hashToken(token: string): string {
return createHash("sha256").update(token).digest("hex");
}
export interface ResetRow {
id: string;
user_id: string;
expires_at: Date;
used_at: Date | null;
}
export class PasswordResetService {
constructor(private readonly db: Pool) {}
async create(userId: string): Promise<string> {
const token = randomBytes(32).toString("hex");
const expiresAt = new Date(Date.now() + TTL_MS);
await this.db.query(
`INSERT INTO password_reset_tokens (user_id, token_hash, expires_at)
VALUES ($1, $2, $3)`,
[userId, hashToken(token), expiresAt]
);
return token; // plaintext returned only for emailing
}
async verify(token: string): Promise<ResetRow | null> {
const { rows } = await this.db.query<ResetRow>(
`SELECT id, user_id, expires_at, used_at
FROM password_reset_tokens WHERE token_hash = $1`,
[hashToken(token)]
);
const row = rows[0];
if (!row) return null;
if (row.used_at) return null;
if (row.expires_at.getTime() < Date.now()) return null;
return row;
}
async consume(id: string): Promise<void> {
await this.db.query(
`UPDATE password_reset_tokens SET used_at = now()
WHERE id = $1 AND used_at IS NULL`,
[id]
);
}
}
import { Router, Request, Response } from "express";
import { PasswordResetService } from "./PasswordResetService";
import { findUserByEmail, updatePassword } from "./userRepo";
import { sendResetEmail } from "./mailer";
export function authRoutes(resets: PasswordResetService): Router {
const router = Router();
router.post("/password-reset", async (req: Request, res: Response) => {
const email = String(req.body.email || "").toLowerCase();
const user = await findUserByEmail(email);
if (user) {
const token = await resets.create(user.id);
await sendResetEmail(email, token);
}
// identical response prevents account enumeration
return res.json({ message: "If that email exists, a reset link was sent." });
});
router.post("/password-reset/confirm", async (req: Request, res: Response) => {
const { token, password } = req.body;
if (typeof token !== "string" || typeof password !== "string") {
return res.status(400).json({ error: "Invalid request" });
}
const row = await resets.verify(token);
if (!row) {
return res.status(400).json({ error: "Invalid or expired token" });
}
await resets.consume(row.id);
await updatePassword(row.user_id, password);
return res.json({ message: "Password updated" });
});
return router;
}
Password reset flows are a classic place to leak security if raw tokens are stored in the database. This snippet demonstrates the safer pattern: a high-entropy random token is generated and emailed to the user, but only a SHA-256 hash of it is persisted. If the database is ever read by an attacker, the stored hashes are useless for redemption because the plaintext token never touches disk.
In passwordResetTokens migration, the password_reset_tokens table stores token_hash (unique) rather than the token itself, alongside an expires_at timestamp and a nullable used_at. The unique index on token_hash lets lookups be a single indexed equality check, and expires_at encodes the short-lived nature of the token directly in the row so expiry can be enforced in SQL.
In PasswordResetService, create uses crypto.randomBytes(32) for the plaintext — 256 bits of entropy makes brute-forcing infeasible — and derives the hash with hashToken, a thin wrapper over createHash('sha256'). A plain SHA-256 is appropriate here (unlike user passwords) because the input is already high-entropy and unguessable, so a slow adaptive hash like bcrypt would add cost without security benefit. Only the plaintext is returned to the caller for emailing; the row keeps the digest.
The verify method is where the trade-offs show. It hashes the incoming token, looks up the matching row, and rejects it if used_at is set or expires_at has passed. Marking the token consumed in consume enforces single use, closing the window where an intercepted link is replayed. Note that lookups are keyed by hash, so a timing side channel on the string compare is avoided — the database does the comparison on an indexed column.
In authRoutes, POST /password-reset always responds with the same generic message regardless of whether the email exists, preventing account enumeration. POST /password-reset/confirm calls verify then consume inside the same request, and only updates the password if verification succeeds. A pitfall worth noting: expired and used rows should be pruned by a periodic job, since they accumulate and the unique index only prevents hash collisions, not unbounded growth.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
Share this code
Here's the card — post it anywhere.