Skip to content

Implementing JWT Refresh Tokens in Node.js for Secure User Authentication

While JWTs (JSON Web Tokens) are a secure way to handle authentication, they are often short-lived, with a limited expiration time to reduce security risks if compromised. To maintain user sessions without requiring frequent re-authentication, refresh tokens allow you to renew access tokens safely. This guide walks through implementing refresh tokens in a Node.js application using Express and Mongoose, allowing for secure and scalable session management.


Why Use Refresh Tokens?

Access tokens usually have short expiration times (e.g., 15 minutes to 1 hour). If an access token expires, the user would have to log in again, which could lead to poor user experience. Refresh tokens enable the client to obtain a new access token without requiring the user to log in, allowing for a seamless experience while keeping sessions secure.

How Refresh Tokens Work

  1. Access Token: Short-lived token used for authorizing requests.
  2. Refresh Token: Long-lived token stored securely on the client side. It’s used to request a new access token when the access token expires.

The flow looks like this:

  1. The server issues an access token and a refresh token during login.
  2. The client stores the refresh token securely (e.g., in an HTTP-only cookie).
  3. When the access token expires, the client uses the refresh token to obtain a new access token.
  4. The server verifies the refresh token, issues a new access token, and, optionally, a new refresh token.

Setting Up the Project

We’ll build upon the previous JWT setup by adding refresh token functionality. Ensure you have Express, Mongoose, jsonwebtoken, and bcryptjs installed.


Step 1: Updating the Auth Routes

In the auth.js route file, we’ll add functionality to handle refresh tokens.

Generate Access and Refresh Tokens

Let’s create functions to generate an access token and a refresh token.

// @filename: index.js
const jwt = require('jsonwebtoken')

const generateAccessToken = (user) => {
  return jwt.sign({ id: user._id }, process.env.JWT_SECRET, {
    expiresIn: '15m',
  }) // Short-lived access token
}

const generateRefreshToken = (user) => {
  return jwt.sign({ id: user._id }, process.env.JWT_REFRESH_SECRET, {
    expiresIn: '7d',
  }) // Long-lived refresh token
}

Here, the generateAccessToken function generates a short-lived token (e.g., 15 minutes), and generateRefreshToken generates a token with a longer lifespan (e.g., 7 days).

Modify the Register and Login Routes

Update the register and login routes to issue both access and refresh tokens.

routes/auth.js

// @filename: routes.js
const express = require('express')
const jwt = require('jsonwebtoken')
const User = require('../models/User')

const router = express.Router()

router.post('/register', async (req, res) => {
  try {
    const { username, email, password } = req.body
    const existingUser = await User.findOne({ email })
    if (existingUser)
      return res.status(400).json({ message: 'Email already registered' })

    const user = new User({ username, email, password })
    await user.save()

    const accessToken = generateAccessToken(user)
    const refreshToken = generateRefreshToken(user)

    res.status(201).json({ user, accessToken, refreshToken })
  } catch (error) {
    res.status(500).json({ message: error.message })
  }
})

router.post('/login', async (req, res) => {
  try {
    const { email, password } = req.body
    const user = await User.findOne({ email })
    if (!user) return res.status(404).json({ message: 'User not found' })

    const isPasswordValid = await user.comparePassword(password)
    if (!isPasswordValid)
      return res.status(400).json({ message: 'Invalid credentials' })

    const accessToken = generateAccessToken(user)
    const refreshToken = generateRefreshToken(user)

    res.status(200).json({ user, accessToken, refreshToken })
  } catch (error) {
    res.status(500).json({ message: error.message })
  }
})

In this update:

  • Both routes now return an accessToken and refreshToken to the client upon successful authentication.

Step 2: Implementing the Refresh Token Endpoint

Next, let’s add a new endpoint to handle token refreshing.

Refresh Token Route

Create a refreshToken route that verifies the refresh token and issues a new access token.

routes/auth.js

// @filename: routes.js
router.post('/refresh-token', async (req, res) => {
  const { token } = req.body
  if (!token) return res.status(401).json({ message: 'Refresh token required' })

  try {
    const decoded = jwt.verify(token, process.env.JWT_REFRESH_SECRET)
    const user = await User.findById(decoded.id)
    if (!user) return res.status(404).json({ message: 'User not found' })

    const newAccessToken = generateAccessToken(user)
    res.status(200).json({ accessToken: newAccessToken })
  } catch (error) {
    res.status(403).json({ message: 'Invalid refresh token' })
  }
})

This route:

  1. Accepts the refresh token in the request body.
  2. Verifies the refresh token using the JWT_REFRESH_SECRET.
  3. If valid, it issues a new access token.

Step 3: Storing and Handling Tokens on the Client

To keep tokens secure, follow best practices for storing and sending tokens.

Securely Storing Tokens

  1. Access Tokens: Store in memory (not in local storage or session storage) to prevent XSS attacks.
  2. Refresh Tokens: Store in an HTTP-only, secure cookie. This prevents JavaScript access to the refresh token, reducing the risk of XSS attacks.

Example: Sending Tokens as Cookies

In the login and register routes, set the refresh token as an HTTP-only cookie.

// @filename: index.js
res.cookie('refreshToken', refreshToken, {
  httpOnly: true,
  secure: true, // Set to true if using HTTPS
  sameSite: 'Strict',
})
res.status(200).json({ user, accessToken })

The refresh token is sent as a cookie, while the access token is returned in the response body for client-side storage in memory.

Using Tokens in Requests

For each API request, include the access token in the Authorization header:

Authorization: Bearer <accessToken>

When the access token expires, send a request to the /refresh-token endpoint, and retrieve a new access token.


Step 4: Middleware for Protected Routes

Create a middleware to verify the access token for protected routes, as done in the previous JWT guide.

middleware/authMiddleware.js

// @filename: config.js
const jwt = require('jsonwebtoken')

const authMiddleware = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1]
  if (!token)
    return res
      .status(401)
      .json({ message: 'Access denied. No token provided.' })

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET)
    req.user = decoded
    next()
  } catch (error) {
    res.status(403).json({ message: 'Invalid or expired access token' })
  }
}

module.exports = authMiddleware

This middleware checks the access token validity before granting access to protected routes.


Best Practices for JWT and Refresh Tokens

  1. Short Access Token Expiration: Use a short expiration (e.g., 15 minutes) to minimize security risks if the token is compromised.
  2. Longer Refresh Token Expiration: Use a longer expiration (e.g., 7 days or more) for refresh tokens.
  3. HTTP-Only Cookies for Refresh Tokens: Store refresh tokens in HTTP-only cookies to prevent JavaScript access and mitigate XSS risks.
  4. Secure Token Storage: Avoid storing tokens in local storage. Use memory storage for access tokens and HTTP-only cookies for refresh tokens.
  5. Logout and Token Revocation: Implement a mechanism to revoke refresh tokens on logout or detect compromised tokens.

Conclusion

Implementing refresh tokens with JWT in a Node.js application adds a layer of security and flexibility to user authentication. With a combination of short-lived access tokens and long-lived refresh tokens, you can improve the user experience by allowing continuous sessions while maintaining robust security.

This approach is ideal for modern applications that need scalable, stateless authentication. Integrate these techniques in your projects to secure user sessions and manage authentication with ease and security.

Node.js JavaScript Backend Express REST API JWT
Share:

Continue Reading

Implementing JWT Authentication in Node.js with Mongoose

Authentication is a fundamental aspect of secure API development, allowing you to protect routes and control access to resources. By implementing JWT (JSON Web Tokens) for authentication in a Node.js and Express application, you can achieve stateless and secure access for your users. This guide covers setting up user registration, logging in, generating JWT tokens, and securing routes with JWT-based authentication.

Read article
Node.jsJavaScriptBackend

Implementing Authentication in a RESTful API with Node.js, Express, and JWT

Authentication is a fundamental aspect of secure API development, allowing you to protect routes and control access to resources. By implementing JWT (JSON Web Tokens) for authentication in a Node.js and Express application, you can achieve stateless and secure access for your users. This guide covers setting up user registration, logging in, generating JWT tokens, and securing routes with JWT-based authentication.

Read article
Node.jsJavaScriptBackend

Implementing Password Reset Functionality in Node.js with JWT and Nodemailer

Password reset functionality is essential for any application that requires user authentication. By combining JWT (JSON Web Token) with Nodemailer, you can build a secure password reset process that allows users to reset their passwords without exposing sensitive data. This guide will walk you through implementing a password reset feature in Node.js with Express, Mongoose, and Nodemailer.

Read article
Node.jsJavaScriptBackend