Security
JWT validation: a runnable example and failure checks
Verify a synthetic token with a named library, reject the wrong audience and expiry, and distinguish decoding from authentication.
A common signed JWT uses three base64url segments: header, payload and signature. Its readable payload is not encrypted. JWT also supports encrypted representations, so the three-segment description is not a rule for every JWT.
Reproduce verification with disposable keys
Create a temporary directory, download jwt-validation.mjs, and install the library used by the example:
npm install --save-exact jose@6.1.3
node jwt-validation.mjs
The example was checked with Node 22.23.2. Installation downloads a package; the script itself makes no network requests. It creates temporary ES256 keys and a fictional user, then runs a positive case and two negative cases. Never substitute a production token or private key into a public example.
import { generateKeyPair, SignJWT, jwtVerify, decodeJwt } from 'jose';
import assert from 'node:assert/strict';
// Disposable demonstration keys and claims. Never a production key or live token.
const { publicKey, privateKey } = await generateKeyPair('ES256');
const token = await new SignJWT({ role: 'reader' })
.setProtectedHeader({ alg: 'ES256', typ: 'JWT' })
.setSubject('demo-user').setIssuer('https://issuer.example')
.setAudience('https://api.example').setIssuedAt().setExpirationTime('2m')
.sign(privateKey);
const options = {
algorithms: ['ES256'], issuer: 'https://issuer.example', audience: 'https://api.example',
typ: 'JWT', requiredClaims: ['sub', 'iat', 'exp'], clockTolerance: 5
};
const { payload } = await jwtVerify(token, publicKey, options);
assert.equal(payload.sub, 'demo-user');
assert.equal(decodeJwt(token).sub, 'demo-user'); // decoding alone establishes no trust
await assert.rejects(jwtVerify(token, publicKey, { ...options, audience: 'https://other.example' }));
await assert.rejects(jwtVerify(token, publicKey, { ...options, currentDate: new Date(Date.now() + 300000) }));
console.log('Valid token accepted; wrong audience and expired token rejected');
Expected output: Valid token accepted; wrong audience and expired token rejected. Decoding the same token also reads demo-user, but the decoder is not the step that establishes authenticity. An unsigned or tampered token must not become trusted just because its payload is readable.
Define the verifier’s contract
In your application, bind keys to an expected issuer and allowed algorithm set. Require the claims your protocol needs and check the audience. exp is required by this example’s contract; the base JWT specification does not make it mandatory for every possible JWT use. Clock tolerance should fit measured clock behavior, not silently add a long validity window.
For remote JWKS, use your issuer’s documented trusted endpoint with bounded fetching and caching. Do not fetch arbitrary URLs supplied by an untrusted token. Distinguish access tokens, ID tokens and other signed objects: a valid signature on one does not make it suitable for every endpoint.
Choose algorithms for compatibility and trust boundaries
ES256 in this example is a supported choice, not a universal winner. RSA, elliptic-curve and HMAC deployments have different interoperability, key-management and policy constraints. With HS256, a verifier holding the shared secret can also create tokens. Asymmetric verification lets consumers verify without receiving the signing key. Do not choose an algorithm from a speed ratio without a relevant benchmark.
Expiry, revocation and browser storage
Short expiry limits the duration of a stolen access token but does not invalidate it immediately. Immediate revocation needs a checked session/version record, denylist or another protocol-specific mechanism. Revoking a refresh token generally stops renewal, not every access token already issued.
HttpOnly cookies prevent direct JavaScript reading, but cookie authentication still needs CSRF defenses. Memory-only storage avoids a persistent token copy but does not neutralize XSS. The right design depends on the application; neither storage option removes the need to prevent injected scripts.
For inspection, use a synthetic token and decode its segments locally. JSONCraft’s Base64 tool and hash tool do not constitute a JWT signature-and-claims verifier. Use your authentication library and test the rejected cases too.
References: JWT RFC 7519, JWT best current practices, and jose jwtVerify API.
Related tools
Frequently asked questions
Does decoding a JWT verify its signature?
No. Decoding reads claims supplied by the sender; trust requires cryptographic verification and checks required by your application.
Is an in-memory browser token immune to XSS?
No. Code executing in the page can act with the user’s authority and may access or intercept tokens. Storage choice is only one part of the threat model.