4

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:

"https://api.myapp.example.com/v1/users/verify?vt=12345"

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:

  1. Possibly leaves my app open to some type of attack/exploit; and
  2. 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.

smeeb
  • 689
  • 6
  • 11
  • 1
    Best not to rely on UUIDs being secret https://security.stackexchange.com/q/157270/151903 – AndrolGenhald Jan 09 '18 at 15:41
  • Thanks for the pointer @AndrolGenhald (+1) - I'm all ears on anything that is better than a standard fare UUID! – smeeb Jan 09 '18 at 15:54
  • Also @AndrolGenhald I'm using Java's `UUID.randomUUID()` which uses Java's [`SecureRandom`](https://docs.oracle.com/javase/8/docs/api/java/security/SecureRandom.html) under the hood...can you confirm that I'm actually ok here? Should be secure... – smeeb Jan 09 '18 at 18:13
  • 1
    If it's using a CSPRNG then it's fine, it's just best avoided because what if someone changes how UUIDs are generated without understanding the security implications? Is there any reason not to use SecureRandom directly? – AndrolGenhald Jan 09 '18 at 18:26

1 Answers1

4

I see a few concerns with what you've described:

  1. Depending on how you generate the UUIDs, they may not be all that unpredictable -- they're not cryptographically strong tokens.
  2. You do not describe a "purpose" in your security_tokens table, which might (depending on your code) allow someone to use a token for a different purpose than was intended.
  3. Your system requires a lot of database traffic -- this might or might not be a problem, depending on the amount of traffic you anticipate receiving.

An alternative solution would be to use (for example) JWT and include in the signed data:

  1. The user ID for which it is valid.
  2. The purpose for which it is valid.

JWT itself provides the functionality for managing expirations. Note that this does generate tokens that can be 'double spent', but if they are for validating emails or unsubscribing, that is not a problem. (Is validating the same email twice a concern?)

For these tokens, all you need is a secret stored on the application server, and it incurs no additional database writes to either generate or spend the tokens. (Which, again, is an issue of scaling, so may or may not apply to your application.)

David
  • 15,814
  • 3
  • 48
  • 73
  • 1
    Thanks @David (+1) for your first concern, the UUIDs not being cryptographically-strong, I am using Java `UUID.randomUUID()` which uses Java's [`SecureRandom`](https://docs.oracle.com/javase/8/docs/api/java/security/SecureRandom.html) class under the hood. Can you confirm that they are in fact cryptographically-strong and that I should be OK here? Thanks again! – smeeb Jan 09 '18 at 18:12