How to properly create a password reset token?
Password reset tokens aren't very much like passwords. They're short-lived, single-use, and - most relevantly here - machine generated and non-memorable.
- You absolutely must use a cryptographically secure random number generator to produce the token.
crypto.randomBytes
is good enough, yes. Make sure the token is long enough; something on the order of 16 bytes (128 bits) should work. - Because the "preimage" (the value fed into the hash function, in this case the token) is random, there's no need to use a slow hash. Slow hashes are for brute-force protection, because passwords suck and usually don't take long (in machine terms) to guess; 128-bit random values are not brute-forceable for any practical meaning of the term. Just do a single round of SHA-256 or similar.
- Bad idea; it just complicates things. Store all that stuff in the DB instead. Yes, you could do it statelessly by putting it in a JWT, but the DB makes much more sense:
- You'll need to interact with the DB anyhow, to reset the password.
- You'll need to bypass any caching anyhow; even if you have a distributed system they'll all need to see this change so there's no point making it stateless.
- You only need to do one DB read to verify the token (both that its value is correct and that it hasn't expired yet, which can be done in a single query), which is a relatively rare event; it's not like needing to do a session token lookup on every request.
- JWTs have expiry but there's no convenient way to make them single-use (you have to store state saying "these JWTs are no longer valid" on the server until each one expires, which kind of misses the point of JWTs.) Password reset tokens stored in the DB can (and should) be made single-use by deleting them upon verification.
- You can have a cleanup task that goes through and periodically removes expired tokens (that were never used), if you want. They don't take up that much space in the DB, though (a hash digest and a timestamp on each user, if you only want one per user valid at a time, or a hash digest, a timestamp, and a foreign key into the Users table if you want to allow multiple tokens to be valid at once for a user; both have advantages and disadvantages).
- JWTs have more potential vulnerabilities (somebody steals your signing key, somebody discovers that the key was generated insecurely and hasn't been rotated since the bug was fixed, your JWT library is vulnerable to key or algorithm confusion, etc.) compared to just checking the DB (which is vulnerable to basically nothing except SQLi, I guess, which hopefully you know how to avoid).
Your basic process makes sense.
1. Yes, you should use a cryptographic method to generate the token. Always use cryptographic randomness whenever you're doing anything that's related to security. Yes, crypto.randomBytes
is fine. Use 16 bytes (you could get away with a little less, but don't take risks unless the token will absolutely need to be typed rather than just clicked on or copy-pasted). (16 bytes translates to 24 characters of Base64 or 32 hexadecimal digits.)
2. Hash the token with a cryptographic hash such as SHA-256 or SHA-512. You don't need a password hash here: password hashes such as bcrypt are for when the input is a password that a human remembers, and are useless when the input is a randomly generated string of sufficient less. A password hash is a bit harder to use and a lot slower, and you don't need one here.
3. I don't see any advantage in adding information in the token. Information such as the expiry date and what the token is for needs to be in the database anyway.