Verify JWT in Node.js (jsonwebtoken & jose)
Verify JWT signatures in Node.js with jsonwebtoken and jose. Sign and verify HS256 and RS256 tokens, validate iss/aud/exp claims, and verify JWTs against a JWKS endpoint with full code examples.
Node.js has two production-grade JWT libraries: jsonwebtoken (the long-standing default for HS256/RS256) and jose (the modern choice that supports every JOSE algorithm including EdDSA, plus built-in JWKS verification). This page covers both: signing, verifying with claim validation, and verifying against a JWKS endpoint.
Install
# jsonwebtoken — HS256, RS256, simple synchronous API
npm install jsonwebtoken
# jose — all JOSE algorithms, JWKS, JWE, maintained
npm install jose
Sign and verify an HS256 token (jsonwebtoken)
const jwt = require("jsonwebtoken");
// 32+ bytes of cryptographically random data — never a password
const secret = process.env.JWT_SECRET;
// Sign
const token = jwt.sign(
{ sub: "user_123", role: "admin" },
secret,
{
algorithm: "HS256",
expiresIn: "15m",
issuer: "https://auth.example.com",
audience: "https://api.example.com",
}
);
// Verify — hardcode the algorithm, validate iss + aud + exp
const payload = jwt.verify(token, secret, {
algorithms: ["HS256"], // never read from token header
issuer: "https://auth.example.com",
audience: "https://api.example.com",
clockTolerance: 60, // 60s skew tolerance for exp/nbf
});
console.log(payload.sub); // "user_123"
Sign and verify an RS256 token (jose)
import { SignJWT, jwtVerify, importPKCS8, importSPKI } from "jose";
const privateKey = await importPKCS8(process.env.RSA_PRIVATE_PEM, "RS256");
const publicKey = await importSPKI(process.env.RSA_PUBLIC_PEM, "RS256");
// Sign
const token = await new SignJWT({ sub: "user_123", role: "admin" })
.setProtectedHeader({ alg: "RS256", kid: "rsa-key-v1" })
.setIssuedAt()
.setIssuer("https://auth.example.com")
.setAudience("https://api.example.com")
.setExpirationTime("15m")
.sign(privateKey);
// Verify
const { payload } = await jwtVerify(token, publicKey, {
issuer: "https://auth.example.com",
audience: "https://api.example.com",
});
Verify a JWT against a JWKS endpoint (jose)
When you validate tokens issued by Auth0, Okta, AWS Cognito, Google, or Microsoft Entra ID, the public keys live at a JWKS URL. jose handles fetching and caching for you:
import { jwtVerify, createRemoteJWKSet } from "jose";
const JWKS_URL = new URL("https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json");
const JWKS = createRemoteJWKSet(JWKS_URL); // jose caches keys automatically
async function verifyAccessToken(token) {
const { payload, protectedHeader } = await jwtVerify(token, JWKS, {
issuer: `https://YOUR_DOMAIN.auth0.com/`,
audience: "https://api.example.com",
});
return payload;
}
createRemoteJWKSet caches the JWKS response and only refetches when a token’s kid is not in the cache or the cached set is stale. Do not fetch the JWKS yourself on every request — that adds latency and makes the issuer a synchronous dependency for every authenticated request.
Validation order
Verify in this exact order — reading claims before signature verification is a security bug:
- Structure — is it three dot-separated base64url segments?
- Signature — does it verify against the expected key and hardcoded algorithm?
- Claims —
exp(not expired),nbf(active),iss(expected issuer),aud(expected audience). - Authorization — do the claims permit this action?
Both jsonwebtoken and jose perform steps 1–2 on every verify call. Steps 3–4 require you to pass issuer and audience explicitly — neither library guesses them.
Continue reading
- JWT decoder → - paste a token to decode and verify it in your browser
- JWT Signing Algorithms → - HS256, RS256, ES256, EdDSA with key sizes and code
- Registered Claims Reference → - iss, sub, aud, exp, jti, and every RFC 7519 claim
- JWT Security Vulnerabilities → - alg:none, algorithm confusion, kid injection, and every fix
- Verify JWT in Python → - PyJWT and SimpleJWT code
- Verify JWT in Java → - Spring Security and jjwt, including the aud mismatch fix