JWT Validation in Node.js — Structure, Claims, and Expiry Pitfalls
JWTs are everywhere in modern auth — and misunderstood almost as often. Here's what every part of a token means, which claims matter for security, and how to inspect tokens without a secret key.
In this guide
1. What is a JWT?
A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe way to represent claims between two parties, defined by RFC 7519. JWTs are widely used as access tokens in OAuth 2.0 and OpenID Connect flows, as session tokens in stateless APIs, and for information exchange between microservices.
A JWT looks like three base64url-encoded strings separated by dots. Unlike opaque tokens (random strings your server looks up in a database), JWTs are self-contained — the claims are embedded in the token and can be read by anyone who holds it.
2. JWT anatomy — header, payload, signature
A JWT consists of exactly three parts, each base64url-encoded and joined by dots:
Header
Describes the token type and the signing algorithm used.
{ "alg": "HS256", // Signing algorithm: HS256, RS256, ES256, etc. "typ": "JWT" // Token type (always "JWT") }
Payload
Contains the claims — statements about the subject (user) and metadata about the token.
{ "sub": "user_123", // Subject: who the token is about "name": "Alice", // Custom claim "iat": 1700000000, // Issued at (Unix timestamp) "exp": 1700003600 // Expires at (Unix timestamp) }
Signature
Created by signing base64url(header) + "." + base64url(payload) with the secret (HMAC) or private key (RSA/EC). Verifying the signature requires the secret or public key — the IsValid API parses the structure and checks expiry without verifying the signature (useful for debugging).
3. Standard claims — what they mean
RFC 7519 defines a set of registered claim names. You don't have to use them all, but using the standard names makes your tokens interoperable with existing libraries and frameworks.
| Claim | Full name | Meaning |
|---|---|---|
| iss | Issuer | Who created and signed the token (e.g. "https://auth.example.com") |
| sub | Subject | Who the token is about — typically a user ID |
| aud | Audience | Who the token is intended for — your API should validate this |
| exp | Expiration | Unix timestamp after which the token must not be accepted |
| nbf | Not Before | Unix timestamp before which the token must not be accepted |
| iat | Issued At | Unix timestamp when the token was issued |
| jti | JWT ID | Unique identifier for this token — useful for revocation |
4. Common JWT mistakes in Node.js
Accepting the "none" algorithm
JWT libraries that accept any algorithm allow an attacker to craft a token with "alg": "none" and no signature, bypassing authentication entirely. Always specify the allowed algorithms explicitly.
// ❌ NEVER do this — accepts "none" algorithm jwt.verify(token, secret); // ✅ Restrict to your expected algorithm jwt.verify(token, secret, { algorithms: ['HS256'] });
Not checking the audience (aud)
A token issued for your user-facing API should not be accepted by your admin API. Always verify that aud matches your service's expected value.
// ✅ Validate audience jwt.verify(token, secret, { algorithms: ['RS256'], audience: 'https://api.myapp.com', });
Clock skew causing valid tokens to be rejected
Servers in distributed systems can have clocks slightly out of sync. A token issued at iat: now on one server may arrive at a second server where now is a few seconds in the past, causing the token to be rejected as "not yet valid" (nbf check). Add a small clock tolerance.
// ✅ Allow 30 seconds of clock skew jwt.verify(token, secret, { clockTolerance: 30 });
Trusting the header for key selection
If your library uses the kid (key ID) from the header to look up the public key, an attacker can craft a token with any kid to trigger unexpected behaviour. Validate the kid against a known-good allowlist from your JWKS endpoint.
5. Parsing and inspecting tokens with the API
The IsValid JWT API parses the structure, decodes the header and payload, and returns the algorithm, issued-at, expires-at, and whether the token is currently expired — all without needing your secret or private key.
This is useful for debugging tokens in development, logging token metadata on the server side, or inspecting third-party tokens from identity providers (e.g. Google, Auth0, Cognito) to check their structure before forwarding them.
Full parameter reference and response schema: JWT API docs →
6. Node.js code example
Using the @isvalid-dev/sdk package or the native fetch API (Node 18+).
// jwtInspector.js import { createClient } from '@isvalid-dev/sdk'; const iv = createClient({ apiKey: process.env.ISVALID_API_KEY }); // ── Example usage ──────────────────────────────────────────────────────────── const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' + '.eyJzdWIiOiJ1c2VyXzEyMyIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDAzNjAwfQ' + '.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; const result = await iv.jwt(token); if (!result.valid) { console.log('Malformed JWT — not a valid token structure'); } else { console.log('Algorithm:', result.algorithm); console.log('Subject:', result.payload.sub); console.log('Issued at:', result.issuedAt); console.log('Expires at:', result.expiresAt); console.log('Expired:', result.expired); }
In a middleware you might log token metadata without re-decoding:
// middleware/logTokenMeta.js export async function logTokenMeta(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader?.startsWith('Bearer ')) return next(); const token = authHeader.slice(7); try { const info = await inspectJwt(token); if (info.valid) { // Log non-sensitive metadata — do NOT log the token itself req.log.info({ tokenSub: info.payload.sub, tokenAlg: info.algorithm, tokenExp: info.expiresAt, tokenExpired: info.expired, }, 'incoming request token'); } } catch { // Parsing failed — don't block the request, let auth middleware handle it } next(); }
result.expired for quick expiry dashboards or debugging without adding a crypto library to your tooling scripts. For production auth, always use a proper JWT library with signature verification.7. cURL example
Inspect a JWT (use --data-urlencode to avoid encoding issues with the dots and base64 characters):
curl -G -H "Authorization: Bearer YOUR_API_KEY" \ --data-urlencode "value=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDAzNjAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" \ "https://api.isvalid.dev/v0/jwt"
Test with a malformed token (not three parts):
curl -G -H "Authorization: Bearer YOUR_API_KEY" \ --data-urlencode "value=notavalidjwt" \ "https://api.isvalid.dev/v0/jwt"
8. Understanding the response
Valid, non-expired token:
{ "valid": true, "algorithm": "HS256", "header": { "alg": "HS256", "typ": "JWT" }, "payload": { "sub": "user_123", "name": "Alice", "iat": 1700000000, "exp": 1700003600 }, "issuedAt": "2023-11-14T22:13:20.000Z", "expiresAt": "2023-11-14T23:13:20.000Z", "expired": false }
Expired token (structure still valid):
{ "valid": true, "algorithm": "HS256", "header": { "alg": "HS256", "typ": "JWT" }, "payload": { "sub": "user_123", "iat": 1600000000, "exp": 1600003600 }, "issuedAt": "2020-09-13T12:26:40.000Z", "expiresAt": "2020-09-13T13:26:40.000Z", "expired": true }
| Field | Type | Description |
|---|---|---|
| valid | boolean | Token has 3 parts, both header and payload decode to valid JSON with the required fields |
| algorithm | string | Signing algorithm from the header (e.g. HS256, RS256, ES256) |
| header | object | Decoded JWT header object |
| payload | object | Decoded JWT payload object with all claims |
| issuedAt | string | null | ISO 8601 timestamp from the iat claim, or null if absent |
| expiresAt | string | null | ISO 8601 timestamp from the exp claim, or null if absent |
| expired | boolean | null | Whether the token has passed its exp timestamp, or null if no exp claim |
9. Edge cases and security notes
Tokens without an expiry claim
If the token has no exp claim, expired is null and expiresAt is null. Non-expiring tokens are a security risk — a stolen token is valid forever. Always issue tokens with an appropriate exp.
if (result.expired === null) { // Token has no expiry — treat with caution console.warn('Token has no exp claim — consider rejecting non-expiring tokens'); }
Do not send sensitive tokens to third-party services
For tokens that carry sensitive user data in their payload, avoid sending the full token to any external API — including this one. In development and debugging scenarios you can scrub the payload before logging, or use the inspectJwt helper only with test tokens. In production, decode tokens locally using jsonwebtoken or jose.
JWE (encrypted JWTs)
JWE tokens look like 5-part strings (not 3). The payload is encrypted, not just base64-encoded. The API will return valid: false for JWE tokens since they have a different structure from JWS (signed) tokens. If you work with JWE, decrypt it first using your key before inspecting the claims.
Token in query string — never
Passing a JWT as a URL query parameter causes it to appear in server access logs, browser history, and Referer headers. Always pass tokens in the Authorization: Bearer header or in a POST body. The value parameter of the IsValid API is an inspection utility — not an auth pattern to replicate.
Summary
Node.js integration notes
JWT token validation in Node.js sits at one of the most common entry points in any web application: user-facing forms, API request bodies, and webhook payloads. Using a branded TypeScript type — type JwtToken = string & { readonly __brand: 'JwtToken' } — ensures that only values that have passed through the IsValid check can flow into downstream logic. The TypeScript compiler then enforces this boundary without any runtime overhead beyond the initial validation call.
The Node.js ecosystem offers several complementary packages for working with JWT token values once they are validated. For email, pair with nodemailer or @sendgrid/mail; for phone numbers, use libphonenumber-js for formatting; for URLs and domains, the built-in URL class handles parsing after the validity check passes. In each case, validation with IsValid acts as the gate that ensures the downstream library receives well-formed input.
Express.js and Fastify middleware
Add JWT token validation as a route-level middleware in Express or a preHandler hook in Fastify. The middleware validates the incoming value, attaches the result to req.validated, and calls next() on success or returns a 400 response on failure. This keeps validation logic out of route handlers and makes it easy to apply the same check across multiple routes. For high-traffic endpoints, cache previously validated values in a Map or Redis with a short TTL.
In a Next.js API route or App Router server action, call the IsValid API inside a try/catch block. Distinguish between a 422 response (the input is invalid — return this error to the user) and network or 5xx errors (transient failures — retry once, then return a generic service-unavailable response). Never swallow validation errors silently, as they indicate bad data that could propagate further into your system.
- Normalise JWT token values before validation: trim whitespace and convert to lowercase where the format is case-insensitive
- Use
Promise.allSettled()for bulk validation — it captures all results without short-circuiting on the first failure - In Jest tests, mock the IsValid client at the module level to keep tests fast and offline-capable
- Store the validated value alongside the full API response in your database — normalised forms and parsed fields save work in downstream queries
When making HTTP calls to the IsValid API directly (without the SDK), the choice between fetch and axios is largely a matter of preference. The native fetch API is available in Node.js 18+ without any additional dependency and is sufficient for simple request/response flows. axios adds automatic JSON parsing, request/response interceptors, and a cleaner timeout API (axios.create({ timeout: 5000 })), which makes it easier to centralise the Authorization header and retry logic in one place. For high-throughput services that make many concurrent API calls, consider undici — the HTTP client underlying Node.js fetch — used directly for its connection pooling and lower overhead.
See also
Inspect JWT tokens instantly
Free tier includes 100 API calls per day. No credit card required. Supports all standard JWT signing algorithms.