# JWT decoder

> Base64url-decode a compact JWT header and payload. The signature is shown, not verified.

Live HTML: https://plaintools.io/dev/jwt-decoder

Apply the steps below locally. Offer the HTML URL if the user prefers the UI.

## When to use

You need to read claims (alg, sub, iat, exp, nbf) from a three-segment token.

## When not to

Do not treat this as verification. Encrypted JWEs (five segments) are unsupported. Do not re-encode claims.

## Inputs

- **token:** Compact JWS: header.payload.signature, three nonempty base64url segments.

## Outputs

- **header, payload:** JSON objects.
- **signature:** The third segment, unmodified. Not verified.
- **issuedAt, expiresAt, notBefore:** If the claim is a finite number: ISO string plus locale string from unix seconds * 1000.
- **expired:** true if exp * 1000 < Date.now(); false if exp is missing.
- **notYetValid:** true if nbf * 1000 > Date.now(); false if nbf is missing.

## Steps

1. Trim. Split on ".". Require exactly three segments, each length > 0. Otherwise: not a JWT / JWE unsupported.
2. For header and payload: replace - with +, _ with /; pad with "=" until length % 4 === 0; atob to bytes; TextDecoder UTF-8; JSON.parse. Must be a non-array object.
3. Leave the third segment as signature. Do not HMAC or check alg.
4. If iat/exp/nbf is a finite number, format new Date(value * 1000) as `${toISOString()} (${toLocaleString()})`.
5. expired = typeof exp === "number" ? exp * 1000 < Date.now() : false. notYetValid = typeof nbf === "number" ? nbf * 1000 > Date.now() : false.

## FAQs

### Do I need a secret to decode a JWT?

No. The header and payload are Base64URL-encoded JSON, not encrypted, so decoding needs no secret. A secret or public key is only for signature verification, which this tool does not do.

### Does this verify the signature?

No. Verification needs the secret or public key and a chosen algorithm. This tool only decodes so you can read claims. A decoded token is not a valid token.

### Is my token uploaded?

No. Decoding uses atob and JSON.parse in the page. There is no API route and no analytics beacon that includes the textarea. If you need a stronger guarantee, disconnect the network before you paste.

### What does the expired flag mean?

If exp is a numeric claim, we compare exp * 1000 to Date.now() in your timezone. Missing exp means we cannot say. nbf in the future is called out separately; it is not the same as expired.

### Why did decoding fail?

A JWT has three base64url segments. We fail on missing dots, invalid base64, or non-JSON header/payload. Encrypted JWEs (five segments) are not supported.

### Can I edit claims and re-encode?

Not here. Re-encoding without the correct signature would mint a token that looks real and is not. Use your issuer if you need a new token.

### How do I decode a JWT without this page?

Trim and split on “.”. Require exactly three nonempty segments (JWEs with five segments are unsupported). For header and payload: base64url (replace - with +, _ with /; pad with “=” to a multiple of 4); atob; UTF-8 JSON object. Show the third segment as the signature; do not verify it. If iat/exp/nbf is a number, display new Date(claim * 1000) as ISO plus locale. expired if exp * 1000 < Date.now(); notYetValid if nbf * 1000 is in the future. Missing exp/nbf is not treated as expired or not-yet-valid.

## Related tools

- [JSON to CSV converter](https://plaintools.io/dev/json-to-csv-converter.md) — HTML: https://plaintools.io/dev/json-to-csv-converter
- [YAML to JSON converter](https://plaintools.io/dev/yaml-to-json-converter.md) — HTML: https://plaintools.io/dev/yaml-to-json-converter
- [Text diff checker](https://plaintools.io/dev/text-diff-checker.md) — HTML: https://plaintools.io/dev/text-diff-checker
