Back to Blogs
Deep technical guide
Node.js
Authentication
JWT
Refresh Tokens
Security
Backend

Node.js Authentication with JWT and Refresh Tokens: Secure Login Architecture for Real Apps

A production-focused Node.js authentication guide covering JWT access tokens, refresh tokens, cookies, rotation, logout, token theft, CSRF, XSS, sessions, and secure API design.

JS Interview Prep Editorial Team

Author

July 10, 2026

Published

4 min read

Reading time

1 views

Views

Node.js Authentication with JWT and Refresh Tokens: Secure Login Architecture for Real Apps
SEO-friendly JavaScript learning article

Node.js Authentication with JWT and Refresh Tokens: Secure Login Architecture for Real Apps

Authentication looks simple in tutorials: user logs in, server returns a token, frontend stores it, and protected routes check it. Production authentication is more subtle. You need expiry, refresh, logout, stolen-token handling, cookie security, CSRF protection, XSS awareness, and clear backend ownership.

This guide explains a secure JWT plus refresh-token architecture for Node.js applications, with enough depth for interviews and enough practicality for real apps.

Search intent this article targets

JWT authentication is an evergreen high-search topic. The stronger long-tail angle is not "what is JWT" but "JWT refresh token best practices", "Node.js auth secure cookies", "access token vs refresh token", and "how to logout JWT users".

  • Backend developers building Express or Node APIs.
  • Frontend developers deciding where tokens should live.
  • Interview candidates explaining auth trade-offs.
  • Teams moving from simple tutorial auth to production security.

Access token versus refresh token

An access token is short-lived and sent with API requests. A refresh token is longer-lived and used only to get a new access token. Keeping access tokens short-lived limits damage if one is stolen. Protecting refresh tokens carefully is critical because they can create new access tokens.

  • Access token: minutes, contains minimal claims, used for API authorization.
  • Refresh token: days or weeks, stored securely, rotated on use.
  • Session record: backend source of truth for refresh-token validity.

A practical login flow

On login, the backend verifies credentials, creates a session record, issues a short-lived access token, and sets a refresh token in an HttpOnly secure cookie. The frontend never needs to read the refresh token with JavaScript.

app.post('/auth/login', async (req, res) => {
  const user = await verifyCredentials(req.body.email, req.body.password);
  const session = await createSession(user.id);

  const accessToken = signAccessToken({ sub: user.id, role: user.role });
  const refreshToken = signRefreshToken({ sid: session.id });

  res.cookie('refresh_token', refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    path: '/auth/refresh',
  });

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

The exact cookie sameSite value depends on your cross-site requirements. The important point is that refresh tokens deserve stronger storage than localStorage.

Refresh token rotation

Refresh token rotation means every refresh request invalidates the old refresh token and issues a new one. If an old token is reused, the backend can treat that as a theft signal and revoke the session family.

  • Store a hashed refresh token identifier or token version in the database.
  • On refresh, verify token signature and session status.
  • Invalidate the previous refresh token.
  • Issue a new refresh token and access token.
  • If a used token appears again, revoke the session and require login.

Logout and revocation

JWT access tokens are stateless, so logout is not automatic unless the server keeps a denylist or the access token expires quickly. That is why short access-token lifetimes plus refresh-token revocation is a common practical approach.

app.post('/auth/logout', requireRefreshSession, async (req, res) => {
  await revokeSession(req.session.id);
  res.clearCookie('refresh_token', { path: '/auth/refresh' });
  res.status(204).send();
});

For high-risk systems, maintain token versions or denylists for immediate access-token revocation too.

CSRF, XSS, and storage decisions

Token storage is a trade-off. localStorage is easy but exposed to XSS. HttpOnly cookies protect against JavaScript reads but require CSRF-aware design. Many apps store refresh tokens in HttpOnly cookies and access tokens in memory, then refresh after reload.

  • Use HttpOnly, Secure, and SameSite cookies for refresh tokens.
  • Use CSRF protection if cross-site cookie behavior is allowed.
  • Keep access token lifetime short.
  • Avoid putting sensitive data inside JWT payloads.
  • Treat XSS prevention as part of auth security, not a separate topic.

Common mistakes

  • Putting long-lived JWTs in localStorage and calling it done.
  • Never rotating refresh tokens.
  • Having no backend session record, making revocation difficult.
  • Storing secrets or personal data in JWT payloads.
  • Using the same secret forever without a rotation plan.
  • Forgetting rate limits on login and refresh endpoints.

Interview explanation

A strong answer: use short-lived access tokens for API authorization and longer-lived refresh tokens for renewal. Store refresh tokens securely, rotate them on use, keep a server-side session record for revocation, protect refresh endpoints from CSRF and abuse, and remember that XSS prevention is still essential.

Quick FAQ

Should JWTs be stored in localStorage?

It is convenient but risky because XSS can read localStorage. Many production apps prefer HttpOnly cookies for refresh tokens and short-lived access tokens.

Why use refresh tokens at all?

They allow access tokens to expire quickly without forcing users to log in constantly. They also provide a server-side revocation point when stored with sessions.

Can JWT logout work?

Yes, but immediate logout requires server-side revocation, token versions, or denylists. Otherwise the access token remains valid until it expires.

Official references

Buy Me A Coffee