Node.js  

How to Implement a JWT Login System in Node.js

Introduction

JSON Web Tokens (JWT) are a popular and compact way to authenticate users in modern web applications. A JWT login system lets clients present a signed token instead of sending a username/password on every request. This guide demonstrates how to construct a secure, production-ready JWT login system in Node.js (Express), featuring examples, middleware, refresh tokens, and best practices for security.

What is JWT and why use it?

A JWT is a signed JSON object (header.payload.signature). It proves the user’s identity without server-side session storage (stateless), which makes it great for scalable APIs and microservices. But JWTs must be handled carefully β€” storing them insecurely or making them too long-lived introduces risk. Trusted tutorials stress these points and caution against storing tokens insecurely (for example, in plain localStorage).

High-level design of a JWT login system

  1. Register: The user signs up; the server stores a hashed password (bcrypt).

  2. Login: The server verifies credentials and issues an access token (a short-lived JWT) and a refresh token (a longer-lived, revocable token).

  3. Protected routes: A middleware checks the access token on requests.

  4. Refresh: When the access token expires, the client requests a new one using the refresh token.

  5. Logout/revocation: Invalidate refresh tokens server-side (DB/Redis) or use rotation.

Many up-to-date guides recommend short-lived access tokens + refresh tokens for safety, and server-side storage or revocation for refresh tokens.

Prerequisites (packages & environment)

Create a Node.js + Express project. Typical packages:

  • express β€” web framework

  • jsonwebtoken β€” sign/verify JWTs

  • bcrypt or bcryptjs β€” hash passwords

  • dotenv β€” manage secrets in env variables

  • cookie-parser β€” if using HTTP-only cookies

  • Database: MongoDB (Mongoose), PostgreSQL, MySQL, or Redis for token revocation

Install example

npm init -y
npm install express jsonwebtoken bcryptjs dotenv cookie-parser
# and your DB driver, e.g. mongoose or pg

Minimal secure architecture decisions (recommendations)

  • Use HTTPS always in production.

  • Use short-lived access tokens (e.g., 5–15 minutes) and longer-lived refresh tokens.

  • Store access tokens in memory or in an httpOnly cookie; avoid localStorage for sensitive tokens. DigitalOcean and Linode articles highlight storage risks and prefer httpOnly cookies for many apps.

  • Keep your JWT secret in environment variables or secret manager (process.env.JWT_SECRET).

  • Hash passwords with bcrypt before saving to DB.

Example. Simple JWT Login System (Express + MongoDB-like pseudo code)

Below is a practical example covering registration, login (issue tokens), middleware, refresh, and logout. Adapt DB calls to your database (Mongoose/Sequelize/etc.).

Note: This example uses HTTP-only cookies for refresh tokens and returns the access token in the JSON response. You can also store the access token as an httpOnly cookie.

1) Environment variables (.env)

PORT=4000
JWT_ACCESS_SECRET=very_secret_access_key
JWT_REFRESH_SECRET=another_secret_for_refresh
ACCESS_TOKEN_EXPIRY=15m
REFRESH_TOKEN_EXPIRY=7d

2) Simple user model (pseudo)

// users collection fields suggested: { id, email, passwordHash, refreshTokenId? }

3) Utility: create tokens

const jwt = require('jsonwebtoken');

function createAccessToken(payload) {
  return jwt.sign(payload, process.env.JWT_ACCESS_SECRET, { expiresIn: process.env.ACCESS_TOKEN_EXPIRY });
}

function createRefreshToken(payload) {
  // include a jti (unique id) if you plan to revoke/track refresh tokens
  return jwt.sign(payload, process.env.JWT_REFRESH_SECRET, { expiresIn: process.env.REFRESH_TOKEN_EXPIRY });
}

4) Register route (hash password)

const bcrypt = require('bcryptjs');

app.post('/register', async (req, res) => {
  const { email, password } = req.body;
  // validate email & password...
  const passwordHash = await bcrypt.hash(password, 10);
  // Save user { email, passwordHash } to DB
  res.status(201).json({ message: 'User created' });
});

5) Login route (issue tokens)

app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  // find user by email from DB
  const user = await User.findOne({ email });
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  const ok = await bcrypt.compare(password, user.passwordHash);
  if (!ok) return res.status(401).json({ error: 'Invalid credentials' });

  // create payload (avoid sensitive data)
  const payload = { userId: user._id, role: user.role };

  const accessToken = createAccessToken(payload);
  const refreshToken = createRefreshToken({ ...payload, tokenId: someUniqueId() });

  // store refresh token id or token in DB for revocation/rotation
  await saveRefreshTokenToDB(user._id, refreshToken /* or tokenId */);

  // set refresh token as httpOnly cookie
  res.cookie('jid', refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/refresh_token' // limit cookie scope
  });

  res.json({ accessToken }); // client uses this for Authorization: Bearer <token>
});

(Using httpOnly cookie for refresh token reduces XSS risk; cookies must be protected with HTTPS and proper SameSite.)

6) Middleware: protect routes (verify access token)

function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  if (!authHeader) return res.status(401).json({ error: 'No token' });

  const token = authHeader.split(' ')[1];
  jwt.verify(token, process.env.JWT_ACCESS_SECRET, (err, user) => {
    if (err) return res.status(403).json({ error: 'Invalid or expired token' });
    req.user = user; // contains userId and role
    next();
  });
}

// usage
app.get('/protected', authenticateToken, (req, res) => {
  res.json({ message: 'Protected data', user: req.user });
});

7) Refresh endpoint (rotate / reissue access token)

app.post('/refresh_token', async (req, res) => {
  const token = req.cookies.jid;
  if (!token) return res.status(401).json({ ok: false, accessToken: '' });

  try {
    const payload = jwt.verify(token, process.env.JWT_REFRESH_SECRET);
    // Check tokenId against DB (revoked/valid)
    const valid = await isRefreshTokenValid(payload.userId, payload.tokenId);
    if (!valid) return res.status(401).json({ ok: false, accessToken: '' });

    // Optionally rotate refresh token: issue a new refresh token (with new tokenId) and store new id in DB
    const newRefreshTokenId = someUniqueId();
    const newRefreshToken = createRefreshToken({ userId: payload.userId, tokenId: newRefreshTokenId });
    await replaceRefreshTokenInDB(payload.userId, payload.tokenId, newRefreshTokenId);

    res.cookie('jid', newRefreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/refresh_token' });

    const accessToken = createAccessToken({ userId: payload.userId });
    return res.json({ ok: true, accessToken });
  } catch (e) {
    return res.status(401).json({ ok: false, accessToken: '' });
  }
});

8) Logout / revoke refresh tokens

app.post('/logout', async (req, res) => {
  const token = req.cookies.jid;
  if (token) {
    // decode or verify refresh token then remove it from DB
    try {
      const payload = jwt.verify(token, process.env.JWT_REFRESH_SECRET);
      await revokeRefreshToken(payload.userId, payload.tokenId);
    } catch (e) {}
  }
  // Clear cookie
  res.clearCookie('jid', { path: '/refresh_token' });
  return res.json({ success: true });
});

Security best practices (practical checklist)

  • Use HTTPS and secure cookies in production.

  • Do not store tokens in localStorage for sensitive apps; prefer httpOnly cookies or in-memory storage.

  • Use short-lived access tokens (minimize time window if leaked). Use refresh tokens for session continuity.

  • Hash passwords with bcrypt before storing.

  • Protect refresh tokens: store them server-side (DB/Redis) or use rotating refresh tokens and a revocation list.

  • Validate inputs and apply rate limiting and account lockouts to prevent brute force.

  • Keep secrets out of source control; use environment secrets or vaults.

  • Use proper JWT algorithms (HS256 is common; for high security, use RS256 with asymmetric keys and rotate keys carefully).

  • Log and monitor suspicious login or token activity.

Common pitfalls & how to avoid them

  • Keeping secret keys in code β€” use process.env or secret manager.

  • Overly long-lived access tokens β€” use brief expiry and refresh flow.

  • No refresh token revocation β€” store token ids (jti) and revoke on logout or suspicious events.

  • Relying only on client-side checks β€” always verify tokens server-side.

  • Not verifying token signature/algorithms β€” use jsonwebtoken.verify() and check the algorithm if needed.

Scaling & production tips

  • Use Redis to store/track refresh token ids and fast revocation.

  • Consider single sign-on or third-party identity (Auth0, Okta) if you do not want to manage auth in-house.

  • Add role-based access control (RBAC) inside the JWT payload (role) and check it in the middleware. Many tutorials show role examples for real-world apps.

  • Rate-limit login and refresh endpoints to reduce abuse.

FAQs

Should I store JWTs in cookies or localStorage?

A: Cookies (httpOnly, secure, sameSite) are safer vs XSS; localStorage is vulnerable to XSS. For APIs used by SPAs, you can store the access token in memory and refresh via an HTTP-only cookie.

Do I need refresh tokens?

A: For long sessions without re-login, yes β€” use refresh tokens to renew short-lived access tokens and support revocation.

Summary

A robust JWT login system in Node.js uses short-lived access tokens plus revocable refresh tokens, secure password hashing, HTTPS, and careful token storage (httpOnly cookies or in-memory). Use jsonwebtoken and bcrypt, verify tokens in middleware, and implement refresh/rotation and revocation strategies for production safety. Following these patterns β€” supported by established guides β€” will give you a scalable and secure authentication flow.