The implementation I feel most comfortable with:
1. Generate a 256+ bit cryptographically secure random number and base64 it to create a token.
2. Record that token in your database, timestamped, along with the user account for which the token was requested.
3. Mail the token to the user's email address.
4. When the user returns to the site after recovering the token, use that token to look up their account from the database.
5. Expire tokens within single-digit hours so users don't end up accidentally banking password-equivalents in their email accounts.
6. When a user changes their password or requests another password reset, expire all tokens already associated with their account.
I would also recommend:
(a) Not having any in-band administration functionality in your application; instead, have a separate admin application, attached to the same databases, available only on a VPN.
(b) Require 2-factor authentication (such as Duo Security) for both admin VPN access and admin login.
A knock-on benefit of (a): your admin functionality is easier to build, because it doesn't have to do the UI/UX chinups your normal exposed app code has to do; crappy looking admin screens nobody but your employees see are generally fine.
I'm not trying to drum up business. I would strongly prefer that you not try to be clever with this feature. Or, you can ignore the audit and just be the subject of a blog post like this sometime in the future.
No nonce is generated and nothing is stored. The user is emailed a link with her user ID and a token that's a hash of (last login timestamp + the user's ID + the user's (hashed) password + current timestamp). The token is HMAC-signed with the site's secret key.
This way the token automatically expires if the user either successfully changes her password (the password hash will change) or manages to log in (last login timestamp changes).
It seems that in Django password reset tokens are valid forever, but it would be trivial to add the current timestamp to the token and include it when computing the HMAC signature; then the password reset form would check if the token has been generated recently enough.
I like this method because you never need to touch the database and store tokens; it's all fairly stateless.
Personally, I have this particular bit of appsec down cold, and if I was building a new app, I wouldn't even think about it: I'd use a random token and save it in the database.
I wouldn't roll my own password reset feature if I can just take the builtin one from Django, which is what I did.
Note though that that's not the case for 3rd-party password reset libraries or, more likely, the all-purpose security library that provides it. I'd be very wary about using a 3rd party library for password reset unless they've got a credible for story for it having been reviewed.
Django: Good.
3rd Party Library: Less Good
Just Using A Random Token: Good
Cryptography: You Will Perish In Flames
Asking this since it's probably the most widely used authentication gem in Rails etc...
https://docs.djangoproject.com/en/1.4/ref/settings/#std:sett...
Haven't looked too closely at it but Bruno Renié has a Django app that has done most of the heavy-lifting to make password reset customizable by providing as class-based views and changing the timeout period granularity to seconds (it defaults to 172800 seconds or 2 days):
The main criticism of both this and the contrib.auth are that they use the (hashed) user info directly, rather than generating a random code and associating it with the user in a lookup table.
This app actually appears (on my brief inspection) to be less secure than the django.contrib.auth one, since it is using the django.core.signing.loads/dumps methods with a simple static salt, whilst the contrib.auth uses django.contrib.auth.tokens.PasswordResetTokenGenerator which includes a bunch more state in the token, so that it's auto-invalidated if the user subsequently logs in, changes their password, or other things).
Personally, I wouldn't recommend it.
I'm torn between the "standard" contrib.auth implementation, and the basic {random token, user, expiry} model espoused by tptacek and others throughout this thread.
I am curious as to why the Django devs implemented this the way they did though, given the significant added complexity.
Additionally, this token could be valid for a very long time.
I would probably flag this approach in an assessment.
It doesn't handle point 6 of tptacek's list though; that subsequent tokens should invalidate all those prior. You could do that by adding an incrementing 'password_resets' or 'last_reset_issued_at' field to the User model, and including that in the token generator state, but it feels a bit clunky.
I'm not sure why people are placing such an emphasis on DB avoidance; it seems to me that password-reset activities should be a relatively minor source of load for your application in virtually all circumstances.
The token is a password equivalent, and should thus not be stored in clear (use scrypt or a similar scheme).
I've tested many many many many applications in the last X years and not one of them has ever done this, nor would I ever recommend that they do it.
Probably, yeah...
In the case they only have read access to the token DB, and need write access, or read access to another part of the system, it becomes an attack vector.
It is not a cargo-cult measure, and it doesn't add much complexity (just re-use your password encoding logic).
The same goes for session tokens, BTW.
> [...] nor would I ever recommend that they do it.
Maybe you should reconsider that. I know who you are, and how knowledgeable and experienced you are, but in this case I think you're just wrong.
A cargo cult is not an argument.
I would not hash reset tokens, but I didn't downmod you for suggesting it. I would get mad at you if you worked on my team and dinged a client for not doing it, though. :)
And now you're begging the question!
Actually, some people use it :-).
It shuts down an attack vector, and it's cheap to implement. Why is it silly? My rule of thumb is to treat all passwords equivalents in the same way.
I'm honestly surprised by your hostility towards the idea (not mine, BTW), and by the downvotes for promoting a strategy that provably (in the math sense) increases security.
http://stackoverflow.com/questions/549/the-definitive-guide-...
Edit: Even if it were just me, it doesn't make the argument invalid.
Your arguments so far are
1) it's unlikely to be the weakest link. Textbook case of Murphy's law,
2) nobody does it, and
3) I've never recommended it, therefore it's useless.
I don't understand how you can resort to that... Seriously, I'm at loss here. Are you waiting for a high profile attack to react?
I know you have a reputation to defend, but I think you screwed it somehow in this case.
At least, it proves you're not a machine :-)
:)
Edit: random Divine Comedy song: http://www.youtube.com/watch?v=EN65hsrtg94
And with 6, i suggest expire at step 4.
(I thought about it and decided to just have one expiry instruction).