SyntaxStudy
Sign Up
Express.js Refresh Tokens and Secure Cookie Storage
Express.js Beginner 1 min read

Refresh Tokens and Secure Cookie Storage

Storing JWTs in `localStorage` exposes them to XSS attacks because any JavaScript on the page can read `localStorage`. A safer approach is to store the short-lived access token in memory (a JavaScript variable) and the refresh token in an `httpOnly; Secure; SameSite=Strict` cookie that JavaScript cannot read. When the access token expires, the client silently calls a `/auth/refresh` endpoint. The server reads the refresh token from the cookie, validates it against a whitelist in the database, and issues a new access token. Rotating the refresh token on every use (token rotation) limits the damage from a stolen refresh token. Refresh token revocation is straightforward with a database whitelist: delete the token entry to invalidate it immediately (for logout and password changes). Without a whitelist you must wait for the token to expire naturally, which is an unacceptable security gap. The added database round-trip is a worthwhile trade-off.
Example
const express      = require('express');
const jwt          = require('jsonwebtoken');
const cookieParser = require('cookie-parser');
const crypto       = require('crypto');
const router       = express.Router();
router.use(cookieParser());

const ACCESS_SECRET  = process.env.ACCESS_SECRET  || 'access_secret';
const REFRESH_SECRET = process.env.REFRESH_SECRET || 'refresh_secret';

// In-memory refresh token store (use Redis/DB in production)
const refreshTokens = new Set();

function issueTokens(res, userId) {
    const accessToken = jwt.sign({ sub: userId }, ACCESS_SECRET, { expiresIn: '15m' });
    const refreshToken = crypto.randomBytes(40).toString('hex');
    refreshTokens.add(refreshToken);

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

router.post('/login', (req, res) => {
    // Validate credentials (omitted for brevity)
    const accessToken = issueTokens(res, 1);
    res.json({ accessToken });
});

router.post('/refresh', (req, res) => {
    const { refreshToken } = req.cookies;
    if (!refreshToken || !refreshTokens.has(refreshToken)) {
        return res.status(401).json({ error: 'Invalid refresh token' });
    }
    refreshTokens.delete(refreshToken); // rotate
    const accessToken = issueTokens(res, 1);
    res.json({ accessToken });
});

router.post('/logout', (req, res) => {
    refreshTokens.delete(req.cookies.refreshToken);
    res.clearCookie('refreshToken');
    res.status(204).end();
});

module.exports = router;