f in x
JWT Authentication in Node.js — Access and Refresh Tokens for Secure Session Management
> cd .. / HUB_EDITORIALE
Sviluppo di siti web

JWT Authentication in Node.js — Access and Refresh Tokens for Secure Session Management

[2026-06-30] Author: Ing. Calogero Bono
Zenithby Meteora Web The operating system for your business. Social, clients, bookings and invoices in one platform. Gyms, barbers, professionals. Discover Zenith Free demo · no card

You're building a Node.js app and need to handle authentication without server-side sessions? JWT sounds like the answer, but managing token expiration, refresh, and security can get tricky. If a token is stolen or you don't handle refresh properly, users lose sessions or, worse, you leave the door open to attacks. At Meteora Web, we work daily on Node.js backends for clients who can't afford security gaps. This guide shows you how to implement JWT authentication with access and refresh tokens in Node.js, with real code and field-tested practices.

What is JWT and why use it for authentication in Node.js?

JWT (JSON Web Token) is a compact, self-contained format for securely transmitting information between parties. Unlike traditional sessions that require server-side storage, JWT carries all necessary data inside the token itself, digitally signed. That makes it perfect for REST APIs and stateless architectures.

Why Node.js and JWT pair well? Node.js is asynchronous and handles thousands of connections. With JWT you avoid querying a database on every request to verify the session — just decode and verify the signature. You save latency and resources. But the convenience of "everything in the token" hides pitfalls if you don't manage expiration and token rotation correctly.

Access token vs refresh token: the winning pair

A single JWT with a long expiration (e.g., 24 hours) is risky: if stolen, the attacker has access for too long. The standard solution is to use two tokens:

Sponsored Protocol

  • Access token — short-lived (e.g., 15 minutes), contains user identity and permissions. Sent with every request.
  • Refresh token — long-lived (e.g., 7 days), used only to obtain a new access token. Must be stored securely (e.g., HttpOnly cookie) and can be revoked.

This architecture limits exposure: even if the access token is stolen, it's valid for a short time. The refresh token never travels with normal API requests, so it's much harder to intercept.

How to implement access and refresh tokens in a Node.js project?

Let's build a practical example using Express and the jsonwebtoken package. For the refresh token, we'll use a database (e.g., PostgreSQL or Redis) to store tokens and enable revocation. We strongly advise against storing the refresh token in localStorage — use an HttpOnly and Secure cookie instead.

1. Project setup

npm init -y
npm install express jsonwebtoken bcryptjs dotenv
npm install --save-dev @types/jsonwebtoken  # if using TypeScript

Create a .env file for secret keys:

ACCESS_TOKEN_SECRET=your_very_long_random_secret
REFRESH_TOKEN_SECRET=a_different_secret
ACCESS_TOKEN_EXPIRES=15m
REFRESH_TOKEN_EXPIRES=7d

Never hardcode keys. Use environment variables and generate secure keys with openssl rand -base64 32.

2. Token generation

const jwt = require('jsonwebtoken');

function generateAccessToken(user) {
  return jwt.sign(
    { id: user.id, role: user.role },
    process.env.ACCESS_TOKEN_SECRET,
    { expiresIn: process.env.ACCESS_TOKEN_EXPIRES }
  );
}

function generateRefreshToken(user) {
  return jwt.sign(
    { id: user.id },
    process.env.REFRESH_TOKEN_SECRET,
    { expiresIn: process.env.REFRESH_TOKEN_EXPIRES }
  );
}

The refresh token contains only the user ID, not permissions. To enable revocation, save it in the database.

Sponsored Protocol

3. Login flow

app.post('/login', async (req, res) => {
  // Validate credentials...
  const user = { id: 123, role: 'admin' };

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

  // Save refresh token (e.g., in database)
  await saveRefreshToken(user.id, refreshToken);

  // Send access token in body, refresh token in secure cookie
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000
  });
  res.json({
    accessToken,
    expiresIn: 900 // 15 minutes in seconds
  });
});

The access token should be sent by the client in the Authorization: Bearer <token> header. The refresh token must never be accessible via JavaScript (HttpOnly).

4. Middleware to protect routes

function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  if (!token) return res.sendStatus(401);

  jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
    if (err) {
      if (err.name === 'TokenExpiredError') {
        return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
      }
      return res.sendStatus(403);
    }
    req.user = user;
    next();
  });
}

5. Refresh endpoint

app.post('/refresh', async (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  if (!refreshToken) return res.sendStatus(401);

  // Verify token is still valid in database
  const stored = await getRefreshToken(refreshToken);
  if (!stored) return res.sendStatus(403);

  jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
    if (err) return res.sendStatus(403);

    // Generate new access token and new refresh token (rotation)
    const newAccessToken = generateAccessToken({ id: user.id, role: stored.role });
    const newRefreshToken = generateRefreshToken({ id: user.id });

    // Revoke old refresh token and save new one
    await revokeRefreshToken(refreshToken);
    await saveRefreshToken(user.id, newRefreshToken);

    res.cookie('refreshToken', newRefreshToken, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000
    });
    res.json({ accessToken: newAccessToken, expiresIn: 900 });
  });
});

Refresh token rotation is crucial: each time you use a refresh token, replace it with a new one. If an attacker steals a refresh token and uses it, the legitimate token is invalidated. If the legitimate owner tries to use the old one, you detect the theft and can revoke all tokens for that user.

Sponsored Protocol

What are the best security practices for JWT in Node.js?

Security doesn't stop at signing the token. Here are the measures we at Meteora Web apply in every project.

Sponsored Protocol

Don't put sensitive data in the payload

JWT is signed but not encrypted. Anyone can decode it (base64). Don't include passwords, credit card numbers, or unnecessary personal data. Use only identifiers and permissions.

Use different secrets for access and refresh tokens

If one secret is compromised, the other remains safe. For higher security, consider asymmetric keys (RS256).

HttpOnly cookie for refresh token

Never store the refresh token in localStorage or sessionStorage — they are vulnerable to XSS. HttpOnly, Secure, and SameSite cookies are the safest choice.

Implement blacklist or revocation

When a user logs out, invalidate the refresh token (delete from database). For theft scenarios, maintain a blacklist of revoked but still-valid tokens (e.g., in Redis with TTL). We use a refresh_tokens table with a revoked_at column.

Limit token validity

Access token: 15 minutes max. Refresh token: 7 days for most apps. For banking or healthcare, reduce further. The longer the refresh token validity, the greater the risk if stolen.

Sponsored Protocol

Protect the refresh endpoint from CSRF

Since the refresh token is in a cookie, it could be sent automatically in a cross-site request. Use sameSite: 'strict' and, for extra safety, an anti-CSRF header (e.g., X-Requested-By or a CSRF token in the body).

Logging and monitoring

Track all failed refresh attempts, especially when a revoked token is reused. This could indicate an ongoing attack.

What to do next

You've learned how to implement JWT authentication with access and refresh tokens in Node.js, with security in mind. Now it's your turn:

  1. Install the jsonwebtoken package and build your login system.
  2. Integrate refresh token storage in a database (Redis is ideal for performance).
  3. Protect the refresh token with HttpOnly and Secure cookies, and implement rotation.
  4. Test a theft scenario — simulate an XSS attack and verify the refresh token is inaccessible.
  5. Monitor logs for abnormal refresh token usage.

If your stack is Node.js and you want a secure backend without reinventing the wheel, we at Meteora Web can help design it. We've built JWT-based platforms for clients across Italy, always starting from numbers and security. For a broader view of Node.js in your project, check our pillar guide on Node.js for the backend.

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere informatico, fondatore di Meteora Web e Zenith OS. System administrator e progettista di piattaforme, app e CMS proprietari, con esperienza in sviluppo full-stack, marketing digitale ed ecosistema Google.
[ Read Full Dossier ]

> METEORA_WEB // DIGITAL AGENCY

We build the digital presence your business deserves.

Websites, social media, online advertising, e-commerce and high-performance hosting, engineered with method by computer engineers in Sciacca, for all of Italy.

> MW_JOURNAL

> READ_ALL()