Skip to content
LangStop

Tutorials

Keyboard Shortcuts

ActionShortcut
Toggle SidebarCtrl+B
Save TabCtrl+S
Close TabAlt+W
Switch to Tab 1Alt+Shift+1
Switch to Tab 2Alt+Shift+2
Switch to Tab 3Alt+Shift+3
Switch to Tab 4Alt+Shift+4
Switch to Tab 5Alt+Shift+5
Switch to Tab 6Alt+Shift+6
Switch to Tab 7Alt+Shift+7
Switch to Tab 8Alt+Shift+8
Switch to Tab 9Alt+Shift+9

How to Generate JWT in Node.js — JWT Signing Guide

What is JWT?

JWT (JSON Web Token) is an open standard (RFC 7519) that defines a compact and self-contained way to securely transmit information between parties as a JSON object. JWTs are digitally signed, which means the information can be verified and trusted.

A JWT consists of three parts separated by dots:

  1. Header — Contains the token type (typ) and signing algorithm (alg)
  2. Payload — Contains the claims (statements about the entity and additional metadata)
  3. Signature — Verifies the token hasn't been tampered with

A typical JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWTs are commonly used for authentication, authorization (Bearer tokens), and secure data exchange. You can inspect any JWT using the LangStop JWT Decoder.


Installing jsonwebtoken

The jsonwebtoken npm package is the de facto standard for JWT operations in Node.js. Install it with:

npm install jsonwebtoken

Or using yarn:

yarn add jsonwebtoken

For TypeScript projects, install type definitions as a dev dependency:

npm install -D @types/jsonwebtoken

jwt.sign() — Generating a Token

The jwt.sign() method creates a signed JWT. At minimum, it requires a payload and a secret or private key:

const jwt = require('jsonwebtoken');
 
const payload = {
  userId: 123,
  role: 'admin',
};
 
const secret = 'your-secret-key';
 
const token = jwt.sign(payload, secret);
 
console.log(token);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Specifying Options

const token = jwt.sign(
  { userId: 123 },
  secret,
  {
    expiresIn: '1h',       // Token expiration
    issuer: 'my-app',       // iss claim
    subject: 'user:123',    // sub claim
    audience: 'api:prod',   // aud claim
    algorithm: 'HS256',     // Signing algorithm
    notBefore: 0,           // nbf claim (seconds since epoch)
    jwtid: 'unique-id-1',   // jti claim (unique identifier)
  }
);

Using a Callback

jwt.sign({ userId: 123 }, secret, { expiresIn: '1h' }, (err, token) => {
  if (err) {
    console.error('Signing error:', err);
    return;
  }
  console.log('Generated token:', token);
});

The callback is called asynchronously. If no callback is provided, jwt.sign() returns the token synchronously.


JWT Claims

Claims are statements encoded in the JWT payload. They fall into three categories:

Registered Claims (Standard)

These are predefined claims defined by the JWT specification:

Claim Full Name Description Example
sub Subject The entity the token applies to (usually a user ID) "user:123"
iss Issuer The party that issued the token "my-app"
aud Audience The intended recipient of the token "api:prod"
exp Expiration Unix timestamp when the token expires 1718956800
nbf Not Before Unix timestamp before which the token is invalid 1718870400
iat Issued At Unix timestamp when the token was issued 1718870400
jti JWT ID Unique identifier for the token (prevents replay) "uuid-v4"

Public Claims

Custom claims that you define. To avoid collisions, register them in the IANA JSON Web Token Registry or use a namespaced naming convention:

const token = jwt.sign(
  {
    sub: 'user:123',
    // Custom public claims
    'https://api.example.com/claims/role': 'admin',
    'https://api.example.com/claims/permissions': ['read', 'write'],
  },
  secret,
  { expiresIn: '1h' }
);

Private Claims

Custom claims agreed upon between the issuer and the consumer. These are not registered or namespaced:

const token = jwt.sign(
  {
    sub: 'user:123',
    // Private claims
    role: 'admin',
    department: 'engineering',
    features: ['dashboard', 'reports'],
  },
  secret,
  { expiresIn: '1h' }
);

Signing Algorithms: HS256 vs RS256

HS256 (HMAC with SHA-256)

HS256 uses a shared secret — the same key is used to sign and verify tokens.

const jwt = require('jsonwebtoken');
const secret = process.env.JWT_SECRET;
 
// Signing
const token = jwt.sign({ userId: 123 }, secret, { algorithm: 'HS256' });
 
// Verifying
const decoded = jwt.verify(token, secret);

Pros: Simple, fast, no need for key pairs. Cons: The secret must be shared with every service that needs to verify tokens. Compromising the secret allows forging tokens.

Best for: Single-service applications, development environments, internal microservices on a trusted network.

RS256 (RSA with SHA-256)

RS256 uses a public/private key pair. The token is signed with the private key and verified with the public key.

const jwt = require('jsonwebtoken');
const fs = require('fs');
 
const privateKey = fs.readFileSync('private.pem', 'utf8');
const publicKey = fs.readFileSync('public.pem', 'utf8');
 
// Signing with private key
const token = jwt.sign({ userId: 123 }, privateKey, { algorithm: 'RS256' });
 
// Verifying with public key
const decoded = jwt.verify(token, publicKey);

Pros: The private key stays on the authorization server. Any service with the public key can verify tokens without being able to forge them. Cons: Slower, requires key pair management.

Best for: Multi-service architectures, API gateways, third-party integrations, production environments.

Algorithm Comparison

Aspect HS256 RS256
Key Type Shared secret Public/private key pair
Speed Fast Slower (asymmetric crypto)
Key Distribution Must share secret with all verifiers Share only public key
Security Risk Secret compromise = forged tokens Private key stays on issuer
Use Case Single app, dev environments Microservices, production
Token Size Smaller Slightly larger

jwt.verify() — Verifying a Token

Verifying a token confirms its signature and checks that it hasn't expired:

const jwt = require('jsonwebtoken');
 
const token = 'eyJhbGciOiJIUzI1NiIs...';
const secret = process.env.JWT_SECRET;
 
try {
  const decoded = jwt.verify(token, secret);
  console.log('Token is valid:', decoded);
  // { userId: 123, role: 'admin', iat: 1718870400, exp: 1718874000 }
} catch (err) {
  if (err.name === 'TokenExpiredError') {
    console.error('Token has expired');
  } else if (err.name === 'JsonWebTokenError') {
    console.error('Invalid token signature');
  } else if (err.name === 'NotBeforeError') {
    console.error('Token is not active yet');
  } else {
    console.error('Verification error:', err.message);
  }
}

Verifying with Options

You can enforce specific claims during verification:

try {
  const decoded = jwt.verify(token, secret, {
    algorithms: ['HS256'],       // Only accept HS256
    issuer: 'my-app',             // Must match iss claim
    audience: 'api:prod',         // Must match aud claim
    subject: 'user:123',          // Must match sub claim
    clockTolerance: 30,           // 30 seconds leeway for clock drift
    ignoreExpiration: false,      // Reject expired tokens
  });
} catch (err) {
  // Handle error
}

jwt.decode() — Decoding Without Verification

Use jwt.decode() to read the payload without verifying the signature. This is useful for debugging or reading claims from an already-verified token:

const decoded = jwt.decode(token);
console.log(decoded);
// { userId: 123, role: 'admin', iat: 1718870400, exp: 1718874000 }
// WARNING: This does NOT verify the signature!

Always use jwt.verify() before trusting the payload in production code.


Async Patterns

Promise-based (using util.promisify)

const jwt = require('jsonwebtoken');
const { promisify } = require('util');
 
const signAsync = promisify(jwt.sign);
const verifyAsync = promisify(jwt.verify);
 
async function generateAndVerify() {
  const secret = process.env.JWT_SECRET;
 
  const token = await signAsync({ userId: 123 }, secret, { expiresIn: '1h' });
  console.log('Token:', token);
 
  const decoded = await verifyAsync(token, secret);
  console.log('Decoded:', decoded);
}
 
generateAndVerify().catch(console.error);

With async/await and try/catch

const jwt = require('jsonwebtoken');
 
function signToken(payload) {
  return new Promise((resolve, reject) => {
    jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1h' }, (err, token) => {
      if (err) reject(err);
      else resolve(token);
    });
  });
}
 
function verifyToken(token) {
  return new Promise((resolve, reject) => {
    jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
      if (err) reject(err);
      else resolve(decoded);
    });
  });
}
 
// Usage
(async () => {
  try {
    const token = await signToken({ userId: 123 });
    const decoded = await verifyToken(token);
    console.log('Success:', decoded);
  } catch (err) {
    console.error('JWT operation failed:', err.message);
  }
})();

Environment Variables for Secrets

Never hardcode JWT secrets in your source code. Use environment variables:

# .env file
JWT_SECRET=a-complex-and-long-secret-string-at-least-32-chars
JWT_REFRESH_SECRET=a-different-secret-for-refresh-tokens
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
// Load .env in development
require('dotenv').config();
 
const jwt = require('jsonwebtoken');
 
function generateAccessToken(user) {
  return jwt.sign(
    { userId: user.id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: process.env.JWT_EXPIRES_IN || '15m' }
  );
}

Generating a Strong Secret

Use Node.js built-in crypto to generate a cryptographically strong secret:

node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
# Output: a7d3b8f2e1c9...

For HS256, the secret should be at least 32 bytes (256 bits). For RS256, generate a proper RSA key pair:

# Generate private key
openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
 
# Extract public key
openssl rsa -pubout -in private.pem -out public.pem

Token Expiration

Setting a reasonable expiration time is one of the most important security practices for JWTs:

// Short-lived access token
const accessToken = jwt.sign(
  { userId: 123 },
  process.env.JWT_SECRET,
  { expiresIn: '15m' }    // 15 minutes
);
 
// Longer-lived refresh token
const refreshToken = jwt.sign(
  { userId: 123, tokenId: 'refresh-uuid' },
  process.env.JWT_REFRESH_SECRET,
  { expiresIn: '7d' }     // 7 days
);

Common Expiration Values

Use Case Duration expiresIn Value
Access token (high security) 5–15 minutes '5m', '15m'
Access token (standard) 1 hour '1h'
Access token (low security) 24 hours '24h'
Refresh token 7 days '7d'
Refresh token (remember me) 30 days '30d'
Email verification link 24 hours '24h'
Password reset token 1 hour '1h'

Checking Expiration Manually

const decoded = jwt.decode(token);
const now = Math.floor(Date.now() / 1000);
 
if (decoded.exp < now) {
  console.log('Token has expired');
} else {
  console.log(`Token expires in ${decoded.exp - now} seconds`);
}

Refresh Tokens

A refresh token strategy allows users to obtain new access tokens without re-authenticating:

const jwt = require('jsonwebtoken');
const crypto = require('crypto');
 
// Store refresh tokens (in production, use a database)
const refreshTokens = new Map();
 
function generateAccessToken(user) {
  return jwt.sign(
    { userId: user.id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }
  );
}
 
function generateRefreshToken(user) {
  const refreshTokenId = crypto.randomUUID();
  const refreshToken = jwt.sign(
    { userId: user.id, tokenId: refreshTokenId },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: '7d' }
  );
 
  // Store the refresh token (replace with DB in production)
  refreshTokens.set(refreshTokenId, {
    userId: user.id,
    createdAt: new Date(),
  });
 
  return refreshToken;
}
 
function refreshAccessToken(oldRefreshToken) {
  try {
    const decoded = jwt.verify(oldRefreshToken, process.env.JWT_REFRESH_SECRET);
 
    // Check if the refresh token is still valid in our store
    if (!refreshTokens.has(decoded.tokenId)) {
      throw new Error('Refresh token has been revoked');
    }
 
    // Generate a new access token
    return generateAccessToken({ id: decoded.userId });
 
  } catch (err) {
    throw new Error('Invalid refresh token');
  }
}

Refresh Token Rotation

For enhanced security, rotate refresh tokens on each use — invalidate the old one and issue a new one. This limits the window for token theft:

function rotateRefreshToken(oldRefreshToken) {
  const decoded = jwt.verify(oldRefreshToken, process.env.JWT_REFRESH_SECRET);
 
  // Remove old token
  refreshTokens.delete(decoded.tokenId);
 
  // Issue new access + refresh tokens
  const accessToken = generateAccessToken({ id: decoded.userId });
  const newRefreshToken = generateRefreshToken({ id: decoded.userId });
 
  return { accessToken, refreshToken: newRefreshToken };
}

Security Best Practices

1. Keep Secrets Out of Source Code

Never commit JWT secrets to version control. Use environment variables (.env), secret managers (AWS Secrets Manager, HashiCorp Vault), or CI/CD secrets.

2. Use Short Expiration Times

Access tokens should expire in 5–15 minutes. This limits the damage if a token is stolen. Use refresh tokens for longer sessions.

3. Validate All Claims on Verification

const decoded = jwt.verify(token, secret, {
  algorithms: ['HS256'],
  issuer: 'my-app',
  audience: 'api-prod',
});

4. Prefer RS256 Over HS256 in Multi-Service Architectures

With RS256, each service only needs the public key to verify tokens. The private key remains on the authentication server.

5. Implement Token Revocation

Maintain a blocklist of revoked tokens (by jti claim) or use a version number in the payload that increments when a user changes their password:

// Add token version to user's profile
const token = jwt.sign(
  {
    userId: user.id,
    tokenVersion: user.tokenVersion, // Increment on password change
  },
  secret,
  { expiresIn: '15m' }
);
 
// Verify token version
function verifyAndCheckVersion(token) {
  const decoded = jwt.verify(token, secret);
  const user = getUserFromDb(decoded.userId);
 
  if (decoded.tokenVersion !== user.tokenVersion) {
    throw new Error('Token has been revoked');
  }
 
  return decoded;
}

6. Use HTTPS Everywhere

JWTs transmitted over plain HTTP can be intercepted. Always use TLS (HTTPS) in production.

7. Store Tokens Securely

  • Web apps: Use httpOnly and Secure cookies with SameSite=Strict
  • Mobile apps: Use the device's secure keychain (iOS Keychain, Android Keystore)
  • Never store tokens in localStorage (vulnerable to XSS attacks)

8. Limit Token Size

Keep the payload as small as possible. Every byte increases the token size, which affects bandwidth and storage. Include only essential claims — avoid adding large objects like full user profiles.

9. Use Unique Token IDs (jti)

Assign a unique jti to each token. This enables logging, auditing, and targeted revocation:

const crypto = require('crypto');
 
const token = jwt.sign(
  {
    userId: 123,
    jti: crypto.randomUUID(),
  },
  secret,
  { expiresIn: '15m' }
);

10. Handle Errors Gracefully

Always wrap verification in try/catch blocks. Provide clear, user-friendly error messages without revealing internal details:

function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;
 
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or malformed token' });
  }
 
  const token = authHeader.split(' ')[1];
 
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET, {
      algorithms: ['HS256'],
      issuer: 'my-app',
    });
    req.user = decoded;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
    }
    return res.status(403).json({ error: 'Invalid token', code: 'INVALID_TOKEN' });
  }
}

Complete Example: Auth Middleware with Express

const express = require('express');
const jwt = require('jsonwebtoken');
require('dotenv').config();
 
const app = express();
app.use(express.json());
 
// Mock user store
const users = [
  { id: 1, username: 'alice', password: 'hashed-password', role: 'admin' },
];
 
// Login endpoint
app.post('/login', (req, res) => {
  const { username, password } = req.body;
 
  // Validate credentials (use bcrypt in production)
  const user = users.find(u => u.username === username);
  if (!user || password !== 'password123') {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
 
  // Generate tokens
  const accessToken = jwt.sign(
    { userId: user.id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }
  );
 
  const refreshToken = jwt.sign(
    { userId: user.id, tokenId: require('crypto').randomUUID() },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: '7d' }
  );
 
  res.json({ accessToken, refreshToken });
});
 
// Auth middleware
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
 
  if (!token) {
    return res.sendStatus(401);
  }
 
  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) {
      return res.sendStatus(403);
    }
    req.user = user;
    next();
  });
}
 
// Protected route
app.get('/me', authenticateToken, (req, res) => {
  res.json({ user: req.user });
});
 
// Refresh endpoint
app.post('/refresh', (req, res) => {
  const { refreshToken } = req.body;
 
  if (!refreshToken) {
    return res.sendStatus(401);
  }
 
  try {
    const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
    const newAccessToken = jwt.sign(
      { userId: decoded.userId, role: decoded.role },
      process.env.JWT_SECRET,
      { expiresIn: '15m' }
    );
    res.json({ accessToken: newAccessToken });
  } catch (err) {
    res.sendStatus(403);
  }
});
 
app.listen(3000, () => console.log('Server running on port 3000'));

Using the LangStop JWT Tools

Related Tools

Try these complementary developer tools: