Password Reset Token Expiration

Password Reset Token Expiration

Paige Niedringhaus writes a nice blog post called Password Reset Emails In Your React App Made Easy with Nodemailer, where she outlines how to use Nodemailer + ReactJS + Node.js + MySQL + Sequelize to build a simple password reset flow. Everyone reading these words has doubtless gone through the familiar password reset song and dance. Enter your email into a form field, receive an email with a link, click the link, reset your password, voilà!

Page Niedringhaus

Sidenote about blogging

I appreciate Paige’s candid approach in her blog post. In her article she covers her thoughts, assumptions, research notes, and willingness to put her code out there. I’m a big believer in blogging ahem goes without saying, I know.

Why Blogging? I’m a software developer. I initially started my blog back in 2006 as way to keep track of problems I’d been facing. At that time, I clearly remember solving the same problem a year earlier and was dismayed at having to re-figure something I had solved previously. So my blog was born as sort of a journal of problems. As I learned more, the blog grew into more of a how to do something I had figured out.

These days, it’s starting to turn into lessons learned from my many years in many different organizations. A sort of what works, what doesn’t approach. This post is the latter.

Back to the issue at hand

Resist the temptation to over engineer, your future self will thank you.

Password reset flow seems simple on the surface, but when you get down into the details, it because apparent there are several moving pieces. The article is quite long which just goes to show, it’s not exactly a walk in the park to implement a solid password reset workflow. Paige does a good job putting together a simple workflow, NOT over-engineered. There is always the temptation to over-engineer a feature we are developing. Early optimization falls into this, so does unneeded abstraction layers.

Over-engineer: It's What We Do™

Paige’s article mentions using Nodemailer + Gmail to send emails. This is a great quick and dirty solution for a hobby app, however I would go with SparkPost for a production level solution to email sending.

One small suggestion.

As is common, and how I built them for many years, when creating the special reset password link, you need a code to validate later on. The obvious approach is to generate a hash and store the hash in the user’s record. You can see Paige utilizing this approach in her forgotPassword.js code.

  const token = crypto.randomBytes(20).toString('hex');
  user.update({
    resetPasswordToken: token,
    resetPasswordExpires: Date.now() + 360000,
  });

There is a resetPasswordToken and even a resetPasswordExpires field in the user model. Using this method, the system has to hit the database to store the token and again later when it needs to validate the token, then a third time when removing the reset code (presumable it does this while updating the password). It’s time to over-engineer and optimize this 😛

The fastest operation is one that doesn’t happen
— Someone on the Internet

Many years ago, while reading about some code optimization, I came across this idea (can’t remember where, when, or the exact phrase). The fastest optimization is one that doesn’t happen. What does it even mean? In our situation. What if we could create a unique code and expires without storing it in the database?

Without further Ado

Again, I learned this from a StackOverflow answer. Here is the solution I commonly use instead of storing a token. We do have some criteria we need to meet.

  1. Token is created and validated in code, nothing is stored in the database
  2. Token must expire after a set period of time
  3. Token is unique to the user
  4. Token can only be used once. A token used to change a password cannot be used again.

We’ll need four functions to accomplish these goals.

Our generateResetCode() function will receive a user object (pulled from the database). First it will generate a timestamp hash in base64. Then, it will use the user object to generate a userString. The userString is hashed. Those two hashes are combined to make the reset code.

function generateResetCode (user) {
  // create ISO String
  const now = new Date()

  // Convert to Base64
  const timeHash = Buffer.from(now.toISOString()).toString('base64')

  // User string
  const userString = getUserString(user)
  const userHash = getUserHash(userString)

  return `${timeHash}-${userHash}`
}

The two functions we depend on are getUserString and getUserHash. The userString is a concatenated string of properties on the user. Items we need to match against later. Remember one of the important criteria was Token can only be used once. By including both the updatedAt and encPassword fields in this user string, we ensure that when those fields change, this string (token) will no longer be valid. The getUserHash function converts the string into an MD5 hash. In this case, I deem MD5 a suitable enough cyrpto algorithm since it’s fast and meaningful collisions would be extremely rare.

function getUserString (user) {
  return `${user.id}${user.email}${user.encPassword}${user.updatedAt}`
}

function getUserHash (string) {
  return crypto
    .createHash('md5')
    .update(string)
    .digest('hex')
}

I should note. The URL I send the user also includes their userId. I use UUID4 ids in my database, so I don’t consider this much of a security issue. We use it later to look up the user to validate the reset token.

We now have a token that includes a hash for the timestamp and a hash for the user data. The only item left is to have a validateResetCode() function. In this function, we split apart our hashes into a timeHash and reqUserHash (requested user hash). We convert our timeHash from base64 into ASCII so we can check the HOURS_DIFF. This HOURS_DIFF is a constant you set somewhere in your code. I’ve set mine to 24.

I’m using the differenceInHours() function from the date-fns date utility library to compare the timestamp we receive to the current timestamp and check if we’ve exceeded 24 hours. If it passes this test, then we pull the user from the database (remember I said the user id was part of the password reset link).

When we have the user from the DB, we check a few things.

  1. If the user doesn’t exist, then fail
  2. Then we generate the userString and userHash again
  3. If you recall, our user string had id, email, encPassword (encrypted password), and updatedAt. If any of these fields changes, it will change the hash and it will not match what has been passed to us.
  4. Finally we compare the hash passed to us by the client with the hash we generated. If they match, then the password reset is valid.
function async validateResetCode (id, code = '', ctx) {
  // Split code into parts
  const [ timeHash, reqUserHash ] = code.split('-')

  const timestamp = Buffer.from(timeHash, 'base64').toString('ascii')

  // If more than 24 hours, then fail
  const diff = differenceInHours(new Date(timestamp), new Date())
  if (Math.abs(diff) > HOURS_DIFF) return false

  // Get record from DB
  const user = await this.getUser(id, ctx)

  // If nothing found, then bail
  if (!user) return false

  const userString = this.getUserString(user)
  const userHash = this.getUserHash(userString)

  return (reqUserHash === userHash)
}

Recap

Here we looked at the infamous password reset flow. We’ve taken a slightly different approach for token validation. Instead of storing the token, we generate one on the fly and will compare it later with the same set of data points. This new approach saves us two updates to the database, first storing the token, and later removing the token. It also saves us from having to hit the database for an expired token. Thanks to Paige for writing up her article (most people have no clue how time consuming a lengthy tech article can be).