How to Decode a JWT in Node.js
Decode JWT header and payload in Node.js safely, and when to verify signatures instead of only decoding.
By Mariana Soto · Published 2026-07-04 · Updated 2026-08-22
Decoding a JWT in Node.js is straightforward — but decoding alone does not authenticate a request. Anyone can Base64URL-decode a token and read its claims; only signature verification proves the token was issued by a party you trust. Use decode for inspection and debugging; use verify for trust.
This guide walks through manual decoding (educational), library-based verification (production), and how Cyberway fits into a local development workflow.
Split and Base64URL-decode manually
A JWT is header.payload.signature. Each of the first two parts is JSON encoded as Base64URL (URL-safe Base64 without padding).
function base64UrlDecode(str) {
const padded = str + "=".repeat((4 - (str.length % 4)) % 4);
const base64 = padded.replace(/-/g, "+").replace(/_/g, "/");
return Buffer.from(base64, "base64").toString("utf8");
}
function decodeJwt(token) {
const parts = token.split(".");
if (parts.length !== 3) {
throw new Error("Invalid JWT structure");
}
const [headerPart, payloadPart] = parts;
return {
header: JSON.parse(base64UrlDecode(headerPart)),
payload: JSON.parse(base64UrlDecode(payloadPart)),
};
}
const { header, payload } = decodeJwt(token);
console.log(header); // { alg: 'HS256', typ: 'JWT' }
console.log(payload); // { sub: '...', exp: ... }
This teaches the format but must not replace verification in production. An attacker can craft any payload and append a garbage signature — your decode function will happily parse it.
Why not hand-roll crypto in production
Manual decoding skips critical checks:
- Signature validation — the core of authentication
- Algorithm allowlisting — prevents
alg: noneand algorithm confusion - Time claims —
exp,nbfenforcement - Issuer and audience — prevents cross-service token reuse
Padding edge cases, Unicode in claims, and timing-safe comparison are easy to get wrong. Use a maintained library.
Verify with jose (recommended)
jose is the library Cyberway uses internally. For HS256 verification:
import { jwtVerify, decodeJwt } from "jose";
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
// Inspect without verifying (debug only)
const claims = decodeJwt(token);
console.log(claims.exp, claims.sub);
// Production path — verify signature and claims
const { payload, protectedHeader } = await jwtVerify(token, secret, {
algorithms: ["HS256"],
issuer: "https://auth.example.com",
audience: "https://api.example.com",
});
console.log(payload.sub);
console.log(protectedHeader.alg);
For RS256, pass a CryptoKey or import a PEM public key instead of a shared secret. Always pass algorithms explicitly — never trust the header's alg alone.
Error handling
Wrap verification and map errors to HTTP responses:
import { jwtVerify, errors } from "jose";
try {
const { payload } = await jwtVerify(token, secret, {
algorithms: ["HS256"],
});
req.user = payload;
} catch (err) {
if (err instanceof errors.JWTExpired) {
return res.status(401).json({ error: "token_expired" });
}
if (err instanceof errors.JWTClaimValidationFailed) {
return res.status(401).json({ error: "invalid_claims" });
}
return res.status(401).json({ error: "invalid_token" });
}
Log the error type server-side; return generic messages to clients to avoid leaking validation details.
Testing fixtures with Cyberway
A practical workflow:
- Open the Cyberway JWT Generator and create a token with known claims and a test secret.
- Copy the token into your Node test or REPL.
- Run
jwtVerifywith the same secret and confirm claims match. - Paste the token into the JWT Decoder to visually compare what Cyberway shows vs what your code reads.
Generate tokens with deliberate flaws (expired exp, weak secret) to test your error paths. Because Cyberway runs client-side, test secrets never leave your machine during fixture creation.
Security checklist before production
- [ ] Verify signature on every protected route
- [ ] Allowlist algorithms (
HS256only, orRS256only — not both blindly) - [ ] Validate
expandnbf(library default injwtVerify) - [ ] Validate
issandaudwhen multiple services share an auth issuer - [ ] Reject tokens with unexpected claim shapes (schema validation)
- [ ] Never log full tokens in production — they are credentials
See RFC 7515 for JWS structure and common JWT security mistakes for pitfalls.
Express middleware example
A minimal Express middleware pattern:
import { jwtVerify } from "jose";
export function requireAuth(secret) {
return async (req, res, next) => {
const header = req.headers.authorization;
if (!header?.startsWith("Bearer ")) {
return res.status(401).json({ error: "missing_token" });
}
const token = header.slice(7);
try {
const { payload } = await jwtVerify(token, secret, {
algorithms: ["HS256"],
});
req.auth = payload;
next();
} catch {
return res.status(401).json({ error: "invalid_token" });
}
};
}
Mount this only on protected routes — public health checks should not parse JWTs at all.
Unit testing tips
- Generate fixtures with Cyberway JWT Generator and commit only the public parts (expected claims JSON), not secrets.
- Test expired tokens by setting
exptoMath.floor(Date.now() / 1000) - 10at test runtime. - Test algorithm rejection by passing a token signed with RS256 to an HS256-only verifier.
When to decode vs verify in Node
Use decodeJwt from jose only in development tooling, admin scripts, or log redaction pipelines where you need claim visibility without trusting the input. Every HTTP middleware, WebSocket auth hook, and background job consumer that gates access must call jwtVerify. If you are unsure which path a file uses, search for decode without Verify — that is a common audit finding.
Continue with the Complete JWT Guide for claims, algorithms, and lifecycle design.
Try the Cyberway JWT Decoder or JWT Generator — both run entirely in your browser.
