DEV Community

Cover image for Reset/Forgot password on a rest api with jsonwebtoken (jwt) in express
renzhamin
renzhamin

Posted on • Edited on • Originally published at blog.renzhamin.com

Reset/Forgot password on a rest api with jsonwebtoken (jwt) in express

Overview

  • Add password reset functionality on top of a rest-api that has authentication and authorization implemented. If you want to know the basics of how jsonwebtoken is used for authorization, you can read here
  • Prisma ORM is used with SQLite. So you don't have to setup any database in your system
  • This approach is stateless and requires the least amount of db calls, therefore prioritizing performance
  • The code is available on github

Flow

Sequence Diagram

Table of Contents

Token Generation

utils/genToken.ts:

// helper function to generate token
export const genToken = (
    user: any,
    secret: string,
    expiresIn: string,
    tokenId?: string
) => {
    const { id, username, email } = user
    const jwtid = tokenId || randomUUID()
    const token = jwt.sign({jwtid, id, username, email}, secret,{
        algorithm: "HS256",
        expiresIn,
    })

    return token
}
Enter fullscreen mode Exit fullscreen mode
  • Generate a token with a 2 minute expiration time (lesser the better)
  • Use an environment variable as secret, this should be large and random
export const genPassResetToken = (user) => {
    return genToken(user, process.env.PASS_RESET_TOKEN_SECRET!, "2m")
}
Enter fullscreen mode Exit fullscreen mode

Why keep the validity duration small ?

As we are using stateless jwt, we can not invalidate the token. To prevent a user from using the same link again and again, we must set the expiration time to a lower value

Why not just use regular accessToken ?

A regular access token provides access to protected resources which we don't want at all.
The user only provides a email address, they may or may not be a legitimate user. Thats why we can't just hand over an accessToken on a password reset request

Sending Email

There are multiple ways to send email from a server.

Here is a sample code to setup a gmail account to send an email using nodemailer.

utils/sendEmail.ts:

export const sendEmail = async (receiverEmail, subject, body) => {
    const transporter = nodemailer.createTransport({
        service: "gmail",
        auth: {
            // your gmail address
            user: process.env.serviceEmail,
            // app password for the gmail
            pass: process.env.serviceEmailPassword,
        },
    })

    const mailOptions = {
        from: process.env.serviceEmail,
        to: receiverEmail,
        subject: subject,
        html: body,
    }

    transporter.sendMail(mailOptions, (error, info) => {
        if (error) {
            console.log("Failed to send email to", receiverEmail)
        } else {
            /* console.log("Email sent: " + info.response) */
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

How to enable app password in google account

  1. Go to google account page
  2. Navigate to security and enable 2-step verification
  3. Search "App Passwords" in the search bar and click on the matched result
  4. Chose app as "Mail" and device as "Other" and put anything there
  5. Click on "GENERATE" button and save the password as serviceEmailPassword in .env file. (NOTE: you only get one chance to save the password)

Sending the reset link

controllers/passReset.ts:

// Route: GET /api/pass-reset; expecting email in request body
export const getPassResetLink = async (req, res) => {
    try {
        if (!req.body.email) {
            return res.status(400).json({ msg: "No email provided" })
        }
        const user = await findUserByEmail(req.body.email)
        if (!user || !user.email) return res.json({ msg: "Email not found" })
        const token = genPassResetToken(user)
        const requrl = req.protocol + "://" + req.get("host") + req.originalUrl
        // the url from which the request came from, in local environment it is,
        // http://localhost:5000/api/pass-reset

        // if the frontend is in different domain declare PASS_RESET_URL in .env file
        const url = process.env.PASS_RESET_URL || requrl
        const resetLink = `<a target='_blank' href='${url}/${user.id}/${token}'>Password Reset Link</a>`
        sendEmail(req.body.email, "Reset Password", resetLink)
        res.json({ msg: "Password Reset Link sent to email" })
    } catch (error) {
        res.status(500).json({ msg: "Failed to send email" })
    }
}
Enter fullscreen mode Exit fullscreen mode

Resetting password

Password Reset Form

  • The frontend should have a route "PASS_RESET_URL/:userId/:token" that takes an email in the body which sends a POST request to /api/pass-reset/:userId/:token on submit
  • Here, I have created the most basic html form to keep this article backend focused

utils/passReset.ts:

// Route : GET /api/pass-reset/:userId/:token
export const getPassResetPage = (req, res) => {
    const { userId, token } = req.params

    res.send(`<form action="/api/pass-reset/${userId}/${token}" method="POST">
             <input type="password" name="password" value="" placeholder="Enter your new password..." /> 
             <input type="submit" value="Reset Password" />
             </form>`)
}
Enter fullscreen mode Exit fullscreen mode

Updating password

Route : router.post("/api/pass-reset/:userId/:token", verifyPasswordResetToken, passReset)

  • verifyPasswordResetToken middleware verifies the token with the secret PASS_RESET_TOKEN_SECRET

utils/passReset.ts:

export const passReset = async (req, res) => {
    try {
        const { password } = req.body
        if (!password) return res.send("Password not provided")
        updatePassword(req.params.userId, password)
        res.json({ msg: "Password Reset Successfull" })
    } catch (error) {
        res.status(404).send("Failed to reset password")
    }
}
Enter fullscreen mode Exit fullscreen mode
  • IMPORTANT: Hash the password before saving it in the database
  • Ensure that you use the same hash function for password hashing in every cases such as user creation, change password, reset password

db/users.ts:

export const updatePassword = async (userId: string, newPassword: string) => {
    newPassword = await hashString(newPassword)
    return db.user.update({
        where: { id: userId },
        data: { password: newPassword },
    })
}
Enter fullscreen mode Exit fullscreen mode

Find me on
💻 Github
🔘 LinkedIn
â­• Twitter

Top comments (0)