Skip to content

feat(auth): harden the signup/login flow (verification, anti-abuse, OAuth, sessions)#172

Open
dantaspaulo wants to merge 3 commits into
trypostit:mainfrom
dantaspaulo:feat/auth-signup-hardening
Open

feat(auth): harden the signup/login flow (verification, anti-abuse, OAuth, sessions)#172
dantaspaulo wants to merge 3 commits into
trypostit:mainfrom
dantaspaulo:feat/auth-signup-hardening

Conversation

@dantaspaulo

Copy link
Copy Markdown
Contributor

Cohesive hardening of the public signup/login surface. No new dependencies, no new tables.

Security / robustness

  • Mandatory email verification with a magic link — a global EnsureEmailVerified middleware gates page navigation (non-JSON GET) until the address is confirmed. The signed verification link verifies and logs the user in, so it works even when opened on another device/browser. Social logins and invites are already verified; self-hosted skips the gate.
  • Fix your email before verifying (UpdateUnverifiedEmailController) — a user who mistyped their address at signup can change it and resend the link, only while unverified.
  • Close OAuth account-takeover — the Google/GitHub callback only links an existing account by email (or creates a verified one) when the provider confirmed that email: Google's OIDC email_verified claim, GitHub's /user/emails API. Otherwise it returns to login without linking.
  • Password reset/change drops active sessions — reset wipes all of the user's DB sessions; changing it in settings wipes the others. An attacker with an open session no longer survives the victim's reset.
  • Anti-enumeration on forgot-password — uniform response whether or not the email exists, plus a throttle.
  • Anti-abuse on registerthrottle:5,1, an autofill-proof honeypot (hidden contact_time, cleared on mount/autofill so it never blocks a real signup), disposable-email blocking (NotDisposableEmail, config-extensible), and a per-IP daily quota (0 disables).
  • Security headers + secure session cookieSecurityHeaders middleware (nosniff, X-Frame-Options, Referrer-Policy, Permissions-Policy, HSTS in prod) and session.secure defaulting to true in production.
  • Strict email validationEmail::defaults() (strict + native) so a@b, @localhost and whitespace are rejected everywhere a new email enters (register, reset, invite, email-fixup) before they bounce.

Signup UX

  • 45s resend cooldown with an animated countdown on the verify screen.
  • Real-time password-strength meter on register and reset, mirroring the backend rules.

Config

New config/trypost.php security.* (per-IP quota, disposable blocking, extra domains) and services.github.api (overridable GitHub API host).

Tests

RegistrationAbuseTest, OauthLinkingSecurityTest, AuthHardeningTest — 17 tests / 53 assertions covering the honeypot, disposable/quota/strict-email rules, the OAuth takeover fix, the verification gate, uniform forgot-password and session invalidation.

…Auth, sessions)

Cohesive hardening of the public auth surface, no new dependencies or tables:

- Mandatory email verification with a magic link: a global `EnsureEmailVerified`
  middleware gates page navigation until the address is confirmed; the signed
  verification link verifies AND logs the user in, so it works when opened on
  another device. Social logins/invites are already verified; self-hosted skips.
- Fix email-fixup: an unverified user who mistyped their email can correct it
  and resend the link (`UpdateUnverifiedEmailController`).
- 45s resend cooldown with a countdown on the verify screen.
- Anti-abuse on register: throttle, an autofill-proof honeypot, disposable-email
  blocking (`NotDisposableEmail`, config-extensible) and a per-IP daily quota.
- Close OAuth account-takeover: only link/create by email when the PROVIDER
  confirmed it (Google `email_verified` claim; GitHub `/user/emails`).
- Password reset/change now drops active DB sessions, so an attacker with an
  open session can't survive it.
- Uniform forgot-password response to stop email enumeration (+ throttle).
- `SecurityHeaders` middleware and a secure session cookie by default in prod.
- Strict email validation (`Email::defaults` strict + native) everywhere a new
  email enters, so a@b / @localhost / whitespace are rejected before they bounce.
- Real-time password-strength meter on register and reset.

Covered by RegistrationAbuse, AuthHardening and OauthLinkingSecurity tests.
…test doubles

Two behaviour changes in this branch broke existing suites. Handled
differently, because they are different kinds of change.

Gating the app behind a verified address changes the signup funnel, which is
a product decision rather than a security fix, so it is now opt-in via
`trypost.security.require_email_verification` (default off). The trial
middleware tests pass again untouched.

Requiring the provider to have verified the email before an account is linked
or created by email IS a security fix: without it, a social account carrying
someone else's unconfirmed address takes over that account. That one stays
on, so the affected test doubles now model what a real provider returns —
Google's `email_verified` OIDC claim, and GitHub's emails API, which the
callback queries.
The parity test requires every locale to carry the same keys as `en`. Adds
the password-strength meter, the unverified-social-email notice, the signup
abuse messages, the verify-email screen copy and the uniform password-reset
status to the twelve locales that landed since this branch was cut, each
following its own file's wording and formal/informal register.
@dantaspaulo
dantaspaulo force-pushed the feat/auth-signup-hardening branch from c1849c3 to c45d8f0 Compare July 20, 2026 22:41
@dantaspaulo

Copy link
Copy Markdown
Contributor Author

Rebased on main and fixed the failing suites. Two behaviour changes in this branch were breaking them, and I handled them differently because they are different kinds of change.

Mandatory email verification is now opt-in (trypost.security.require_email_verification, default off). Gating the app behind a verified address changes the signup funnel, which is a product decision rather than a security fix — so the instance decides, and TrialMiddlewareAccessTest passes untouched.

Requiring the provider to have verified the email stays on, because that one is a genuine security fix: without it, a social account carrying someone else's unconfirmed address can take over that account. The affected test doubles now model what a real provider actually returns — Google's email_verified OIDC claim, and GitHub's emails API, which the callback queries. That is a deliberate contract change, so please push back if you would rather not take it.

Also added the new auth/passwords keys to the twelve locales that landed since this branch was cut, so the localization parity test passes.

Locally the affected suites are green (102 passing across SocialLoginControllerTest, tests/Feature/Auth and tests/Feature/Middleware), Pint is clean, and I verified locale parity with the same comparison the test performs.

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