Logging is more interesting. I don't necessarily mean printf style logging either. Think about audit trails. For example:
One way to do a password reset is to have:
/forgot_password/
GET:
1) Shows a form asking for your email address.
POST:
1) Generates a security token
2) Sends an email with the token
3) Shows "Check your inbox!"
/reset_password/?token=abc123
GET:
1) Verifies the security token
2) Shows a form asking for your new password
POST:
1) Re-verifies the security token
2) Sets the new password
3) Expires the security token
Now, in this case, you'll notice that the security token needs to be stored somewhere. Where do you put it?Well, one easy thing to do would be to add a column to the `users` table called `password_reset_token`. But then, of course, you're gonna want to expire a token that's not used in a while, so you need a `password_reset_token_created_at`. And surely you'll want to know which IP address the request came from, in case you start seeing a pattern of abuse. And what if I request my password be reset twice because the first email takes a little long to show up? When the first email finally comes, will the token already be invalid?
An alternate approach would be to restructure the URLs in a CRUD style:
/password/
GET:
Same as GET /forgot_password/
POST:
Similar to POST /forgot_password/
Store the reset request in a `passwords` table
/password/:token/
GET:
Similar to GET /reset_password/
Supports multiple active tokens
POST:
Similar to POST /reset_password/
Fills the new password into the `passwords` table
This re-imagines passwords as nouns. As a side effect, you get a complete log of whenever anyone changes their password or requests to reset it. Want to enforce password reuse policies? Do analysis of password lifetimes? Track down malicious abuse of your password reset system? Those things are now pretty easy with a schema like this: user_id: Integer
reset_token: String
hash: String
salt: String
created_at: DateTime
updated_at: DateTime
Here, the reset token is blank if the user is setting their initial password or changing their current password using their old password.The created_at timestamp is what it sounds like. The updated_at timestamp would match the created_at timestamp, except for when password reset requests are fulfilled, setting the hash and salt.
Checking a user's current password simply involves:
SELECT hash, salt
FROM passwords
WHERE user_id = ?
AND hash IS NOT NULL
ORDER BY updated_at DESC
LIMIT 1
EDIT: I really like this comment :-P I may clean it up and turn it into a submission of its own.