Skip to content

E-mail verification, forgot/reset password and Sign in with Google (secure rewrite) - #20

Merged
godzam merged 3 commits into
mainfrom
claude/auth-email-oauth
Nov 1, 2026
Merged

godzam merged 3 commits into
mainfrom
claude/auth-email-oauth

Conversation

@godzam

@godzam godzam commented Nov 1, 2026

Copy link
Copy Markdown
Owner

Summary

Replaces the broken account features from the freebuff/… branch (parse error in app_settings.php, a migration calling a method CodeIgniter does not have, OAuth state never sent, guessable md5(uniqid()) tokens, verification never enforced, views/routes that did not exist) with a rewrite. All three features are off by default (Settings → Accounts & e-mail).

  • One-time links: 32 random bytes, only the SHA-256 hash is stored, single use (atomic UPDATE … WHERE used_at IS NULL), reset 1 h / confirm 3 days. New migration 20261008000000_add_account_security (users.email_verified_at, users.google_sub, user_tokens); existing users count as confirmed.
  • E-mail verification: enforced at login after the password is right (no hint for wrong passwords), never logs anybody in, the link opens a button page (POST) so mail scanners cannot use it up, resend is rate-limited and answers the same for unknown addresses.
  • Forgot/reset password: identical answer for known and unknown addresses, limited per IP and per address, Turnstile applies (turnstile_on_forgot), no automatic login afterwards, no-store + Referrer-Policy: no-referrer.
  • Google: authorization-code + PKCE + state + nonce, single-use 10-minute sign-in session, ID token checked (client, issuer, expiry, nonce, Google-verified e-mail), safe linking (an unconfirmed local account has its password replaced), respects login blocks. The button is a plain link (no inline script, so the production CSP is kept).
  • Mail from .env (mail/smtp/sendmail/log; log is development-only), php index.php console mailtest, console prune removes old tokens. No credentials in the repo.

Test plan

  • 12 new unit tests (tokens; Google claims/PKCE incl. the RFC 7636 example); 163 total pass locally with a stub runner; PHPStan and CS-Fixer clean
  • Browser run on MariaDB (79 checks, 0 failed) with a fake Google: registration with/without verification, login before confirming, link use/reuse/expiry/forged, resend and rate limits, forgot/reset (same answer for unknown addresses, single use, newer link cancels older), Google new/link/relink-refused, unverified e-mail, wrong nonce/aud/exp, callback without starting, forged and replayed state, blocked account, settings validation, phone layout
  • Upgrade path: existing users become confirmed after migrating
  • Real SMTP / mail() delivery, real Google, MySQL 8.4 and real PHPUnit: only in CI / not run locally

Generated by Claude Code

Rewrites the account flows with secure defaults; all three are off until
switched on under Settings -> Accounts & e-mail.

- One-time links: 32 random bytes, only the SHA-256 hash is stored, single
  use (atomic UPDATE), reset links live 1 hour, confirmation links 3 days.
- Verification is enforced at login (after the password is right), never
  logs anybody in, and the link needs a POST so mail scanners cannot use it.
- Forgot password answers the same for known and unknown addresses and is
  rate limited per IP and per address; Turnstile applies.
- Google: authorization code + PKCE + state + nonce, single-use sign-in
  session, only Google-verified addresses, safe account linking.
- Mail config comes from .env (mail/smtp/sendmail/log) with a mailtest
  console command; `console prune` removes old tokens.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYbe92Bns1mKdy23b7txT8
With no (or an unreachable) database, /forgot-password and /google/* answered 500
instead of 404: the controllers loaded the user model, which opens the database,
in their constructors. They now check the setting first and load the database
pieces only after that. A verification link opened after the feature was switched
off says "link not valid" instead of failing. Found by the PHPUnit smoke test in CI.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYbe92Bns1mKdy23b7txT8
@godzam
godzam marked this pull request as ready for review November 1, 2026 05:34
@godzam
godzam merged commit d1f10cd into main Nov 1, 2026
10 checks passed
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.

2 participants