Back to Feed
article 3m read

Fresh PINs for every account landed in a CSV during user import, so I threw them out

The first cut of my scheduler's user import minted a plain-text PIN for everyone. Later that day it copied the old app's bcrypt hashes across instead, so people keep the passwords they already know, and a few gaps are still open.

Article DepthPractice

Flow

From the old hash to a signed-in user

The last step doesn't exist yet: old hashes keep their old cost.

  1. 1Copy the hash

    The import moves each old bcrypt hash over unchanged and clears any PIN

  2. 2Report methods only

    The report lists how each account signs in, with no secrets in it

  3. 3Allow the role

    Password login is granted to default, new and existing roles

  4. 4Check the password

    Swap the version label on a copy, then compare

  5. 5Fail the same way

    One error for a bad name or a bad password, and a short lock after too many misses

  6. 6Upgrade the cost

    Not built yet: rehash with a stronger cost after a good login

The first version of my user import gave every account a new PIN and wrote all of them, in plain text, into a CSV.

I'm moving the shift scheduler for the stores off an old PHP app and onto a new backend. My first import made a random PIN for each user, saved a hash of it, marked the account as needing a reset, and put the real PIN in a report file.

Why that file bothered me

It's a key to every account, saved on disk, waiting to be passed around. And the import makes a new PIN each time it runs, so a second run breaks every copy of the list that's already out. People also lose a password they know and get a number they've never seen.

Later the same day the PINs were gone.

What the old hashes already hold

The old app stored passwords as bcrypt hashes. A bcrypt hash keeps its version, its cost and its salt right inside the string, so the new side can check a password with nothing else. Now the import copies the old hash over as it is, and the report just says which login method each account got. Nothing secret in it.

One catch: PHP writes a different version label at the front of the hash than most JavaScript libraries expect. They mean the same thing for normal passwords, so the check swaps the label on a copy before comparing. The saved hash never changes. A test makes a hash, gives it the PHP label, and checks that the right password still works.

Who gets the password page

The login page asks for a username first, then asks the server what to show next. If the account has an old hash, it gets a password page. The account's role also has to allow password login. I gave that permission to the default roles, to new installs, and to roles that already exist, so old and new databases agree.

The same error, mostly

On the password page, a wrong name, a missing hash, a missing permission and a wrong password all get the same error. Too many misses lock the account for a short time. The login page used to promise it stored no passwords, so that line changed.

From the code I can see a few spots where the sameness slips. The first step tells you what kind of login an account uses. A locked account says so. And a wrong username probably comes back faster than a wrong password. I haven't tested any of that.

The old hashes also keep the old cost setting. Nothing upgrades them when someone logs in successfully.

Keep reading

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

5 paths forward