Decoding vs Verifying in JavaScript
Decoding a JWT extracts the header and payload as JavaScript objects — no secret required. Verifying additionally recomputes the signature using the signing key and rejects any tampering, expiry, or algorithm mismatch. For debugging you often want just decoding; for any auth code that runs in production you always want verification.
Method 1: Browser / Vanilla JavaScript (No Dependencies)
Simple one-liner (ASCII payloads)
function decodeJwtPayload(token) {
const payloadB64 = token.split('.')[1];
const b64 = payloadB64.replace(/-/g, '+').replace(/_/g, '/');
return JSON.parse(atob(b64));
}
console.log(decodeJwtPayload('eyJhbG...MifQ.abc'));
// { sub: '1234567890', name: 'Ada', iat: 1516239022 }UTF-8 safe (recommended for real tokens)
function decodeJwt(token) {
const parts = token.split('.');
if (parts.length !== 3) throw new Error('Invalid JWT');
const decode = (s) => {
const b64 = s.replace(/-/g, '+').replace(/_/g, '/');
const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
return JSON.parse(new TextDecoder().decode(bytes));
};
return {
header: decode(parts[0]),
payload: decode(parts[1]),
signature: parts[2],
};
}
const { header, payload } = decodeJwt(myToken);
console.log(header); // { alg: 'HS256', typ: 'JWT' }
console.log(payload); // { sub: '123', exp: 1735689600, name: 'Ada 👋' }Method 2: jose (Recommended for Node.js and Edge)
Install
npm install joseDecode (Inspection Only)
import { decodeJwt, decodeProtectedHeader } from 'jose';
const header = decodeProtectedHeader(token);
const payload = decodeJwt(token); // does NOT verify signature
console.log(header); // { alg, kid, typ }
console.log(payload); // { sub, exp, iat, ... }Verify (HS256 — Shared Secret)
import { jwtVerify } from 'jose';
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
try {
const { payload, protectedHeader } = await jwtVerify(token, secret, {
algorithms: ['HS256'],
audience: 'my-service',
issuer: 'https://auth.example.com',
});
console.log('Valid:', payload);
} catch (err) {
if (err.code === 'ERR_JWT_EXPIRED') return res.status(401).send('Expired');
if (err.code === 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') return res.status(401).send('Bad signature');
return res.status(401).send('Invalid token');
}Verify (RS256 with JWKS — Auth0, Cognito, Google)
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(
new URL('https://YOUR_DOMAIN/.well-known/jwks.json')
);
const { payload } = await jwtVerify(token, JWKS, {
algorithms: ['RS256'],
audience: 'my-api',
issuer: 'https://YOUR_DOMAIN/',
});
// jose caches the JWKS and auto-refreshes on kid miss.
// You get automatic key rotation for free.Method 3: jsonwebtoken (Legacy Node.js Classic)
// npm install jsonwebtoken
const jwt = require('jsonwebtoken');
// Decode only
const decoded = jwt.decode(token, { complete: true });
console.log(decoded.header, decoded.payload);
// Verify with HS256 secret
try {
const payload = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'],
audience: 'my-service',
issuer: 'https://auth.example.com',
});
} catch (err) {
if (err.name === 'TokenExpiredError') return res.status(401).send('Expired');
if (err.name === 'JsonWebTokenError') return res.status(401).send('Invalid');
throw err;
}Method 4: Next.js Middleware
// middleware.js — runs on the edge, must use jose (jsonwebtoken doesn't work here)
import { NextResponse } from 'next/server';
import { jwtVerify } from 'jose';
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
export async function middleware(request) {
const token = request.cookies.get('access_token')?.value;
if (!token) return NextResponse.redirect(new URL('/login', request.url));
try {
const { payload } = await jwtVerify(token, secret, {
algorithms: ['HS256'],
});
// Attach user id to a header the app can read
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', payload.sub);
return NextResponse.next({ request: { headers: requestHeaders } });
} catch {
return NextResponse.redirect(new URL('/login', request.url));
}
}
export const config = { matcher: '/dashboard/:path*' };Common JavaScript JWT Pitfalls
- Using
atobon Base64URL directly — always replace-with+and_with/first. - Losing UTF-8 characters —
atobreturns binary strings, not UTF-8. Convert withTextDecoderfor non-ASCII claims. - Verifying on the client — never do this with a shared secret; the secret would be in your JS bundle for anyone to inspect. Verification belongs on the server.
- Skipping the
algorithmsoption — always pass an explicit list. Never acceptalg: none. - Using
jwt.decode()for auth — jsonwebtoken'sdecode()does NOT verify. Usejwt.verify(). - Missing
awaitwith jose —jwtVerify()returns a Promise; forgetting await gives you a Promise object where you expect a payload.
TypeScript Typing for Decoded Payloads
import { jwtVerify, JWTPayload } from 'jose';
interface AppJwtPayload extends JWTPayload {
sub: string; // required in your app
role: 'user' | 'admin';
tenant_id: string;
}
const { payload } = await jwtVerify<AppJwtPayload>(token, secret, {
algorithms: ['HS256'],
});
// payload.role is now typed as 'user' | 'admin'Related Tools
- JWT Decoder Online — paste-and-inspect UI
- Decode JWT Token — language-agnostic walkthrough
- JWT Decoder in Python — PyJWT and python-jose
- Verify JWT Signature — deep dive on HS256/RS256 verification
- Base64 Encode in JavaScript — the encoding JWT is built on