Safeguard
AppSec

jwt-decode: Why Decoding Is Not Verifying (Security Guide)

The npm jwt-decode package reads JWT claims without checking the signature. That is by design, and it is behind a whole class of authentication bypasses when developers forget it.

Marcus Chen
DevSecOps Engineer
7 min read

The npm jwt-decode package decodes a JSON Web Token's payload without verifying its signature, so any security decision based on its output alone can be forged by anyone who can type Base64. That single sentence explains most of the real-world incidents involving this library. jwt-decode is not vulnerable; misusing it is. This guide covers what the package actually does, where it is legitimately useful, the misuse patterns that turn it into an authentication bypass, and what to reach for instead on the server.

What jwt-decode actually does

A JWT is three Base64url segments joined by dots: header, payload, signature. jwt-decode splits the string, Base64url-decodes the first two segments, and parses them as JSON. That is the whole library. It performs no cryptography, makes no network calls, and checks no expiry:

import { jwtDecode } from "jwt-decode";

const claims = jwtDecode(token);            // payload
const header = jwtDecode(token, { header: true }); // header

The maintainers (the package lives in the Auth0/Okta GitHub org) state this plainly in the README: the library does not validate the token, and any well-formed JWT can be decoded. Notably, when you search jwt decode npm advisories, the package itself has a clean record — vulnerability databases list no direct CVEs against it. The risk lives entirely in how it gets called.

Why "decoded" tells you nothing about "trustworthy"

Signature verification is the only thing binding a JWT's claims to its issuer. Skip it and the token is just attacker-controlled JSON with delusions of authority. Forging one takes a few lines:

const b64 = (o) => Buffer.from(JSON.stringify(o)).toString("base64url");
const forged = [
  b64({ alg: "HS256", typ: "JWT" }),
  b64({ sub: "admin", role: "admin", exp: 9999999999 }),
  "AAAA", // signature nobody checks
].join(".");

Feed that to jwt-decode and you get back a perfectly parsed role: "admin". The function did its job. The application that treats the result as authenticated identity is the bug.

The misuse patterns that keep showing up

Three patterns account for nearly every finding we see in code review and pentest reports:

Server-side authorization from decoded claims. An Express middleware calls jwtDecode(req.headers.authorization) and checks claims.role. There is no verification step anywhere in the request path. This is a straight authentication bypass, and it is depressingly common in codebases that migrated from a framework that verified tokens implicitly.

Client-checks-only expiry. The frontend decodes exp to decide when to refresh, and someone later reuses that helper on the backend as the expiry check. Expiry read from an unverified token is attacker-chosen.

Trusting the header before verification. Reading alg or kid from the decoded header and using it to pick a verification path is how algorithm-confusion attacks start. The 2015 wave of JWT library flaws documented by Auth0 — accepting alg: none, and RS256/HS256 confusion where a public key is repurposed as an HMAC secret — all begin with trusting unverified header fields. Header values may steer a key lookup, but the chosen key and algorithm must be validated against an allowlist you control.

Related server-side history reinforces the point: the jsonwebtoken library (the verifying sibling in the same ecosystem) shipped fixes in version 9.0.0 for a cluster of 2022 advisories, including CVE-2022-23540, where calling verify() with loose options could let unsigned or weakly-signed tokens through. Even verification libraries need strict configuration; a decode-only library used in their place has no chance.

Where jwt-decode is the right tool

None of this means the package should be banned. Legitimate uses are all display-and-UX, on the client, after the server has already done real verification:

  • Showing the logged-in user's name or avatar from an ID token.
  • Scheduling a token refresh a minute before exp.
  • Routing UI (hide the admin menu for non-admins) — as convenience, not enforcement, because the API must re-check every call.
  • Debugging tokens in development tooling.

The rule of thumb: jwt-decode output may influence what the user sees, never what the user may do.

What to use server-side instead

On the backend, verify first, decode as a side effect of verification:

import { jwtVerify, createRemoteJWKSet } from "jose";

const jwks = createRemoteJWKSet(new URL("https://issuer.example.com/.well-known/jwks.json"));

const { payload } = await jwtVerify(token, jwks, {
  issuer: "https://issuer.example.com/",
  audience: "api://my-service",
  algorithms: ["RS256"],
});

jose (or jsonwebtoken v9+, or your framework's OIDC middleware) checks the signature, algorithm, issuer, audience, and expiry in one call and refuses malformed input. If your identity provider publishes keys via JWKS, pair verification with a caching key resolver — we cover that pattern in our jwks-rsa guide. Pin the algorithm list explicitly; never derive it from the token.

Catching decode-without-verify in your pipeline

This bug class is findable before production:

  • Static review: grep for jwt-decode imports in server-side directories. In most repos, any hit outside src/client or a frontend workspace deserves a look. Semgrep has community rules for jwt.decode-style calls used in auth paths.
  • Dynamic testing: a DAST scanner can replay API requests with a signature-stripped or re-signed token and flag endpoints that still answer 200. Safeguard DAST includes this style of auth-token tampering check, which catches the cases static analysis misses because the verification happens (or doesn't) in a framework layer.
  • Dependency review: knowing where jwt decode npm usage enters your tree — direct or via some auth helper package — tells you where to audit. npm ls jwt-decode plus an SCA inventory covers it.

A useful team convention is to wrap all token handling in one audited module, so "who calls jwt-decode" has exactly one answer and code review can hold the line.

Versions, maintenance, and practical notes

jwt-decode is actively maintained under the Auth0 GitHub organization. Version 4.0.0 (September 2023) moved the package to ESM-first with a named jwtDecode export and shipped its own TypeScript types; the old default-export style from v3 no longer works, which is the most common upgrade stumble. The library throws InvalidTokenError on malformed input — catch it, because user-supplied tokens will be malformed, and an unhandled exception in an auth path is a availability bug of its own. For deeper JWT handling patterns, the token-security modules in Safeguard Academy walk through verification, rotation, and storage end to end.

FAQ

Is the jwt-decode npm package vulnerable?

No known CVEs are recorded against jwt-decode itself. It is a tiny Base64url-and-JSON parser. Security issues attributed to it are misuse: applications treating decoded, unverified claims as authenticated identity.

Can I use jwt-decode to check if a token is expired?

Only for UX, such as scheduling a refresh on the client. An attacker controls every claim in an unverified token, including exp, so server-side expiry enforcement must come from a verifying library like jose or jsonwebtoken.

What is the difference between jwt-decode and jsonwebtoken?

jwt-decode only parses the token payload and header; it does no cryptography. jsonwebtoken (and jose) verify the signature, algorithm, expiry, issuer, and audience, and can also sign new tokens. Server-side authentication must use a verifying library.

Is it safe to decode a JWT in the browser?

Yes. The payload is not encrypted, only encoded, so decoding client-side reveals nothing an attacker could not read anyway. Just avoid putting secrets in JWT claims, and remember the API must independently verify every token it receives.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.