Skip to main content
Bytes & Beyond

JWT in Node.js with jsonwebtoken

Implement JWT authentication in Node.js - signing, verifying, middleware, refresh tokens, and production patterns

jsonwebtoken is the industry-standard implementation of JSON Web Tokens for Node.js. It provides methods to sign, verify, and decode JWTs.

Installation

npm install jsonwebtoken

Signing a Token

Signing is the process of creating a new JWT. You take a payload (data) and a secret key, and the library generates a signed string.

const jwt = require('jsonwebtoken');

const payload = { userId: 123, role: 'admin' };
const secretKey = process.env.JWT_SECRET;

// Basic signing
const token = jwt.sign(payload, secretKey);

// With expiration (always recommended)
const token = jwt.sign(payload, secretKey, { expiresIn: '1h' });

// Other expiration formats: '2 days', '10h', '7d', or seconds (e.g., 60)

Verifying a Token

Verifying checks if the token is authentic (not tampered with) and not expired. If invalid, it throws an error.

const receivedToken = '...';

try {
  const decoded = jwt.verify(receivedToken, secretKey);
  console.log("Token is valid:", decoded);
  // Output: { userId: 123, role: 'admin', iat: 1700000000, exp: 1700003600 }
} catch(err) {
  console.log("Invalid token:", err.message);
}

Handling Specific Errors

jwt.verify(token, secretKey, (err, decoded) => {
  if (err) {
    if (err.name === 'TokenExpiredError') {
      console.log("Token has expired");
    } else if (err.name === 'JsonWebTokenError') {
      console.log("Token is malformed/invalid");
    }
  } else {
    console.log("Success!", decoded);
  }
});

Decoding (No Verification)

Read the token’s content without validating the signature. Never trust this for authentication.

const decoded = jwt.decode(token);
console.log(decoded);
// Returns payload without verifying signature

Express Middleware

Protect routes by checking the Authorization header for a valid token.

function authenticateToken(req, res, next) {
  // 1. Get token from header: "Bearer <token>"
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) return res.sendStatus(401); // Unauthorized

  // 2. Verify token
  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) return res.sendStatus(403); // Forbidden

    // 3. Attach user to request
    req.user = user;
    next();
  });
}

// Usage
app.get('/dashboard', authenticateToken, (req, res) => {
  res.json({ message: `Welcome ${req.user.username}` });
});

Complete Login Example

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

async function login(req, res) {
    // 1. Find user
    const user = await User.findOne({ email: req.body.email });
    if (!user) throw new Error('User not found');

    // 2. Verify password
    const valid = await bcrypt.compare(req.body.password, user.password);
    if (!valid) throw new Error('Invalid password');

    // 3. Generate token
    const token = jwt.sign(
        { user_id: user._id, email: user.email, role: user.role },
        process.env.JWT_SECRET,
        { expiresIn: '7d' }
    );

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

    res.json({ message: 'Login successful' });
}

Asymmetric Algorithms (RS256)

For microservices or higher security, use asymmetric encryption:

  • Private Key: Signs (creates) the token
  • Public Key: Verifies the token
const fs = require('fs');

// Signing with private key
const privateKey = fs.readFileSync('private.key');
const token = jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256' });

// Verifying with public key
const publicKey = fs.readFileSync('public.pem');
jwt.verify(token, publicKey, { algorithms: ['RS256'] }, (err, decoded) => {
  console.log(decoded.foo); // 'bar'
});

Token Refresh Pattern

For security, use two tokens:

  1. Access Token: Short life (15 mins). Used for API calls.
  2. Refresh Token: Long life (7 days). Stored in DB. Used only to get a new access token.
// At login: Generate both tokens
const accessToken = jwt.sign(user, JWT_SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ user_id: user.id }, REFRESH_SECRET, { expiresIn: '7d' });

// Store refresh token in DB for revocation
await RefreshToken.create({ user_id: user.id, token: refreshToken });

// Refresh endpoint
async function refresh(req, res) {
    const { refreshToken } = req.body;
    
    const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
    
    // Check if token exists in DB (not revoked)
    const stored = await RefreshToken.findOne({ token: refreshToken });
    if (!stored) throw new Error('Token revoked');
    
    // Issue new access token
    const user = await User.findById(decoded.user_id);
    const newAccessToken = jwt.sign(
        { user_id: user.id, email: user.email },
        JWT_SECRET,
        { expiresIn: '15m' }
    );
    
    res.json({ accessToken: newAccessToken });
}

Implementing Logout

JWT is stateless, but you can implement effective logout with a blacklist:

async function logout(req, res) {
    await TokenBlacklist.create({
        token: req.cookies.token,
        expires_at: new Date(req.user.exp * 1000)
    });
    res.clearCookie('token');
    res.json({ message: 'Logged out' });
}

// Verify checks blacklist
async function verifyToken(token) {
    const decoded = jwt.verify(token, secret);
    const blacklisted = await TokenBlacklist.findOne({ token });
    if (blacklisted) throw new Error('Token revoked');
    return decoded;
}

Environment Variables

JWT_SECRET=your_super_secret_key_minimum_32_characters
JWT_EXPIRY=7d
REFRESH_TOKEN_SECRET=different_secret_for_refresh_tokens
COOKIE_NAME=authToken

Debugging

Use jwt.io to paste a token and visually inspect its header and payload structure.