Authentication and security are crucial for maintaining user privacy and application integrity. A popular method to manage authentication is by using JSON Web Tokens (JWT). In this blog, we’ll walk through how to implement JWT authentication using Node.js, along with strategies to manage token expiration using access tokens and refresh tokens.

What is a JWT?

A JSON Web Token (JWT) is a compact, URL-safe token that carries information between two parties, usually the client and the server. It consists of three parts:

  1. Header: Contains metadata, such as the token type (JWT) and the hashing algorithm used (HS256).
  2. Payload: This is where the actual data resides, like the user’s information (e.g., user ID, username).
  3. Signature: A hashed value created using a secret key. The server uses this to verify that the token wasn’t altered by anyone other than the issuer.

Flow of JWT Authentication with Access and Refresh Tokens

When a user logs in to your application, they receive two tokens:

  1. Access Token: A short-lived token (typically expires in minutes).
  2. Refresh Token: A long-lived token (typically expires in days or weeks).

Why Two Tokens?

  • The access token is used to authenticate most API calls and has a short lifespan to minimize security risks.
  • The refresh token, stored securely (e.g., in an HTTP-only cookie), is used to generate a new access token once the old one expires.

Node.js Implementation

Let’s dive into how to implement JWT authentication in Node.js using both access and refresh tokens.

Generating Tokens

When a user logs in, we create two tokens: the access token and the refresh token. Here’s the code to create these tokens:

const jwt = require('jsonwebtoken');

// Create the JWT and refresh tokens
const token = jwt.sign(
  { userId: user._id, username: user.username },
  process.env.JWT_SECRET, // Secret key for signing the access token
  { expiresIn: "1m" } // Access token expires in 1 minute
);

const refreshToken = jwt.sign(
  { userId: user._id },
  process.env.JWT_REFRESH_SECRET, // Secret key for refresh token
  { expiresIn: "30d" } // Refresh token expires in 30 days
);
  • The access token expires in 1 minute, while the refresh token is valid for 30 days.
  • These tokens are then sent to the client: the access token can be stored in local storage, and the refresh token in an HTTP-only cookie.

Middleware to Authenticate Requests

To protect your API routes, you need a middleware function to authenticate incoming requests using the access token. If the token is valid, the request proceeds; otherwise, an error is returned.

const JWT_SECRET = process.env.JWT_SECRET;

const authenticateJWT = (req, res, next) => {
  // Allow certain requests without authentication
  if (req.url === '/graphql' && req.method === 'GET') {
    return next();
  }

  const query = req.body?.query || '';

  if (query.includes('IntrospectionQuery') ||
      query.includes('mutation LoginUser') ||
      query.includes('mutation RefreshToken')) {
    return next();
  }

  // Extract the token from the authorization header
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).send('Token is required');
  }

  // Verify the token
  jwt.verify(token, JWT_SECRET, (err, user) => {
    if (err) {
      return res.status(403).send('Invalid token');
    }

    req.user = user;
    next();
  });
};

app.use(authenticateJWT);

This middleware checks for the presence of a token in the request header. If the token is valid, it extracts the user’s information and attaches it to the request object for use in the next middleware or route handler.

Refreshing an Expired Access Token

When the access token expires, the client can request a new one using the refresh token. Here’s how you can implement an API endpoint to refresh the access token:

const refreshTokenHandler = (req, res) => {
  const refreshToken = req.cookies.refreshToken; // Assume refresh token is stored in an HTTP-only cookie

  if (!refreshToken) {
    return res.status(403).send('Refresh token is required');
  }

  jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET, (err, user) => {
    if (err) {
      return res.status(403).send('Invalid refresh token');
    }

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

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

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

When the access token expires, the client sends the refresh token to this endpoint. If the refresh token is valid, a new access token is issued. If the refresh token is expired or invalid, the user will be logged out.

Security Considerations

  • Refresh Token Storage: Storing the refresh token in an HTTP-only cookie helps mitigate XSS (Cross-Site Scripting) attacks since JavaScript cannot access the cookie.
  • Short-lived Access Tokens: By keeping access tokens short-lived, the risk of token misuse is minimized.
  • Token Revocation: To improve security, consider storing the refresh tokens in a database. This allows you to revoke them when necessary (e.g., upon user logout).

Conclusion

JWT-based authentication with access and refresh tokens is a powerful approach for managing user authentication in web applications. In this tutorial, we explored the basics of JWT, how to implement token-based authentication using Node.js, and how to securely refresh tokens to keep users logged in without compromising security.

Leave a Reply