My own suggestion to anyone reading this... version your password hashing mechanics so you can upgrade hashing methods as needed in the future. I usually use "v{version}.{salt}.{hash}" where salt and the resulting hash are a base64 string of the salt and result. I could use multiple db fields for the same, but would rather not... I could also use JSON or some other wrapper, but feel the dot-separated base64 is good enough.
I have had instances where hashing was indeed upgraded later, and a password was (re)hashed at login with the new encoding if the version changed... after a given time-frame, will notify users and wipe old passwords to require recovery process.
FWIW, I really wish there were better guides for moderately good implementations of login/auth systems out there. Too many applications for things like SSO, etc just become a morass of complexity that isn't always necesssary. I did write a nice system for a former employer that is somewhat widely deployed... I tried to get permission to open-source it, but couldn't get buy in over "security concerns" (the irony). Maybe someday I'll make another one.
For example, with unsuitable algorithms like sha256, you get this, which doesn't have a version field:
import hashlib; print(f"MD5: {hashlib.md5(b'password').hexdigest()}")
print(f"SHA-256: {hashlib.sha256(b'password').hexdigest()}")
MD5: 5f4dcc3b5aa765d61d8327deb882cf99
SHA-256: 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
But if you use a proper password hash, then your hashing library will automatically take care of versioning your hash, and you can just treat it as an opaque blob: import argon2; print(f"Argon2: {argon2.PasswordHasher().hash('password')}")
import bcrypt; print(f"bcrypt: {bcrypt.hashpw(b'password', bcrypt.gensalt()).decode()}")
from passlib.hash import scrypt; print(f"scrypt: {scrypt.hash('password')}")
Argon2: $argon2id$v=19$m=65536,t=3,p=4$LZ/H9PWV2UV3YTgF3Ixrig$aXEtfkmdCMXX46a0ZiE0XjKABfJSgCHA4HmtlJzautU
bcrypt: $2b$12$xqsibRw1wikgk9qhce0CGO9G7k7j2nfpxCmmasmUoGX4Rt0B5umuG
scrypt: $scrypt$ln=16,r=8,p=1$/V8rpRTCmDOGcA5hjPFeCw$6N1e9QmxuwqbPJb4NjpGib5FxxILGoXmUX90lCXKXD4
This isn't a new thing, and as far as I'm aware, it's derived from the old apache htpasswd format (although no one else uses the leading colon) $ htpasswd -bnBC 10 "" password
:$2y$10$Bh67PQAd4rqAkbFraTKZ/egfHdN392tyQ3I1U6VnjZhLoQLD3YzRe* account ids are numeric, and incrementing
* included in the URL after login, e.g. ?account=123456
* no authentication on requests after login
So anybody moderately curious can just increment to account_id=123457 to access another account. And then try 123458. And then enumerate the space to see if there is anything interesting... :face-palm: :cold-sweat:
A friend at the company started poking around in the CMS. Turns out the login system worked by giving the user a cookie with the mongodb document id for the user they’re logged in as. Not signed or anything. Just the document id in plain text. Document IDs are (or at least were) mostly sequential, so you could just enumerate document IDs in your cookie to log in as anyone.
The ceo told us it wasn’t actually a security vulnerability. Then insisted we didn’t need to assign a CVE or tell any of our customers and users. He didn’t want to fix the code. Then when pushed he wanted to slip a fix into the next version under the cover of night and not tell anyone. Preferably hidden in a big commit with lots of other stuff.
It’s become a joke between us too. He gives self taught programmers a bad rep. These days whenever I hear a product was architected by someone who’s self taught, I always check how the login system works. It’s often enlightening.