16

I am developing a reliable system for token generation and validation used mainly for links in confirmation emails (reset password request, change email flow, activate an account, etc...).

There are a few things that are mandatory:

  • Token must be unique (even when two generated at the same time) in system (in database)

  • Token must be one-time use

  • Token must have expiration
  • Token cannot be guessable

From that I decided to generate token like this:

token = sha256(user.id + time + uuid(v4) + secret)

This token do not need to carry any expiration information, because it is saved in database with those columns externally.

  1. Does this token meet my requirements above points? If not, how to modify my approach?
  2. If this token meets my requirements, is there a way to simplify it while meeting my goals?

I am asking this, because I know there are some known exploits of those types of one-time use tokens sent to email and I am not sure if I will be safe.

schroeder
  • 123,438
  • 55
  • 284
  • 319
Baterka
  • 261
  • 2
  • 4
  • 3
    Basically you describe some form of a nonce. Is this what you are looking for? Why not taking an available library to generate the nonce for you? – Marcel Feb 20 '20 at 13:56
  • 1
    I switched your wording from "safe", which is undefined, to focusing on your requirements. – schroeder Feb 20 '20 at 14:04
  • 4
    Can you explain your objective for wrapping all of that extra data into a one-way hash? – trognanders Feb 20 '20 at 23:55
  • 3
    Quick note as somebody who's done this. Lots of email providers (e.g. Outlook) follow links in emails (to generate thumbnails or for "security") this breaks one time links... – hardillb Feb 21 '20 at 10:49
  • 1
    Instead of `sha256(user.id + time + uuid(v4) + secret)`, why not just use `hex_encode(random_bytes(n))`? With a CSPRNG, this should be sufficiently “safe”. You can track association to user and expiration time in a simple database table. And finally, you can make these even shorter by using Base64url encoding instead of hex encoding. – caw Feb 22 '20 at 19:47

3 Answers3

51

If you are storing all the relevant information (token, expiration time, user) in the database anyway, the only thing you need to make sure about the token is that it is impossible to guess a token.

Your token is impossible to guess if at least one of these two holds:

  • The secret remains secret. It has to have very high entropy, and never be leaked.
  • The UUID is generated using a secure random source.

Actually, your system is more complex than it needs to be. Since the token is only used to look up the info in the database, and not validate it, you could just use the UUID and nothing else - no hash, no secret, no other data. Only thing you need to make sure is that the UUID is generated with a secure random source.

Anders
  • 64,406
  • 24
  • 178
  • 215
  • 6
    I think this is really the correct answer. The UUID alone is sufficient to meet the OPs purposes. Putting additional things on top of that might make things seem more secure, but are more likely to create unexpected problems in the long term, even aside from the obvious - complicated == more maintenance – Conor Mancone Feb 20 '20 at 14:45
  • 1
    The one notable danger when using a secure random token (a type 4 UUID, or any other output of a CS[P]RNG) with an expiration is the risk of timing attacks. Making the expiration really short or limiting the number of times a user can try to use any current token they might have could work, but the simplest fix is just to use a timing-safe comparison. – CBHacking Feb 21 '20 at 07:46
  • @CBHacking Interesting point! Would a timing attack work when you are doing database lookup on the token, and not just straight out compring it to another value like you would with a hash? My gut says no, but I'm not sure I can justify that feeling with an argument... – Anders Feb 21 '20 at 07:55
  • @Anders That could very much depend on how the lookup is implemented in the DBMS at hand, in particular how indexing works, and how performant the lookup is (whether the difference between hit and miss is measurable). Mitigation approaches have been discussed before, e.g. on [SO](https://stackoverflow.com/questions/26077553/countermeasure-to-timing-attack-against-sql-select-of-hash-token). The common motive is to not perform the lookup with the (full) token and perform the token comparison within the application in a timing-safe manner. – Lukas Feb 21 '20 at 21:42
  • 4
    Instead of a v4 Token which has a limited amount of random bits (120) I would go for 32byte binary random and a url-safe encoding. This has the additional advantage that you don't have to Analyse the uuid algorithm, you are guaranteed over 128 bit security and you have the most compact data (instead of hex string) for a custom encoding. – eckes Feb 22 '20 at 01:22
  • shouldn't the token be stored hashed in database ? – Thierry Feb 23 '20 at 15:48
  • @Thierry Yeah, hashing the token in the database is good. Something light like SHA256 and no salt will do the job, given how high the entropy is. You don't give the hash to the user though. The user gets the unhashed version. – Anders Feb 23 '20 at 19:26
4

If you use this information for verification purposes only and are already storing this in the database, it would be sufficient to generate a random UUID and store it with the mata data you need (timestamp, user) in the database. I see no additional benefit over hashing these columns into a token. This setup requires you to sync through the database, ensure one time usage etc.

What you could also do is use cryptography to digitally sign your URL in similar ways that for example Amazon S3 does or as it is done with JWTs.

You would put your regular User ID and expiry timestamp in the URL. Sign this with additional data that needs to hold true as well. A Password Reset would be implemented as this

/reset-password?userid=X&expires=Y&signature=Z

Compute your signature with

    sign(userId=X,expires=Y,currentPasswordHash=..., privateKey)

When the User clicks this link verify the integrity with the signature and check it is not expired. Due to the cryptographic signature, the user can not modify the two parameters in the URL without invalidating the signature. Adding the currentHasswordHash into the signature ensure that once the user successfully changed their password, the signature is no longer valid even if it is not expired yet.

You could even laverage the fact that you do not need to store anything in the database. A sign up / opt in would not need to save the email address of an end user unless they actually clicked the opt in link.

It takes a few extra steps but it scales better as it is stateless. Also, you don't have to clean up your DB and erase old tokens. Of course you need to protect your private key at all costs, otherwise someone else could generate valid URLs.

phisch
  • 1,305
  • 10
  • 14
  • 3
    I'm not sure if a stateless approach has much benefit here because a password reset is an inherently stateful update. Using cryptography to make things stateless is mainly a benefit when the part you are making stateless is the only thing that would otherwise be stateful. To do a password reset you are already going to be checking the database and making changes, so one or two more queries won't really change very much. Granted, some might prefer the stateless approach anyway (and it does have a benefit), but it isn't as clear of a win here as it is in other places. – Conor Mancone Feb 20 '20 at 15:11
  • It all depends on the architecture and use case; I only picked password reset because it was the first of multiple examples the OP listed. It could still benefit the application if one service generates the access token and it is validated in a completely different service. for an opt-in you could create a service that validates the an emailadress without ever storing it in the first place (thinking GDPR) – phisch Feb 20 '20 at 15:16
  • 2
    I think your comment there is helpful background that might be worth working into your answer – Conor Mancone Feb 20 '20 at 15:28
0

I would reckon it is safe. Since you are using userid time, uuid and a secret.

If someone were to create their own tokens, the time alone makes it a hard factor, and with the other secrets added it will make it an annoying thing to do. So even knowing your method I would have to iterate through both time, uuid and the secret. And for what? A chance that token is in the system and not expired? And if you notice one client trying loads and loads of tokens, you just block them.

The secret and uuid are your secrets, the user doesn't know these. With the above knowledge, you will have an unique token everytime(due to the time added).

But why are you making your own method? There are a lot of token generation solutions already there.

Dr_Bunsen
  • 117
  • 5
  • "With the above knowledge, you will have an unique token everytime(due to the time added)" System can generate 2 tokens in exact same time surely, so time not making it 100% unique. You saying there are lot od token generation solutions, can you please send example? – Baterka Feb 20 '20 at 14:27
  • 1
    @Baterka You could add a counter atomically incremented on every generation (it doesn't need to be persistent as a process restart surely implies changing the time). However, your tokens are essentially random because of the use of the random UUID, so you needn't care. Note that the hashing could generate collisions, but they're extremely improbable - about as much as random UUID collisions. – maaartinus Feb 21 '20 at 01:33