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:
- Header — Contains the token type (
typ) and signing algorithm (alg) - Payload — Contains the claims (statements about the entity and additional metadata)
- 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 jsonwebtokenOr using yarn:
yarn add jsonwebtokenFor TypeScript projects, install type definitions as a dev dependency:
npm install -D @types/jsonwebtokenjwt.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.pemToken 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
httpOnlyandSecurecookies withSameSite=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
- JWT Encoder / Decoder — Encode and decode JWTs directly in the browser
- JWT Decoder — Inspect the header and payload of any JWT
- REST API Client — Test your JWT-protected API endpoints
- Base64 Encoder — Encode and decode Base64 data (useful for understanding JWT parts)
- JSON Formatter — Pretty-print and validate JSON payloads