Back to Feed
article 2m read

Login checked the password, then forgot which password it checked

Password and email login in my scheduling app verified the credential in one transaction and issued the session in another, so a reset landing in between still got a session. PIN login already carried a proof forward; now the others do too, and sign-up recovery got the same treatment.

Article DepthPractice

Flow

Cases the tests must cover

  1. 1Hash changed

    Step two sees a new password hash: no session, audit entry or token.

  2. 2Hash removed

    Step two sees no password hash: same refusal.

  3. 3No proof

    Step two is called with no proof: it refuses before any permission lookup.

  4. 4Permission revoked

    The second permission check says no password login: refusal.

  5. 5Email changed or cleared

    Email code login with an old address: refusal.

  6. 6Admin email edit

    Same address after normalizing deletes nothing. A failed delete stops the edit.

Already-reset passwords could still get a session from my login code. It checked that the account still existed. It never checked that the password was the same one.

Two steps with a gap

Login in my scheduling app has two steps. Step one locks the user, checks the password and saves the result. Step two locks the user again and creates the session. I'd guess the split is there because several login types share step two.

The trouble was the gap. If an admin reset the password, removed it, or changed the email between the two steps, step two still saw an active user and logged the person in. Email code login had the same gap.

PIN login was already safe. It passed a small proof of what it had checked, and step two checked that proof again. Password and email login didn't.

The gap is tiny. I still want it shut. People reset passwords when they fear a leak, and that's when an attacker keeps trying.

Passing the proof along

Now step one hands over the exact stored password hash it matched, or the email it verified. A hash is the scrambled copy of a password that the app keeps. Step two locks the user and compares:

if (!proof || fresh.hashOnFile !== proof.hash) throw new LoginRefused();

No proof means no session. Step two also checks, under the same lock, that the user is still allowed to log in with a password. Every failure shows the same plain login error, so nobody learns that a reset was in progress.

Sign-up recovery too

A new sign-up can recover its owner account for a while, based on a verified email. If an admin changed that email, the old sign-up still worked. Now changing the email deletes those sign-ups, but only when the address really changed. If that delete fails, the whole edit stops. Recovery also checks the current email against the verified one.

Every simulated change ends with no session

The tests fake the database. They cover a changed or removed password, a revoked permission, a missing proof, and a changed or cleared email. Each one must end with no session. Next I want a test that forces a reset between the two steps on a real database.

Keep reading

Pick up a connected idea or branch into a nearby one.

5 paths forward