1

There is an existing user database that has the following columns:

  1. userId (v4 uuid)
  2. created timestamp
  3. bcrypt password hash
  4. email

I must add a password reset functionality and I cannot add any new column to the table. So my plan is to implement the following flow:

  1. user triggers password reset
  2. the server emails a JWT token with expiry time in X minutes and the following claims: sub=userId and passwordResetToken=bcrypt(<bcrypt_password_hash>, 10).
  3. The user opens the link, types a new password and sends it back along with JWT token
  4. The server verifies JWT token signature, expiry time and based on userId in token loads the user information and checks whether passwordResetToken matches the calculated one based on current user state in the database.

This flow is supposed to protect against the same token being used more than once and doesn't reveal user's password hash to anyone.

Any problem with this approach? Can I use userId or created timestamp as a bcrypt salt instead of automatically generated one?

Thank you

2 Answers 2

1

This system works, though it's needlessly expensive for your server. Bcrypt is designed for low-entropy inputs that are stored long-term where there's a risk that an attacker will see the digest. That doesn't apply here; the bcrypt digest is technically low-entropy but not dangerously so on the timescale in question (I assume you're looking at a very narrow window, since you mention expiry in "minutes"), and if the attacker could breach the reset token this way they wouldn't need it anyhow (they'd have the password). Besides, you're not storing the result anyhow.

A simple hash (a single round of SHA256 or similar, over the current password hash) is perfectly sufficient and much less computationally expensive. If you want to avoid multiple reset tokens having the same secret in them even when one isn't used, you can salt the hash with a random value, but I don't think that actually adds anything; the actual secret is the JWT signing key and you could, in theory, leave the hash of the password hash out of it entirely! The only reasons to add it are to invalidate existing password reset tokens once the password changes, and to make it more difficult to forge the reset tokens if your signing key leaks (only relevant if your signing key is not also used to sign access tokens). Neither of those uses care whether the hash value is the same across multiple tokens for the same user or not. There's certainly no benefit to using a consistent field (like the userid or creation timestamp) as a salt.


The conventional approach here, assuming you can't alter the existing table, is just to create a new one for the reset tokens. The new table would be indexed (uniqueness optional) on a foreign key to the existing users table - typically either the userid or email would make sense, assuming duplicate emails aren't allowed - and additionally contain a hashed random secret (which need only be a single round of unsalted secure hash function) and an expiry. The actual password reset tokens would only have two parts, the identifier (whatever you indexed the password reset table on) and pre-hashed secret (generated on-demand via CSPRNG for each token). Upon successful reset, you'd just DELETE FROM PasswordResetTokens WHERE TokenId = @id to invalidate any copies (or additional tokens generated for that user, if you allow multiples). A scheduled job could periodically wipe out any tokens that have expired just to keep the table size small.

Given that a password reset operation needs to interact with the DB anyhow, and isn't something you can meaningfully load-balance, there's really no advantage to making the reset token a JWT unless there's extra metadata you want to stuff in there. Even if you're already using JWTs elsewhere, such as for the session/access token, adding more seems like needless complexity. Of course, that assumes you can create a DB table; if you can't do that and can't alter the existing one, then this approach might be next best.

1
  • Thank you for the detailed response. Yes, the only reason to hash the password is to avoid using the same key more than once. For example, in case a public computer was used and the token will be in browser history even after user logs out. Feb 15, 2021 at 23:10
1

Using a signed token as password reset token can be secure. For example, Django uses a signed token.

One risk with this is that the password reset JWT can be used for authentication. Your password reset mechanism creates a JWT with sub=userId. Presumably, your authentication mechanism also creates a JWT with sub=userId. These must not be interchangeable. A password reset token must not be usable as a session identifier.

1
  • Thank you for sharing the link. Yes, thanks for pointing out that access and password reset token must be different. The plan was to use different JWT scopes for access and password reset tokens and to verify if the correct token is used. If it wasn't for CBHacking answer, I would have accepted yours. Thank you! I was happy to learn someone else implemented an equivalent flow. Also it is a good idea to invalidate tokens after a successful login. Feb 15, 2021 at 23:20

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .