Guide · Node.js · REST API

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.

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.

ℹ️The signature proves who issued the token and that its content has not been tampered with. It does not encrypt the payload — anyone with the token can read the claims. Never put sensitive information (passwords, card numbers) in a JWT payload.

2. JWT anatomy — header, payload, signature

A JWT consists of exactly three parts, each base64url-encoded and joined by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsIm5hbWUiOiJBbGljZSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDAzNjAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
HeaderPayloadSignature

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.

ClaimFull nameMeaning
issIssuerWho created and signed the token (e.g. "https://auth.example.com")
subSubjectWho the token is about — typically a user ID
audAudienceWho the token is intended for — your API should validate this
expExpirationUnix timestamp after which the token must not be accepted
nbfNot BeforeUnix timestamp before which the token must not be accepted
iatIssued AtUnix timestamp when the token was issued
jtiJWT IDUnique 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.

⚠️The API parses and validates structure and claims. It does not verify the cryptographic signature — that still requires your secret or the issuer's public key. Never skip signature verification in production auth flows.
All
Algorithms
HS*, RS*, ES*, PS*, EdDSA
<15ms
Response time
pure decode, no crypto
100/day
Free tier
no credit card

Full parameter reference and response schema: JWT API docs →


6. Node.js code example

Using the native fetch API (Node 18+). No dependencies required.

// jwtInspector.js
const API_KEY = process.env.ISVALID_API_KEY;
const BASE_URL = 'https://api.isvalid.dev';

/**
 * Parse and inspect a JWT using the IsValid API.
 * Does NOT verify the signature.
 *
 * @param {string} token - The JWT string (three dot-separated base64url parts)
 * @returns {Promise<object>} Parsed token info
 */
async function inspectJwt(token) {
  const params = new URLSearchParams({ value: token });

  const response = await fetch(`${BASE_URL}/v0/jwt?${params}`, {
    headers: { Authorization: `Bearer ${API_KEY}` },
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    throw new Error(`JWT API error ${response.status}: ${error.message ?? 'unknown'}`);
  }

  return response.json();
}

// ── Example usage ────────────────────────────────────────────────────────────

const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' +
  '.eyJzdWIiOiJ1c2VyXzEyMyIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDAzNjAwfQ' +
  '.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';

const result = await inspectJwt(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();
}
Use 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
}
FieldTypeDescription
validbooleanToken has 3 parts, both header and payload decode to valid JSON with the required fields
algorithmstringSigning algorithm from the header (e.g. HS256, RS256, ES256)
headerobjectDecoded JWT header object
payloadobjectDecoded JWT payload object with all claims
issuedAtstring | nullISO 8601 timestamp from the iat claim, or null if absent
expiresAtstring | nullISO 8601 timestamp from the exp claim, or null if absent
expiredboolean | nullWhether 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

Never accept the "none" algorithm — always specify allowed algorithms
Do not skip audience (aud) validation in multi-service architectures
Do not pass tokens in URL query strings — use Authorization header
Always verify the signature with your secret or the issuer's public key
Issue tokens with an exp claim — non-expiring tokens are a liability
Add clock tolerance to handle minor skew in distributed systems
Log token metadata (sub, exp, alg) — never log the raw token

See also

Inspect JWT tokens instantly

Free tier includes 100 API calls per day. No credit card required. Supports all standard JWT signing algorithms.