Back to Blog

JWT Authentication: Complete Guide with Best Practices

Security Team
August 5, 2024
8 min read
Security & Compliance

JWT Authentication: Complete Guide with Best Practices

JSON Web Tokens (JWT) are a popular method for authentication in modern web applications. Let’s explore how to implement JWT authentication securely.

What is JWT?

JWT is a compact, URL-safe token format for securely transmitting information between parties. A JWT consists of three parts:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Header.Payload.Signature

JWT Structure

Header:

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload:

{
  "userId": "123",
  "email": "user@example.com",
  "exp": 1735689600
}

Signature:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

Basic Implementation

Generate JWT (Node.js)

const jwt = require('jsonwebtoken');

function generateAccessToken(user) {
  const payload = {
    userId: user.id,
    email: user.email,
    role: user.role
  };

  return jwt.sign(payload, process.env.JWT_SECRET, {
    expiresIn: '15m',  // Short-lived access token
    issuer: 'your-app',
    audience: 'your-app-users'
  });
}

function generateRefreshToken(user) {
  const payload = {
    userId: user.id,
    tokenVersion: user.tokenVersion  // For token revocation
  };

  return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET, {
    expiresIn: '7d',  // Long-lived refresh token
    issuer: 'your-app'
  });
}

Verify JWT

function verifyToken(token) {
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET, {
      issuer: 'your-app',
      audience: 'your-app-users'
    });
    return decoded;
  } catch (error) {
    if (error instanceof jwt.TokenExpiredError) {
      throw new Error('Token expired');
    }
    if (error instanceof jwt.JsonWebTokenError) {
      throw new Error('Invalid token');
    }
    throw error;
  }
}

Authentication Flow

Login Endpoint

app.post('/api/auth/login', async (req, res) => {
  const { email, password } = req.body;

  // 1. Find user
  const user = await db.users.findOne({ email });
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

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

  // 3. Generate tokens
  const accessToken = generateAccessToken(user);
  const refreshToken = generateRefreshToken(user);

  // 4. Store refresh token (database or Redis)
  await db.refreshTokens.insert({
    userId: user.id,
    token: refreshToken,
    expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
  });

  // 5. Send tokens
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000
  });

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

Protected Route Middleware

function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

  if (!token) {
    return res.status(401).json({ error: 'Access token required' });
  }

  try {
    const user = verifyToken(token);
    req.user = user;
    next();
  } catch (error) {
    return res.status(403).json({ error: 'Invalid or expired token' });
  }
}

// Use middleware
app.get('/api/profile', authenticateToken, async (req, res) => {
  const user = await db.users.findById(req.user.userId);
  res.json(user);
});

Refresh Token Endpoint

app.post('/api/auth/refresh', async (req, res) => {
  const refreshToken = req.cookies.refreshToken;

  if (!refreshToken) {
    return res.status(401).json({ error: 'Refresh token required' });
  }

  try {
    // 1. Verify refresh token
    const payload = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);

    // 2. Check if token exists in database
    const storedToken = await db.refreshTokens.findOne({
      userId: payload.userId,
      token: refreshToken
    });

    if (!storedToken) {
      return res.status(403).json({ error: 'Invalid refresh token' });
    }

    // 3. Get user and verify token version
    const user = await db.users.findById(payload.userId);
    if (user.tokenVersion !== payload.tokenVersion) {
      return res.status(403).json({ error: 'Token revoked' });
    }

    // 4. Generate new access token
    const accessToken = generateAccessToken(user);

    res.json({ accessToken });

  } catch (error) {
    return res.status(403).json({ error: 'Invalid refresh token' });
  }
});

Logout Endpoint

app.post('/api/auth/logout', async (req, res) => {
  const refreshToken = req.cookies.refreshToken;

  if (refreshToken) {
    // Remove refresh token from database
    await db.refreshTokens.deleteOne({ token: refreshToken });
  }

  res.clearCookie('refreshToken');
  res.json({ message: 'Logged out successfully' });
});

Token Revocation

Increment Token Version

app.post('/api/auth/revoke-all', authenticateToken, async (req, res) => {
  // Increment token version to invalidate all tokens
  await db.users.updateOne(
    { _id: req.user.userId },
    { $inc: { tokenVersion: 1 } }
  );

  // Delete all refresh tokens
  await db.refreshTokens.deleteMany({ userId: req.user.userId });

  res.json({ message: 'All tokens revoked' });
});

Blacklist Tokens (Redis)

const redis = require('redis');
const client = redis.createClient();

async function blacklistToken(token, expiresIn) {
  await client.setex(`blacklist:${token}`, expiresIn, 'true');
}

async function isTokenBlacklisted(token) {
  const result = await client.get(`blacklist:${token}`);
  return result === 'true';
}

// Modified middleware
function authenticateToken(req, res, next) {
  const token = req.headers['authorization']?.split(' ')[1];

  if (!token) {
    return res.status(401).json({ error: 'Access token required' });
  }

  // Check blacklist
  if (await isTokenBlacklisted(token)) {
    return res.status(403).json({ error: 'Token revoked' });
  }

  try {
    const user = verifyToken(token);
    req.user = user;
    next();
  } catch (error) {
    return res.status(403).json({ error: 'Invalid token' });
  }
}

Frontend Integration

Store Tokens

// Store access token in memory (not localStorage!)
let accessToken = null;

async function login(email, password) {
  const response = await fetch('/api/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
    credentials: 'include'  // Include cookies
  });

  const data = await response.json();
  accessToken = data.accessToken;
}

Axios Interceptor

import axios from 'axios';

const api = axios.create({
  baseURL: '/api',
  withCredentials: true
});

// Add access token to requests
api.interceptors.request.use((config) => {
  if (accessToken) {
    config.headers.Authorization = `Bearer ${accessToken}`;
  }
  return config;
});

// Refresh token on 401
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        // Refresh token
        const response = await axios.post('/api/auth/refresh', {}, {
          withCredentials: true
        });

        accessToken = response.data.accessToken;

        // Retry original request
        originalRequest.headers.Authorization = `Bearer ${accessToken}`;
        return api(originalRequest);

      } catch (refreshError) {
        // Redirect to login
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }

    return Promise.reject(error);
  }
);

Security Best Practices

1. Use Strong Secrets

// ❌ Bad
const JWT_SECRET = 'mysecret';

// ✅ Good - generate random secret
// Use: openssl rand -base64 64
const JWT_SECRET = process.env.JWT_SECRET;

2. Short Access Token Expiry

// ✅ Good - short-lived access tokens
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });

// ✅ Good - long-lived refresh tokens
const refreshToken = jwt.sign(payload, secret, { expiresIn: '7d' });

3. Secure Cookie Settings

res.cookie('refreshToken', token, {
  httpOnly: true,      // Prevents XSS
  secure: true,        // HTTPS only
  sameSite: 'strict',  // Prevents CSRF
  maxAge: 7 * 24 * 60 * 60 * 1000
});

4. Never Store Sensitive Data

// ❌ Bad - sensitive data in JWT
const token = jwt.sign({
  userId: user.id,
  password: user.password,  // Never!
  creditCard: user.cc       // Never!
}, secret);

// ✅ Good - only necessary data
const token = jwt.sign({
  userId: user.id,
  email: user.email,
  role: user.role
}, secret);

5. Validate All Claims

const decoded = jwt.verify(token, secret, {
  issuer: 'your-app',
  audience: 'your-app-users',
  algorithms: ['HS256']  // Prevent algorithm confusion
});

Common Mistakes

❌ Storing JWT in localStorage

// ❌ Bad - vulnerable to XSS
localStorage.setItem('token', accessToken);

// ✅ Good - store in memory or httpOnly cookie
let accessToken = null;  // Memory
// or
res.cookie('token', refreshToken, { httpOnly: true });

❌ Long-lived Access Tokens

// ❌ Bad - can't revoke easily
jwt.sign(payload, secret, { expiresIn: '30d' });

// ✅ Good - short-lived with refresh
jwt.sign(payload, secret, { expiresIn: '15m' });

❌ No Token Revocation

// ✅ Good - implement token versioning
const payload = {
  userId: user.id,
  tokenVersion: user.tokenVersion
};

Testing

const request = require('supertest');

describe('Authentication', () => {
  it('should login and return tokens', async () => {
    const response = await request(app)
      .post('/api/auth/login')
      .send({ email: 'user@example.com', password: 'password' });

    expect(response.status).toBe(200);
    expect(response.body).toHaveProperty('accessToken');
  });

  it('should access protected route with valid token', async () => {
    const token = generateAccessToken({ userId: '123' });

    const response = await request(app)
      .get('/api/profile')
      .set('Authorization', `Bearer ${token}`);

    expect(response.status).toBe(200);
  });

  it('should reject invalid token', async () => {
    const response = await request(app)
      .get('/api/profile')
      .set('Authorization', 'Bearer invalid');

    expect(response.status).toBe(403);
  });
});

Conclusion

JWT authentication is powerful when implemented correctly. Remember to:

  • Use short-lived access tokens
  • Implement refresh token rotation
  • Store tokens securely
  • Validate all claims
  • Implement token revocation
  • Never store sensitive data in JWTs

Follow these best practices to build secure, scalable authentication systems.

JWTAuthenticationSecurityBackendWeb Development

Related Articles

Security & Compliance

Web3 Security: Protecting Smart Contracts and DApps

Essential security practices for blockchain developers building smart contracts and decentralized applications...

July 28, 2025
9 min
Read More
Security

Implementing ISO 27001: Lessons from Real Projects

What we learned helping companies achieve ISO 27001 certification and build robust security management systems...

October 15, 2025
5 min
Read More
Web Development

React Performance Optimization: Tips and Tricks for 2025

Master React performance optimization with modern techniques including memoization, code splitting, and concurrent features...

June 10, 2025
6 min
Read More