Repository navigation
fix(auth): tell the user a reset link is dead BEFORE they choose a password (TIGER-387) - #365
Merged
Merged
Conversation
…ssword (TIGER-387) A password-reset link that had expired, been used, or was malformed rendered a perfectly ordinary "Set a new password" screen. The user chose a password, typed it twice, submitted — and only then learned the link was dead. The refusal was always correct and safe; it just arrived after the work instead of before it. The people who hit that are the ones already locked out. The token is now checked on GET, and a dead one gets a card that says so and offers a one-click "Request a new link" straight to /auth/forgot. Tiger_Service_Authentication::resetTokenIsLive() is a READ-ONLY peek: it never consumes the challenge and never costs an attempt, so it is safe at render time. It checks the challenge only, not the code — a code cannot be verified without spending an attempt, and the challenge id is an opaque v4 UUID the link holder already has, so answering leaks nothing the link did not. resetPassword() still re-checks everything at submit, because a token can expire in between. Verified on tiger-dev through the browser, all three branches: bogus/empty token -> dead-link card live token -> form renders, reset succeeds, signs in at ?pwreset=1 consumed token -> dead-link card (the realistic case: an old link in the inbox) Tests: two integration tests, including that a peek does not burn the token and that unknown / consumed / expired / wrong-type challenges all read as not-live. Mutation-checked — an always-live stub fails them. i18n: the three new keys are in all six shipped locales. A missing key would render the raw key on a non-English page, since Zend_Translate has no fallback locale configured here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013vdyTeoDrGE4hKxt53TsE8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found by the new UI probe (PR #364), verified in a real browser, fixed here.
The bug
A password-reset link that had expired, been used, or was malformed rendered a perfectly ordinary "Set a new password" screen. You chose a password, typed it twice, submitted — and only then got "This reset link is invalid or has expired."
The refusal was always correct and safe: fails closed, clear message, nothing written. The defect is that it arrived after the work instead of before it. And the people who hit it are the ones whose link already expired — already locked out, already frustrated.
The error also offered no way forward: "Back to sign in", but no way to request a new link.
The fix
The token is checked on
GET. A dead one gets a card that says the link expired and offers a one-click Request a new link →/auth/forgot.Tiger_Service_Authentication::resetTokenIsLive()is a read-only peek:resetPassword()still re-checks everything at submit. A token can expire between render and submit, so the submit-time gate stays as defence in depth.Verified on tiger-dev, through the browser
?pwreset=1The middle row is the one that matters most — the guard must not break real reset links. The third is the realistic case: an old link still sitting in someone's inbox.
Tests
Two integration tests in
AuthenticationTest:email_loginchallenge must never open the reset screenMutation-checked: stubbing
resetTokenIsLive()to always return true fails them. Full unit suite green (1195 tests); the reset integration tests pass alongside the three pre-existing ones.i18n
Three new keys, added to all six shipped locales (en/es/pt/fr/de/hi). This isn't optional here —
Zend_Translateis configured withdisableNoticesand no fallback locale, so a missing key renders the raw key as page text on a non-English page.tlhis left alone, consistent with its existing tracked gap.Scope
core/controllers/AuthController.php,core/views/scripts/auth/reset.phtml, one new@apiservice method, six language files, one test file. No schema change, no migration, no change to the submit path.🤖 Generated with Claude Code
https://claude.ai/code/session_013vdyTeoDrGE4hKxt53TsE8