2

I have a Node.js/Express 4/JWT user authentication service using Passport.js, with Sequelize and MySQL for database.

In my service, upon signing up/resetting a password, the user will be redirected to a page telling them to

  • click the link in the email that was just sent, or
  • click a button to resend the email (if they did not get it)

In the database I have a dynamic_urls table for activate/reset links. The dynamic_urls table has a cap of 5 resend attempts before they are directed to contact the admin (obv. incremented with every resend). The URL itself is generated via JWT, with a payload of the user's id, password hashed password, with their account creation date, and a private key of a long "secret" string. It is then stored in the database until it is clicked or expires.

I have a few questions for best info sec practices with JWT, namely:

  1. Should I even care about having the resend button have a random URL?
    I assume that I must, because the resend request has to correspond to the correct dynamic URL, which requires user information, which must not be leaked.

And if so:

  1. What should I use for in the payload when creating the dynamic URL of the resend button?
    I assume I shouldn't use the same/similar payload that I used to make the email link. UUID maybe? (but then I'd have to presumably stash the UUID in the database as well, no?)
  2. Should I renew the resend URL with each resend attempt.
    (E.g. each resend lands user on new page with new unique resend link)

Thank you in advance!

EDIT: Passwords are hashed with bcrypt; for new users that hashed password is immediately used to make a temporary URL from a JWT, a la

jwt.sign( { data: newUser.id }, newUser.login_pass + "-" + newUser.account_created)

which gives something like http://domain.com/activate/eyJkYXRhIjoxMDI1LCJpYXQiOjE1ODU5MzAwNTd9.Y2BYzfc0oETn9kbzQ_ek_0FYTV8WDPoRBT4jmAQeQ68

2
  • dynamic_urls is a really weird name for this. account_activation_tokens would be much clearer.
    – user229044
    Commented Jul 12, 2020 at 0:11
  • Cheerfully seconded. Funnily enough, one of the many sticky notes for this project (orbiting my monitor like little, persistent, nagging stars) specifically includes a bullet for "rename SQL fields/tables." I was thinking temporary_urls because the table also contains temporary URLs for password resetting, but I'm open to suggestions.
    – SKNB
    Commented Jul 14, 2020 at 19:17

3 Answers 3

1

JWT isn't necessary or appropriate for this.

You have a database connection, and you'll need to use that connection upon initiating and completing either a signup or password reset flow, so there is no benefit to a stateless JWT. You should simply generate a random opaque token, store it in your database, and send that token to the user. The user can "redeem" that token to complete their signup or password reset flow.

Specifically, you would create a table called password_reset_tokens, with three fields:

  • email - the email for which the password reset was triggered
  • token - the random opaque token generated for this request
  • created_at - the time at which the password reset request was initiated

When a user enters [email protected], you should deliver a password reset email to that address. It should contain one of two things:

  • If you have an account, you should send the typical password reset email, including a link back to your site to complete the change request, or
  • If you have no account for that email, you should deliver an email that says "Somebody tried to initiate a password reset flow for MyGreatServce using the email "[email protected]", but we have no account for that email. If this was you, please try a different email address. If this was not you, please ignore this email".

In the case where the user does have an account, you should include a link like <a href="https://www.mygreatservice.com/password-reset?token=<unique_token>">Complete Password Reset</a>.

That page should verify the token is valid, unexpired (by comparing the current time with created_at) and that it hasn't already been redeemed. Then it should render the typical password/confirm password form.

There is no need for resend logic, the user would simply go through the password reset flow again. You do need to rate limit the number of password resets against a given email address and from a given IP address, to prevent an attacker from flooding one recipient with many emails, or from triggering emails to many recipients.

For signup, you would create a different table called account_activation_tokens. It would have similar logic, but include a user_id foreign key, linking back to the account that the token will activate. You would rate limit the "resend" button the same way you rate limit the initiation of the "password reset" flow: A finite number from one IP or against one email.

Neither of these things requires a JWT. Upon redemption of either type of token, you need to go to the database and mutate some stored state, so you might as well just store the token in the database. The "self-verifying"/stateless nature of JWTs is of no benefit here.

Additionally, JWTs for this purpose can be harmful, as you typically want to prevent reuse of a token. To do so with a JWT requires storing a JWT in revocation list and verifying each JWT against that list, so now you're storing the tokens in your database anyways and gaining no value at all from using a JWT.

6
  • You're almost certainly correct, I don't need to resend logic or make more tokens. What I am thinking of trying is having "active password" or "reset password" routine check the database first and increment accordingly, regardless of origin. E.g. clicking reset password from the home screen, or clicking the resend button on the page after the initial attempt, or refreshing the home screen and resending again, etc. all point to the same place. It makes an entry in the db if there isn't one, but it always checks and increments. Thank you, I think your post solved my problem!
    – SKNB
    Commented Jul 20, 2020 at 22:35
  • Every so often I come across an answer that makes me think "huh, did I write this and forget it?". JWTs are not the tool for the job, any time you're putting one in the DB this should be a clear sign you're using the wrong tool. I'm also suspicious of the JWT having anything to do with the password (hashed or not) - that's very much not best practice, you want as few things as possible to touch the passwords or their storage - but you covered the big ones. I will say that sending a "somebody put in the wrong email" message isn't needed; you can just say "no email means no account".
    – CBHacking
    Commented Sep 3, 2022 at 7:43
  • Oh, I will add a small note: the opaque random tokens need to be cryptographically securely random, reasonably long (128 bits is conventional), and not reusable (once they've passed through a URL one time, they should be assumed compromised). It's also a good idea to hash them in the DB, so that if somebody has read-only access to the relevant table they can't get other users' PW reset tokens, and this also prevents timing attacks (a single-round SHA2 or SHA3 hash is fine here). As for the token format, type 4 UUIDs technically work, but it's not what they're for. Just hex-encode 16 bytes.
    – CBHacking
    Commented Sep 3, 2022 at 7:47
  • @CBHacking Hashing in the DB is a good idea, but I don't see how SHA* aid with timing attacks here.
    – user229044
    Commented Sep 3, 2022 at 18:38
  • @user229044 The standard timing attack - used for linear-time brute-forcing of early-exit comparators - is to change only the first character / byte of the password / token / key / etc. until one takes minutely longer to reject (because it matches, but now the second character doesn't). Then you "lock in" that first character and repeat with the second, then with the third after you find the second, etc. Hashing solves this because the attacker can't generate strings that hash to "the same thing except one character changes" and the comparison is on the hash digest not the input string.
    – CBHacking
    Commented Sep 5, 2022 at 8:07
0

First of all I have an important question why do you have the user's password. A password should be hashed and never encrypted or in plain text you should't know the password. (Argon 2, SCrypt, Bcrypt PKDF2 are all excellent "password Hashing" functions). Secondly what JWT algorithm do you use? By default JTW payload is NOT encrypted whatever you put inside there can be seen by any one having the JWT(remember emails are not the most secure). Personally I wouldn't recommend putting the password or the password hash in there. In fact a lot of data are not needed. Just the userId and a expiration date of the JWT seems more than enough for me. Remember what you want to achieve. By presenting the JWT to the server you can be certain the user has access to the email account, job achieved. As for storing urls in the database I'm a little confused isn't the hole idea of JWT that you don't need to bother the database? Can't you simply get the userID is from the JWT and check if the expiration date is still valid? Then allow the password reset? (remember payload data can be trusted the server singed them)

  1. To answer the first question isn't the JTW random enough and also unique at the same time? If you want to make it even more random ad an extra field time JTW was created.
  2. Why bother creating new dynamic urls? re sending the same JWT isn't good enough? Having different JWT valid at the same doesn't seem as a good solution it adds unnecessary complexity.

The bellow url is an example of what you should go for in my opinion. https://your_site.co/account/reset-password?token=jwt.goes.here

1
  • Ah, sorry, I phrased it incorrectly, and you were correct to point it out! Edited OP to reflect this.
    – SKNB
    Commented Jul 14, 2020 at 18:59
0

There are a few different ways to approach this, but ultimately it depends on your specific needs and security requirements.

If you are concerned about leaking user information, then you may want to consider using a different payload for the resend button. This way, even if the resend button is compromised, the attacker would not be able to use it to access the user's information.

Alternatively, you could simply generate a new dynamic URL for each resend attempt. This would make it more difficult for an attacker to guess the URL, and would also ensure that each resend attempt is unique.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .