I'm build the backend for a web app. When a new user goes to the site and clicks the Sign Up button, they'll fill out a super simple form asking them for their username + password and they'll submit. This prompts the server to send a verification email to that email address. They'll then check their email, click a link (which verifies their email) and then be routed to the login page so they can sign in if they choose.
In order to verify their email, when the server generates the email it will need to create (and store) a verification token (likely a UUID) and attach it to this link in the email, so that the link looks something like:
Where vt=12345
is the "verification token" (again likely a UUID). So the user clicks this link and my GET v1/users/verify
endpoint looks at the token, somehow confirms its valid, and makes some DB updates to "activate" the user. They can now log in.
Similar scenarios for when a user wants to unsubscribe from receiving email, or when they can't remember their password and need to recover it so that they can log in.
Unsubscribe
User wants to stop receiving emails but still wants to use the app. They click an "Unsubscribe" link in a weekly newsletter we send them. This link needs to contain some kind of similar "unsubscribe token" that, like the verification token above, is generated + stored on the server, and is used to authenticate the user's request to unsubscribe from email.
Recover Password
Here the user has forgotten their password and needs to recover it. So at the login screen they click the "Forgot my password" link, and are presented with a form where they must fill out their email address. Server sends an email to that address. They check this email and it contains a link to a form where they can enter their new password. This link needs to contain a "reset password token" that -- like the verification token above -- is generated + stored on the server, and is used to authenticate the user's request to change their password.
So here we have three very similar problems to solve, all requiring the use of what I'm calling "one-time only (OTO) security tokens". These OTO tokens:
- Must be generated server-side and persisted (maybe to a
security_tokens
table) - Must be something that can be attached to links that we'll expose from inside of emails
- Must only be valid one time: once they click it, the token is "used" and cannot be reused
My question
The solution I came up was simple...almost too simple.
For the tokens I am just generating random UUIDs (36-char) and storing them to a security_tokens
table that has the following fields:
[security_tokens]
---
id (PK)
user_id (FK to [users] table)
token (the token itself)
status (UNCLAIMED or CLAIMED)
generated_on (DATETIME when created)
When the server creates them they are "UNCLAIMED". When the user clicks a link inside the table they are "CLAIMED". A background worker job will run periodically to clean up any CLAIMED tokens or to delte any UNCLAIMED tokens that have "expired" (based on their generated_on
fields). The app will also ignore any tokens that have been previously CLAIMED (and have just not yet been cleaned up).
I think this solution would work, but I'm not a super security guy and I'm worried that this approach:
- Possibly leaves my app open to some type of attack/exploit; and
- Possibly reinvents the wheel when some other solution might work just as well
Like for the 2nd one above I'm wondering if I should be using a hash/HMAC/JWT-related mechanism instead of a dead simple UUID. Maybe there's some smart crypto/security folks who found a way to make these tokens contain CLAIM status and expiration date themselves in a secure/immutable fashion, etc.