# jwt-decode: Decoding JWTs in the Browser Without Verifying Them

> jwt-decode is a small TypeScript library from Auth0 that turns a Base64Url JSON Web Token into a plain object. It does not check signatures, and the README says so in bold.

**auth0/jwt-decode** — Decode JWT tokens; useful for browser applications.

- Repository: https://github.com/auth0/jwt-decode
- Stars: 3,399 · Forks: 344
- Language: TypeScript
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/auth0-jwt-decode

## What jwt-decode solves, and who actually needs it

A JWT is three Base64Url-encoded segments joined by dots. Reading the middle one by hand means splitting on a dot, fixing up the Base64 padding, running it through atob(), and parsing the result as JSON. jwt-decode packages that sequence into one function call and gives it a typed return value.

The audience is narrow and the README is explicit about it: the package description calls it "mostly useful for browser applications." The typical caller is a single-page app that has just received an access token from an identity provider and wants to show a username, check an exp timestamp before firing a request, or read a kid from the header to pick a key. Server-side code that already has a JWT middleware has little reason to reach for this.

The important boundary is stated at the top of the README in bold: "This library doesn't validate the token, any well-formed JWT can be decoded." That is not a footnote. It defines the tool. Anyone who wants a library that rejects tampered tokens is looking at the wrong package, and the README points them at express-jwt, koa-jwt and Microsoft.AspNetCore.Authentication.JwtBearer instead.

## How the decode path works inside the library

The token format the README documents is [part1].[part2].[part3]. By default jwtDecode reads part 2, the payload. Pass { header: true } as the second argument and it reads part 1, the JOSE header, which is where typ, alg and kid live.

Each part goes through Base64Url decoding and then JSON.parse. The library's own error messages map onto those two steps, which tells you the order of operations: it first checks that the input is a string, then that the expected part exists after splitting on dots, then that the part decodes as Base64, then that the decoded bytes are valid JSON. A failure at any stage throws an InvalidTokenError carrying one of four documented messages, for example "Invalid token specified: missing part #" or "Invalid token specified: invalid json for part #".

The decoded value is a plain object, not a class instance. Nothing is cached and no state is kept between calls, so decoding the same token twice produces two independent objects. The package is published as "type": "module" with a dual exports map: an ESM build under build/esm and a CommonJS build under build/cjs, each with its own type declarations. That is why the same import specifier works from a bundler and from require().

## Installing jwt-decode and decoding your first token

Installation is a single npm or Yarn command. The README gives both forms; npm is shown here.

```bash
npm install jwt-decode
```

After installation, import the named export jwtDecode and pass it a token string. The README's example uses a placeholder token and prints the payload object.

```js
import { jwtDecode } from "jwt-decode";

const token = "eyJ0eXAiO.../// jwt token";
const decoded = jwtDecode(token);

console.log(decoded);
```

The README says this prints an object shaped like { foo: "bar", exp: 1393286893, iat: 1393268893 }. The claim names come from the token itself, so your output will differ; what matters is that you get an object back rather than a string.

To read the header instead, pass the options object. The README notes this is useful when you need kid to verify a JWT.

```js
const decodedHeader = jwtDecode(token, { header: true });
console.log(decodedHeader);
```

In TypeScript the return type follows the header option: JwtPayload when it is omitted or false, JwtHeader when it is true. The README shows the explicit type argument form, jwtDecode<JwtPayload>(token), and states that both JwtHeader and JwtPayload can be extended to cover non-standard claims. For a CommonJS consumer the README gives const { jwtDecode } = require('jwt-decode'). There is also a script-tag route: copy jwt-decode.js out of the build/esm folder in the repository and import jwtDecode from it inside a script element marked type="module".

## The atob() dependency and the environments it breaks

jwt-decode does not ship its own Base64 decoder. It calls the global atob(), which the README describes as available on all modern browsers and every supported Node environment, with a link to the MDN compatibility table.

That assumption fails in React Native before version 0.74, which the README names directly, citing a Hermes issue. If your runtime has no atob(), you have to supply one. The README offers two routes. The first is core-js:

```js
import "core-js/stable/atob";
```

The second is to polyfill the global yourself with the base-64 package.

```js
import { decode } from "base-64";
global.atob = decode;
```

Both snippets come from the README. Notice what this means for bundle size: the library itself is thin, but the polyfill you bolt on to make it run may not be. On a modern browser target you pay nothing extra. On an older or non-browser runtime you are adding a dependency that exists only to satisfy one function call.

There is a second, quieter failure mode. Passing a falsy or malformed token throws InvalidTokenError rather than returning null or undefined. Code that assumes a decode always succeeds will surface an uncaught exception at the point where it tries to read a claim. Wrap the call if the token comes from storage, a query parameter, or anywhere a user can influence it.

## jwt-decode versus verifying the token

The natural comparison is not another decoder but a verifier. express-jwt, koa-jwt and Microsoft.AspNetCore.Authentication.JwtBearer all appear in the README as the place where validation belongs. The difference is not one of quality but of what the code has access to.

Signature verification needs the signing key or the public key behind the kid in the header, plus the algorithm allowlist and the clock. A browser application typically has none of those, and shipping a shared secret to the client would defeat the point. So the split the README describes is structural: decode on the client to read claims for display or routing, verify on the server before trusting anything.

If you genuinely need to verify in JavaScript, jwt-decode is the wrong package and no amount of wrapping changes that. The README's own wording is that you "should validate the token in your server-side logic" using one of the middleware libraries it lists. A decoder that starts rejecting tokens would be a different library with a different API, and the four InvalidTokenError messages are all about format, never about signatures.

One practical consequence: a decoded payload is attacker-controlled input until a server has verified it. Rendering a name claim is fine. Using a role claim to decide which API routes to call is a client-side convenience, not access control.

## Version 4, maintenance signals, and what the licence permits

The current release line is 4.0.0, published on 2023-10-27, following betas in August and September of 2023. The repository's last push was on 2026-09-01, so the codebase has seen activity since the 4.0.0 tag even though no newer release appears in the list. The repository is not archived.

The 4.0.0 major bump is the one upgrade decision that matters for existing code. The package is now "type": "module" and exposes separate ESM and CommonJS builds through the exports map, with build/cjs/package.json marking that subtree as commonjs. An older codebase that imported the default export rather than the named jwtDecode export will need to change its import statements. The README documents the named export everywhere, in the ESM, CommonJS and script-tag examples.

Licensing is MIT, stated in the README badge, the package.json "license" field and the LICENSE file at the repository root. MIT is permissive: it allows commercial and closed-source use, requires the copyright notice and licence text to be preserved, and disclaims warranty. Nothing in the repository imposes a field-of-use restriction or a copyleft obligation on your application. This is a description of the licence text, not legal advice; if your organisation has a policy on third-party dependencies, run it through that.

The maintenance cost of this dependency is close to zero in practice. It has no runtime dependencies listed in package.json, and its surface is one exported function. The real upgrade risk is the atob() global, which is an environment property rather than a versioned API.

## Conclusion

Adopt jwt-decode when a browser or edge client needs to read claims such as exp, iat or a custom role field out of a token it already received, and when signature checking happens elsewhere. Do not adopt it as a security control: the README states plainly that any well-formed JWT can be decoded. Before wiring it in, confirm that your runtime exposes atob(), because React Native before 0.74 and similar environments need a polyfill, and confirm that your server-side middleware actually rejects tokens with a bad signature, since jwt-decode will not do it for you.

## FAQ

### How do I decode a JWT token with jwt-decode?

Import the named export and pass the token string: jwtDecode(token) returns the payload as an object. Pass { header: true } as the second argument to get the JOSE header instead, which is where kid and alg live.

### Is jwt-decode safe to use?

For reading claims, yes, but it performs no validation. The README states in bold that the library does not validate the token and that any well-formed JWT can be decoded, so signature checking has to happen in your server-side logic.

### Can I decode a JWT without the secret key?

Yes. The payload and header are Base64Url-encoded JSON, not encrypted, and jwt-decode reads them without any key. That is exactly why the decoded contents cannot be trusted until a server has verified the signature.

### How do I install jwt-decode?

Run npm install jwt-decode or yarn add jwt-decode. The package ships an ESM build and a CommonJS build, so both import and require work from the same specifier.

### What does jwt-decode return?

A plain object parsed from the decoded JSON. The README's example prints an object with a custom claim plus exp and iat, and the return type is JwtPayload by default or JwtHeader when the header option is true.

### How is jwt-decode different from verifying a token?

jwt-decode only converts Base64Url JSON into an object and throws InvalidTokenError on malformed input. Verification, which the README assigns to middleware such as express-jwt or koa-jwt, checks the signature and is what makes a claim trustworthy.

## Sources

- [auth0/jwt-decode on GitHub](https://github.com/auth0/jwt-decode)
- [Issues](https://github.com/auth0/jwt-decode/issues)
- [License: MIT](https://github.com/auth0/jwt-decode/blob/main/LICENSE)
- [README](https://github.com/auth0/jwt-decode/blob/main/README.md)
- [Releases](https://github.com/auth0/jwt-decode/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/auth0-jwt-decode
