JWT Authentication with Access and Refresh Tokens in Node.js

JWT authentication in Node.js showing access tokens, refresh tokens, Express middleware, and secure token validation.

Post Date: September 21, 2024
Last Updated: November 02, 2026

Introduction

Authentication and security are crucial for maintaining user privacy and application integrity. One popular method of managing authentication in web applications is JSON Web Token (JWT).

JWT authentication allows applications to securely exchange information between clients and servers. By implementing access tokens and refresh tokens, developers can manage user sessions while reducing the security risks associated with long-lived authentication tokens.

In this tutorial, we will learn how to implement JWT authentication with access and refresh tokens in Node.js using the jsonwebtoken package and Express.js. We will also explore token expiration, middleware authentication, and secure refresh-token storage.

What is a JWT?

A JSON Web Token (JWT) is a compact, URL-safe token that carries information between two parties, usually a client and a server.

A JWT consists of three parts:

1. Header

The header contains metadata about the token, such as the token type (JWT) and the signing algorithm used (for example, HS256).

2. Payload

The payload contains claims or information about the user, such as the user ID, username, and other relevant details.

3. Signature

The signature is generated using the encoded header, payload, and a secret key. The server uses it to verify that the token has not been modified.

A JWT is signed, not automatically encrypted. Therefore, sensitive information such as passwords or confidential personal data should never be included in its payload.

Flow of JWT Authentication with Access and Refresh Tokens

When a user successfully logs in to an application, the server generates two tokens.

Access Token

  • A short-lived token used to authenticate API requests.
  • Typically expires within a few minutes.
  • Sent with requests to protected endpoints.

Refresh Token

  • A long-lived token used to generate a new access token.
  • Typically remains valid for several days or weeks.
  • Should be stored securely, preferably in an HTTP-only cookie.
  • Can be revoked when the user logs out or when suspicious activity is detected.

Why Use Two Tokens?

Access tokens have a short expiration time to reduce the impact of token theft. Refresh tokens allow users to obtain new access tokens without repeatedly entering their login credentials.

For example, when an access token expires, the client sends a request to the refresh-token endpoint. The server validates the refresh token and generates a new access token if the refresh token is still valid.

Prerequisites

Before implementing JWT authentication in Node.js, make sure you have:

  • Basic knowledge of JavaScript and Node.js.
  • Node.js and npm installed.
  • Basic understanding of Express.js.
  • Familiarity with REST API development.
  • An existing user authentication and login process.

Node.js Implementation

Step 1: Install the Required Packages

First, install the required packages for JWT authentication and Express.js.

npm install express jsonwebtoken cookie-parser dotenv

The packages are used for the following purposes:

  • express – Creates the Node.js application and handles HTTP requests.
  • jsonwebtoken – Generates and verifies JWT tokens.
  • cookie-parser – Reads cookies from incoming requests.
  • dotenv – Loads environment variables from a .env file.

Step 2: Configure Environment Variables

Create a .env file in your project root directory and configure separate secret keys for access tokens and refresh tokens.

JWT_SECRET=your_access_token_secret
JWT_REFRESH_SECRET=your_refresh_token_secret
NODE_ENV=development
PORT=3000

Use strong, randomly generated secrets in production. Keep the .env file private and never commit it to a public repository.

Step 3: Generate Access and Refresh Tokens

When a user successfully logs in, the application generates two JWT tokens.

The access token is configured to expire after one minute for demonstration purposes, while the refresh token expires after 30 days.

const jwt = require('jsonwebtoken');

const generateTokens = (user) => {

    // Generate the access token
    const accessToken = jwt.sign(
        {
            userId: user._id,
            username: user.username
        },
        process.env.JWT_SECRET,
        {
            expiresIn: "1m"
        }
    );

    // Generate the refresh token
    const refreshToken = jwt.sign(
        {
            userId: user._id
        },
        process.env.JWT_REFRESH_SECRET,
        {
            expiresIn: "30d"
        }
    );

    return {
        accessToken,
        refreshToken
    };
};

Explanation:

  • jwt.sign() generates a signed JWT.
  • user._id identifies the authenticated user.
  • username is included in the access token payload.
  • JWT_SECRET is used to sign the access token.
  • JWT_REFRESH_SECRET is used to sign the refresh token.
  • expiresIn specifies how long each token remains valid.

The refresh token contains the user ID, which will be used to identify the user when generating a new access token.

After successful login, return the access token in the response and store the refresh token in a secure HTTP-only cookie.

The refresh token should not be exposed to client-side JavaScript. An HTTP-only cookie helps prevent JavaScript from reading the refresh token during an XSS attack.

The following example shows how to send the tokens after successful user authentication.

const sendTokens = (req, res, user) => {

    const { accessToken, refreshToken } = generateTokens(user);

    res.cookie("refreshToken", refreshToken, {
        httpOnly: true,
        secure: process.env.NODE_ENV === "production",
        sameSite: "strict",
        maxAge: 30 * 24 * 60 * 60 * 1000
    });

    res.json({
        accessToken
    });
};

Call sendTokens() after verifying the user’s login credentials.

The cookie settings provide the following protections:

  • httpOnly: true prevents JavaScript from accessing the cookie.
  • secure: true sends the cookie only over HTTPS in production.
  • sameSite: "strict" helps reduce cross-site request risks.
  • maxAge sets the cookie lifetime to 30 days.

If your frontend and backend are hosted on different sites, you may need to configure SameSite=None, Secure, credentialed requests, and appropriate CSRF protection.

Step 5: Create Middleware to Authenticate Requests

To protect API routes, create middleware that verifies the access token sent through the Authorization header.

The client should include the access token in the following format:

Authorization: Bearer ACCESS_TOKEN

The middleware checks whether the token exists and verifies its signature and expiration.

const jwt = require('jsonwebtoken');

const authenticateJWT = (req, res, next) => {

    // Extract the access token from the Authorization header
    const authHeader = req.headers.authorization;

    if (!authHeader || !authHeader.startsWith("Bearer ")) {
        return res.status(401).json({
            message: "Access token is required"
        });
    }

    const token = authHeader.split(" ")[1];

    // Verify the access token
    jwt.verify(token, process.env.JWT_SECRET, (err, user) => {

        if (err) {
            return res.status(403).json({
                message: "Invalid or expired access token"
            });
        }

        // Attach the decoded user information to the request
        req.user = user;

        next();
    });
};

module.exports = authenticateJWT;

Explanation:

  • The middleware extracts the token from the Authorization header.
  • It checks whether the Bearer token is available.
  • jwt.verify() validates the token using the access-token secret.
  • If the token is invalid or expired, the server returns an error.
  • If the token is valid, the decoded user information is attached to req.user.
  • next() allows the request to continue to the next middleware or route handler.

Step 6: Create an API Endpoint to Refresh the Access Token

When the access token expires, the client can request a new access token using the refresh token stored in the HTTP-only cookie.

Create a refresh-token handler to verify the refresh token and generate a new access token.

const jwt = require('jsonwebtoken');

const refreshTokenHandler = (req, res) => {

    // Get the refresh token from the HTTP-only cookie
    const refreshToken = req.cookies.refreshToken;

    if (!refreshToken) {
        return res.status(401).json({
            message: "Refresh token is required"
        });
    }

    // Verify the refresh token
    jwt.verify(
        refreshToken,
        process.env.JWT_REFRESH_SECRET,
        (err, user) => {

            if (err) {
                return res.status(403).json({
                    message: "Invalid or expired refresh token"
                });
            }

            // Generate a new access token
            const newAccessToken = jwt.sign(
                {
                    userId: user.userId
                },
                process.env.JWT_SECRET,
                {
                    expiresIn: "1m"
                }
            );

            res.json({
                accessToken: newAccessToken
            });
        }
    );
};

module.exports = refreshTokenHandler;

Explanation:

  • The refresh token is retrieved from the HTTP-only cookie using req.cookies.refreshToken.
  • The server checks whether the refresh token is available.
  • jwt.verify() validates the refresh token using JWT_REFRESH_SECRET.
  • If the refresh token is invalid or expired, the server returns an error.
  • If the refresh token is valid, a new access token is generated.
  • The new access token is returned to the client.

The refresh token remains in the cookie until it expires or is cleared. For production applications, implement refresh-token rotation and server-side revocation to prevent the continued use of compromised refresh tokens.

Step 7: Configure Express Routes

Now, configure the Express application to use the authentication middleware and refresh-token handler.

The refresh endpoint must be accessible without an access token because its purpose is to obtain a new access token.

const express = require('express');
const cookieParser = require('cookie-parser');
const dotenv = require('dotenv');

const authenticateJWT = require('./middleware/authenticateJWT');
const refreshTokenHandler = require('./controllers/refreshTokenHandler');

dotenv.config();

const app = express();

app.use(express.json());
app.use(cookieParser());

// Public refresh-token endpoint
app.post('/refresh-token', refreshTokenHandler);

// Protected API endpoint
app.get('/profile', authenticateJWT, (req, res) => {

    res.json({
        message: "Authentication successful",
        user: req.user
    });

});

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

Explanation:

  • express.json() parses incoming JSON request bodies.
  • cookieParser() makes cookies available through req.cookies.
  • /refresh-token is a public endpoint that validates the refresh token independently.
  • /profile is protected by the authenticateJWT middleware.
  • Only requests with a valid access token can access the protected profile endpoint.

If you are using GraphQL, authentication can also be implemented through the GraphQL context so that protected resolvers receive the authenticated user information. Public login and token-refresh operations should be handled explicitly rather than bypassing authentication through simple request-body string matching.

Step 8: Implement Refresh Token Revocation

A refresh token can remain valid until its expiration time unless the application implements a revocation mechanism.

For production applications, consider storing refresh-token records or token identifiers in a database. When a user logs out, revoke the corresponding refresh token and clear the cookie.

The following example clears the refresh-token cookie during logout:

app.post('/logout', (req, res) => {

    res.clearCookie("refreshToken", {
        httpOnly: true,
        secure: process.env.NODE_ENV === "production",
        sameSite: "strict"
    });

    res.json({
        message: "Logged out successfully"
    });

});

Clearing the cookie removes it from the browser, but it does not invalidate a copied JWT. For complete revocation, the server must also invalidate the refresh-token record or token identifier stored in the database.

Security Considerations

When implementing JWT authentication with access and refresh tokens, consider the following security practices:

1. Refresh Token Storage

Store refresh tokens in HTTP-only, Secure cookies. Configure appropriate SameSite and CSRF protections based on your application architecture.

2. Short-Lived Access Tokens

Use short expiration periods for access tokens to reduce the impact of token theft.

3. Separate Secret Keys

Use separate, strong secret keys for access tokens and refresh tokens. Store them securely as environment variables.

Conclusion

JWT authentication with access and refresh tokens is an effective approach to managing authentication in Node.js applications.

In this tutorial, we explored the structure of JWTs, generating access and refresh tokens, implementing authentication middleware, refreshing expired access tokens, and handling token revocation.

By using short-lived access tokens, securely storing refresh tokens, and implementing proper token validation and revocation mechanisms, developers can build more secure authentication systems for their applications.

FAQ

1. What is the difference between an access token and a refresh token?

An access token is a short-lived token used to authenticate API requests. A refresh token has a longer lifetime and is used to generate new access tokens when the existing access token expires.

2. Where should refresh tokens be stored in Node.js applications?

Refresh tokens should preferably be stored in HTTP-only, Secure cookies with appropriate SameSite and CSRF protections. This prevents client-side JavaScript from directly accessing the cookie.

3. What happens when a refresh token expires?

When a refresh token expires, the server rejects the refresh request. The user must authenticate again by logging in to obtain new access and refresh tokens.

Consult Our Node.js Development Experts

Looking for reliable Node.js development, application security, or authentication solutions?

Pheonix Solutions provides technology services covering application security, software development, cloud infrastructure, DevOps, and technical support.

Our experts can help you build secure applications, implement authentication mechanisms, and maintain reliable software systems.

Contact our experts to discuss your requirements.

bharat

Bharath is a Developer with experience in building and maintaining web and mobile applications. Scalable and reliable solutions are developed using modern technologies such as React, Node.js, TypeScript, GraphQL, and MySQL. Application performance, functionality, and code quality are continuously improved, and technical issues are identified and resolved efficiently. A strong focus is placed on delivering secure, user-friendly, and high-quality software solutions.

Leave a Comment

Scroll to Top