Recently I was tasked with building a feature that allowed a user to reset her password via email — securely. I used Node and Express on the backend. Turns out this was both easier and harder than I expected.
JS Everywhere
We were using JWT for encoding sensitive data passed between the client and server, so I decided to stick with that because typing npm i
sounded hard.
Check out the project’s source code on GitHub, or watch the 30 second demo:
We’ll be coding this feature entirely in JavaScript. Let’s dive in!
Dependencies
- Express & JWT: I’m assuming you know enough Express to create a simple service that listens on a given route. Working knowledge of JWTs will also help; if you’re fuzzy, check out the JWT website for a refresher on JWT headers, payloads and signatures.
- Nodemailer: Super simple npm module for sending email. We’ll create a quick and dirty email template and configure it to send reset emails from our own email address — make sure you use something like dotenv if you plan on pushing to GitHub, otherwise your email password will be made public!
Theory
This is the part of the blog post I would usually skip personally, but I recommend you actually read this part, otherwise you might end up being confused when we get to implementation.
Let’s make sure we’re on the same page about a few things, security-wise:
- We’re not going to send the user her password via email.
- Because we care about security, we’re using something like bcryptjs so that we don’t store the user’s password at all. At least not in plain text. Instead we hash it, and sending the user a hashed password is worse than doing nothing at all because she will probably assume her password has been compromised.
- We’re also not going to send the user a temporary password because we can’t assume the user’s inbox is secure. If the user gets distracted and forgets to follow the link we email her, there is a live password just waiting in her inbox. Not to mention a malicious actor or jilted ex could reset the user’s password over and over, keeping her from logging into the wonderful app we made.
Instead, we’re going to generate a single-use link that the user can follow to enter a new password. This link will also expire after a set amount of time, in this case 60 minutes.
Creating a Single-Use URL
We need a way to validate our user. To do this, we’re going to embed a JWT token that identifies her in the URL we generate. When she clicks this link, our user will be presented a form that allows her to enter a new password.
As a payload, the JWT token needs to carry something that we can use to effectively fingerprint the user. Once she visits the route we send her, our server will pull the token from the route params and use it to identify her.
Before we talk about the JWT payload, let’s make sure we all understand the control flow:
Order of operations:
- User enters her email into a password reset form which sends a POST request to the endpoint
/reset_pw/user/:email
- We sign a JWT on the backend using a dynamic payload and secret key
- An email service mounted at that route in step 1 emails her a URL containing the token
- The user clicks the link and is taken to a client-side form. Then, our client-side router pulls the token off params as it resolves her request (our team used React Router to do this)
- User submits the form which makes a POST request to
/new_pw/:userId/:token
with the new password on the request body (parameters being easier to intercept) - Our then server decodes the token using a secret key unique to the user, hashes the new password, and replaces the old password hash with the new one
JWT Secret & Payload
So what should our JWT’s payload be?
This confused me until I realized I was asking the wrong question. A better question is: What should our secret key be?
The payload itself doesn’t matter as much as the fact that it matches.
Let’s say you and I are both spies, meeting for the first time. We don’t know if we can trust each other yet. Of course, no good spy actually trusts another spy, not truly; but we agree that some tests are better than others, and begin talking about ways to verify that the other is in face who she claims to be.
Before long we reach an impasse. Given our line of work, we cannot use information as a way to authenticate each other, because we might be giving something away by the very act of trying to verify it. We could try blurting out the name of the target together on three, but that’s the oldest trick in the book and neither one of us is falling for that one again.
Instead, you point out that it doesn’t actually matter what we know, but that we can demonstrate the ability to know it. We don’t need to know what’s behind door number 1, so long as we both have a key that can open it.
To make a one-time URL, we use the user’s old hashed password as the JWT secret key. When the user updates her password, we will replace the old hash with the new one, and no one can access the secret key anymore.
In order to make sure this link can’t be used and reused over and over again, the link should “expire” — meaning that our token (and by extension, the URL) should only work once.
After researching how to do this, I came across a very clever solution:
To make a one-time URL, we use the user’s old hashed password as the JWT secret key. When the user updates her password, we will replace the old hash with the new one, and no one can access the secret key anymore.
This helps to ensure that if the user’s password was the target of a previous attack (on an unrelated website), then the user’s created date will make the secret key unique from the potentially leaked password.
Get it?
It took me a bit to wrap my head around.
To make this even more secure, we can concatenate the old password hash with the user’s createdAt
value, so that if someone intercepts the user token from the network, he would still need a user’s timestamp to crack the secret key.
With the combination of the user’s password hash and createdAt
date the JWT becomes a one-time-use token, because once the user has changed her password, successive calls to that route will generate a new password hash, invalidating the secret key which references the defunct password.
So what should the payload be?
As long as it’s something unique to the user that our server can compare, that’s up to you. In our case, we used the userId
, and the server just made sure that the decoded payload matches our user’s ID in the database.
Implementation
Now that all the theory is behind us, let’s look at one possible implementation:
Server
File: email.controller.js
:
Imports:
1import jwt from "jsonwebtoken"2import bcrypt from "bcryptjs"3import { User } from "../user/user.model"4import {5 transporter,6 getPasswordResetURL,7 resetPasswordTemplate8} from "../../modules/email"
Here we pull in our User model and email module, which are implementation details. Again, see the GitHub repository if you want to dig into all the specifics.
Make token from hash helper function:
1// `secret` is passwordHash concatenated with user's2// createdAt value, so if someone malicious gets the3// token they still need a timestamp to hack it:4export const usePasswordHashToMakeToken = ({5 password: passwordHash,6 _id: userId,7 createdAt8}) => {9 // highlight-start10 const secret = passwordHash + "-" + createdAt11 const token = jwt.sign({ userId }, secret, {12 expiresIn: 3600 // 1 hour13 })14 // highlight-end15 return token16}
Here we pass the old password hash, user ID and created at values into a function that signs a token with userId
as the payload and ${passwordHash}-${createdAt}
as a secret.
Sending the email:
1//// Sends an email IRL! ////2export const sendPasswordResetEmail = async (req, res) => {3 const { email } = req.params4 let user5 try {6 user = await User.findOne({ email }).exec()7 } catch (err) {8 res.status(404).json("No user with that email")9 }10 const token = usePasswordHashToMakeToken(user)11 const url = getPasswordResetURL(user, token)12 const emailTemplate = resetPasswordTemplate(user, url)1314 const sendEmail = () => {15 transporter.sendMail(emailTemplate, (err, info) => {16 if (err) {17 res.status(500).json("Error sending email")18 }19 console.log(`** Email sent **`, info.response)20 })21 }22 sendEmail()23}
This function find’s our user by email, makes a token, builds up a URL, then calls our email module and fires off an email IRL.
Updating the user’s password:
1export const receiveNewPassword = (req, res) => {2 const { userId, token } = req.params3 const { password } = req.body45 // highlight-start6 User.findOne({ _id: userId })7 .then(user => {8 const secret = user.password + "-" + user.createdAt9 const payload = jwt.decode(token, secret)10 if (payload.userId === user.id) {11 bcrypt.genSalt(10, function(err, salt) {12 // Call error-handling middleware:13 if (err) return14 bcrypt.hash(password, salt, function(err, hash) {15 // Call error-handling middleware:16 if (err) return17 User.findOneAndUpdate({ _id: userId }, { password: hash })18 .then(() => res.status(202).json("Password changed accepted"))19 .catch(err => res.status(500).json(err))20 })21 })22 }23 })24 // highlight-end2526 .catch(() => {27 res.status(404).json("Invalid user")28 })29}
This nasty function is the meat of the program. Originally we wrote this using the async/await protocol like sendPasswordResetEmail
above, but bcrypt didn’t seem to like async/await.
What’s interesting is that, despite the callback hell, this function never created any bugs and never needed to be rewritten.
File: email.restRouter.js
Then we mount the service in email.restRouter.js
(rest router as opposed to GraphQL router, which this blog is written in):
1import express from "express"2import * as emailController from "./email.controller"34export const emailRouter = express.Router()56emailRouter.route("/user/:email").post(emailController.sendPasswordResetEmail)78emailRouter9 .route("/receive_new_password/:userId/:token")10 .post(emailController.receiveNewPassword)
What about the client?
Initially we were going to do everything on the server, but then we decided that React Router and Axios were enough to pass params to and from the Express server.
There are really only 2 client-side files of interest.
File: RecoverPassword.js
This component doesn’t need to accept any props because the user hasn’t taken any action yet besides navigate to the component. Notice how the component renders based on local state, and how the Axios call to our server endpoint is made:
1import React, { Component } from "react"2import { Link } from "react-router-dom"3import styled from "styled-components"45import { Button, GhostInput } from "./styledComponents"6import RecoverPasswordStyles from "./RecoverPassword.styles"78import axios from "axios"9import SERVER_URI = "localhost:3000"1011class RecoverPassword extends Component {12 state = {13 email: "",14 submitted: false15 }1617 handleChange = e => {18 this.setState({ email: e.target.value })19 }2021 sendPasswordResetEmail = e => {22 e.preventDefault()23 const { email } = this.state24 axios.post(`${SERVER_URI}/reset_password/user/${email}`)25 this.setState({ email: "", submitted: true })26 }2728 render() {29 const { email, submitted } = this.state3031 return (32 <RecoverPasswordStyles>33 <h3>Reset your password</h3>34 {submitted ? (35 <div className="reset-password-form-sent-wrapper">36 <p>37 If that account is in our system, we emailed you a link to reset38 your password.39 </p>40 <Link to="/login" className="ghost-btn">41 Return to sign in42 </Link>43 </div>44 ) : (45 <div className="reset-password-form-wrapper">46 <p>47 It happens to the best of us. Enter your email and we'll send you48 reset instructions.49 </p>50 <form onSubmit={this.sendPasswordResetEmail}>51 <GhostInput52 onChange={this.handleChange}53 value={email}54 placeholder="Email address"55 />56 <Button className="btn-primary password-reset-btn">57 Send password reset email58 </Button>59 </form>60 <Link to="/login">I remember my password</Link>61 </div>62 )}63 </RecoverPasswordStyles>64 )65 }66}6768export default RecoverPassword
That’s it, it’s actually pretty simple. We can use React Router to mount it wherever we want. We went with /password/recover
because we had other password functionality that we bundled under the /password
namespace.
File: UpdatePassword.js
Here’s how we might use the UpdatePassword
component using React Router:
1<Route2 path="/update-password"3 render={({ match }) => (4 <UpdatePassword userId={match.params.userId} token={match.params.token} />5 )}6/>
And here’s what the component itself looks like:
1import React, { Component } from "react"2import axios from "axios"3import { Link } from "react-router-dom"4import PropTypes from "prop-types"56import { Button, GhostInput } from "./customStyledComponents"7import { RecoverPasswordStyles as UpdatePasswordStyles } from "./RecoverPassword"89const SERVER_URI = "localhost:3000"1011class UpdatePassword extends Component {12 state = {13 password: "",14 confirmPassword: "",15 submitted: false16 }1718 handleChange = key => e => {19 this.setState({ [key]: e.target.value })20 }2122 updatePassword = e => {23 e.preventDefault()24 const { userId, token } = this.props25 const { password } = this.state2627 axios28 .post(29 `${SERVER_URI}/reset_password/receive_new_password/${userId}/${token}`,30 { password }31 )32 .then(res => console.log("RESPONSE FROM SERVER TO CLIENT:", res))33 .catch(err => console.log("SERVER ERROR TO CLIENT:", err))34 this.setState({ submitted: !this.state.submitted })35 }3637 render() {38 const { submitted } = this.state3940 return (41 <UpdatePasswordStyles>42 <h3 style={{ paddingBottom: "1.25rem" }}>Update your password</h3>43 {submitted ? (44 <div className="reset-password-form-sent-wrapper">45 <p>Your password has been saved.</p>46 <Link to="/login" className="ghost-btn">47 Sign back in48 </Link>49 </div>50 ) : (51 <div className="reset-password-form-wrapper">52 <form53 onSubmit={this.updatePassword}54 style={{ paddingBottom: "1.5rem" }}55 >56 <GhostInput57 onChange={this.handleChange("password")}58 value={this.state.password}59 placeholder="New password"60 type="password"61 />62 <GhostInput63 onChange={this.handleChange("confirmPassword")}64 value={this.state.confirmPassword}65 placeholder="Confirm password"66 type="password"67 />6869 <Button className="btn-primary password-reset-btn">70 Update password71 </Button>72 </form>73 </div>74 )}75 </UpdatePasswordStyles>76 )77 }78}7980UpdatePassword.propTypes = {81 token: PropTypes.string.isRequired,82 userId: PropTypes.string.isRequired83}8485export default UpdatePassword
That’s a wrap! Of course there’s more to the implementation, but for sake of brevity I will simply link to the other relevant files here:
Thanks for reading! Feel free to drop me a line if you have any questions or feedback at ahrjarrett@gmail.com.
Check out my other projects on GitHub!