mrkeyoor.com_
Thu 06 Aug 07:41 UTC
npmSecurityupdated 06 Aug 2026

jwt-decode

jwt-decode is a tiny function from Auth0 that splits a JSON Web Token on its dots, base64url-decodes one of the parts, and hands you back the JSON inside. By default it decodes part two, the payload, where claims like sub, exp, and your app's custom claims live. Pass { header: true } and it decodes part one instead, which is how you read the kid before fetching a signing key. That is the entire library. It does not check the signature, it does not check whether the token expired, and it does not talk to your auth server. The README says this in bold at the very top because people keep getting it wrong: any well-formed JWT decodes fine, including one an attacker typed by hand.

Verdict

For reading claims out of a token you already trust, in a browser, this is the right size of tool and costs you nothing. The moment a decision depends on the token being genuine, throw it out and verify with jose.

API stability5/5One exported function with two overloads, unchanged since 4.0.0 in October 2023. The only breaking change in recent memory was dropping the default export and the atob polyfill in v4.
Docs4/5The README is short and covers everything the library does, including every error message it can throw and the React Native polyfill. There is no separate docs site, but for a single function there does not need to be.
Maintenance3/5Auth0 owns it and the repo saw a push in August 2026 with only 2 open issues (16 issues and PRs total), but there has been no new release since October 2023. Read that as finished rather than abandoned, and plan accordingly.
Ecosystem4/5About 18.6M weekly downloads and it is the default answer in most React and Angular auth tutorials. It has no plugin surface, so ecosystem here means everyone already has it in their lockfile.

Use it if

  • You are in browser or React Native code that already received a token from a trusted backend and you want to read exp, sub, or a custom claim to render UI, like showing a username or hiding an admin tab
  • You need the kid or alg out of the token header so you can pick the right key from a JWKS endpoint before verifying elsewhere
  • You want a scheduled silent refresh: read exp, subtract a safety margin, and set a timer to call your refresh endpoint before the access token dies
  • You care about bundle size in a client app; the whole package gzips to well under a kilobyte with zero dependencies, versus the much larger crypto-carrying verification libraries
Skip it if

Setup reality

npm install jwt-decode, then import { jwtDecode } and you are done. Two things trip people up. First, v4 removed the default export, so the widely copied `import jwtDecode from "jwt-decode"` snippet from old blog posts now yields undefined at call time; you must use the named import. Second, the library calls the global atob(), which v4 stopped polyfilling. Every current browser and Node 18 or newer has it, but React Native before 0.74 does not, so you import core-js/stable/atob or assign global.atob yourself before the first decode. There is no CJS/ESM drama otherwise, the exports map covers both, and TypeScript types ship in the package.

Patterns

Read the claims out of a tokendecode-payload

import { jwtDecode } from "jwt-decode";

const decoded = jwtDecode(accessToken);
console.log(decoded.sub, decoded.exp);

Named import only. v4 dropped the default export, so `import jwtDecode from "jwt-decode"` compiles and then blows up with "jwtDecode is not a function" at runtime.

Get the kid and alg from the headerdecode-header

import { jwtDecode } from "jwt-decode";

const header = jwtDecode(token, { header: true });
console.log(header.kid, header.alg);
// { typ: "JWT", alg: "RS256", kid: "NkJC..." }

This is the legitimate server-side use: read kid, fetch the matching key from JWKS, then verify with a real library. Never trust header.alg to choose your verification algorithm.

Type the payload with your own claimstyped-custom-claims

import { jwtDecode, type JwtPayload } from "jwt-decode";

interface AppClaims extends JwtPayload {
  "https://myapp.com/roles": string[];
  email: string;
}

const claims = jwtDecode<AppClaims>(token);
const roles = claims["https://myapp.com/roles"];

The generic is a cast, not a check. If the token lacks the claim you get undefined at runtime while TypeScript still believes it is a string array.

Catch malformed tokenshandle-invalid-token

import { jwtDecode, InvalidTokenError } from "jwt-decode";

try {
  return jwtDecode(raw);
} catch (err) {
  if (err instanceof InvalidTokenError) {
    // "missing part #2", "invalid base64 for part #2", etc.
    return null;
  }
  throw err;
}

It throws for a non-string input, a missing dot, undecodable base64, or non-JSON content. An empty string or null token throws too, so guard before calling on values from localStorage.

Test whether the token is past expcheck-expiry

import { jwtDecode } from "jwt-decode";

function isExpired(token, skewSeconds = 30) {
  const { exp } = jwtDecode(token);
  if (!exp) return false; // no exp claim means no expiry
  return exp * 1000 <= Date.now() + skewSeconds * 1000;
}

exp is seconds since the epoch, Date.now() is milliseconds. Forgetting the factor of 1000 is the single most common bug with this library. This is a UI hint only; the server still has to reject the expired token.

Refresh shortly before the token diesschedule-silent-refresh

import { jwtDecode } from "jwt-decode";

function scheduleRefresh(token, refresh) {
  const { exp } = jwtDecode(token);
  const msLeft = exp * 1000 - Date.now() - 60_000; // 60s early
  return setTimeout(refresh, Math.max(msLeft, 0));
}

Browser timers do not fire on schedule in background tabs and laptops that slept. Re-check expiry on every request too instead of trusting the timer alone.

Use it from CommonJScommonjs-require

const { jwtDecode } = require("jwt-decode");

const decoded = jwtDecode(token);

The package.json exports map ships both builds, so require works without a bundler shim. Destructure the named export here as well.

Make it work on older React Nativepolyfill-atob

// entry point, before any decode happens
import "core-js/stable/atob";

// or roll your own
import { decode } from "base-64";
global.atob = decode;

Needed on React Native before 0.74, where Hermes had no atob. Without it the first jwtDecode call throws a ReferenceError that looks nothing like a JWT problem.

Pull scopes out of a space-delimited claimread-namespaced-claims

import { jwtDecode } from "jwt-decode";

const { scope = "" } = jwtDecode(accessToken);
const scopes = scope.split(" ").filter(Boolean);

if (scopes.includes("read:reports")) showReportsTab();

OAuth puts scope in a single space-separated string, not an array. Use this to decide what to render, never to decide what the API returns.

What to do instead on the serververify-server-side

// WRONG: trusts whatever the client sent
// const { sub } = jwtDecode(bearerToken);

// RIGHT: verify the signature first
import { createRemoteJWKSet, jwtVerify } from "jose";

const jwks = createRemoteJWKSet(new URL("https://issuer.example.com/.well-known/jwks.json"));
const { payload } = await jwtVerify(bearerToken, jwks, {
  issuer: "https://issuer.example.com/",
  audience: "my-api",
});

Anyone can craft a token whose payload says admin: true, and jwtDecode will happily return it. jose checks the signature, issuer, audience, and expiry before you see the claims.

Load it in a plain HTML pagescript-tag-esm

<script type="module">
  import { jwtDecode } from "/vendor/jwt-decode.js";
  console.log(jwtDecode(token));
</script>

v4 dropped the UMD bundle, so there is no global window.jwt_decode anymore. Copy build/esm/index.js out of the package or point at an ESM CDN.

Alternatives

PackageRegistryPick it when
josenpmYou need actual verification: signature checks, JWKS fetching, JWE, and it works in Node, browsers, and edge runtimes on Web Crypto.
jsonwebtokennpmNode-only server code that signs and verifies tokens with a shared secret or PEM key and wants the long-established API.
fast-jwtnpmHigh-throughput Node services where verification cost per request matters and you want caching built into the verifier.