Skip to content

fix(auth): store only SHA-256 hashes of reset/verify one-time tokens#800

Open
k08200 wants to merge 1 commit into
mainfrom
fix/hash-onetime-tokens
Open

fix(auth): store only SHA-256 hashes of reset/verify one-time tokens#800
k08200 wants to merge 1 commit into
mainfrom
fix/hash-onetime-tokens

Conversation

@k08200

@k08200 k08200 commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • User.verifyToken / User.resetToken stored the raw emailed token, while Device.tokenHash already held only a SHA-256 hash — an inconsistent standard where a DB read (backup leak, replica misconfig, SQLi elsewhere) yielded directly usable password-reset / email-verify links.
  • New one-time-token.ts (mintOneTimeToken/hashOneTimeToken): all 3 mint sites (register, forgot-password, resend-verification) now store the hash and put the raw token only in the email; both redemption sites (reset-password, verify-email) hash the presented token for lookup. Atomic single-use updateMany semantics unchanged.
  • Migration nulls outstanding plaintext tokens — they can no longer be redeemed against hashed lookups. Affected users simply re-request (verify links were 24h-lived, reset links 1h).
  • Security review note (accepted trade-off): during a zero-downtime deploy overlap, a token minted by the old container after the migration ran would be stored plaintext and never match — effect is a re-request, not a bypass.

Type

  • Security fix (non-breaking hardening)

Checklist

  • My code follows the code style of this project (biome errors-only gate clean)
  • I have added tests that prove my fix is effective (2 new hash-storage assertions; 2 existing round-trip tests rewired to use the emailed raw token)
  • New and existing unit tests pass locally with my changes
  • TypeScript compiles with no errors (tsc --noEmit)

Test plan

  • vitest run in packages/api — full suite 2109 passed (routes-auth.test.ts 64/64: hash-storage assertions, reset/verify round trips through hashed lookup, single-use, expiry, enumeration-safe responses)
  • tsc --noEmit — clean; biome check --diagnostic-level=error src/ — clean
  • Dual review completed: security review (no blocking findings — SHA-256 on a 256-bit random token is the correct scheme; all 5 mint/redemption sites covered; migration idempotent) and code review (no blocking findings; removes a pre-existing verifyToken identifier shadow)
  • Post-deploy: request a password reset and click the emailed link — resets successfully; DB row shows a 64-hex hash distinct from the emailed token

verifyToken/resetToken were stored as raw hex in the User table while
Device.tokenHash already stored only a SHA-256 hash — a DB read (backup
leak, replica misconfig, SQLi elsewhere) yielded directly usable
password-reset and email-verify links.

- new one-time-token.ts: mintOneTimeToken/hashOneTimeToken
- 3 mint sites (register, forgot-password, resend-verification) store
  the hash and email the raw token only
- 2 redemption sites (reset-password, verify-email) hash the presented
  token for lookup; atomic single-use semantics unchanged
- migration nulls outstanding plaintext tokens (users re-request;
  verify links were 24h-lived, reset links 1h)
@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
klorn-web Ready Ready Preview, Comment Jul 9, 2026 3:53pm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant