0

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

  • dynamic_urls is a really weird name for this. account_activation_tokens would be much clearer. – meagar Jul 12 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 Jul 14 at 19:17
0

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 john@example.com, 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 "john@example.com", 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.

| improve this answer | |
  • 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 Jul 20 at 22:35
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

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.