JWT Decoder
A JWT decoder splits a JSON Web Token into its header, payload and signature and turns the two encoded parts back into readable JSON, so you can see who issued the token, what it claims and when it expires. Paste a token below, with or without "Bearer ", to inspect it while you debug authentication.
1 Paste a JWT
2 Decoded
Paste a token to see its header.
Paste a token to see its payload.
Registered claims
| Claim | Value | What it means |
|---|
Registered claims (iss, sub, aud, exp, nbf, iat, jti) found in the payload appear here, with exp, nbf and iat converted to UTC and local time.
How to decode a JWT
-
1
Copy the token
Take the JWT from the Authorization header in your browser's network tab, a cookie, local storage or a login response. Copying "Bearer " along with it is fine.
-
2
Paste it into the box
The prefix, quotes, spaces and line breaks are stripped automatically, and the token is split into its color-coded header, payload and signature.
-
3
Check the algorithm and status
Read alg and typ from the header chips, and the status badge for whether the token is valid, expired or not active yet. A red warning appears if alg is "none".
-
4
Read the claims
Use the decoded payload and the claims table to confirm the issuer, subject and audience, and to see exp, nbf and iat as UTC and local times.
-
5
Copy what you need, then verify on the server
Copy the header or payload JSON for a bug report or test. Remember the signature has not been checked: your API must verify it before trusting any claim.
What is inside a JWT
A JSON Web Token, defined in RFC 7519, is three Base64URL strings joined by dots. Here is a complete token with fake data, signed with the demo secret frost-rank-demo-secret so you can verify it with any JWT library:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyXzEwNDIiLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImV4cCI6MTc5MDg0ODgwMCwibmJmIjoxNzkwODQ1MjAwLCJpYXQiOjE3OTA4NDUyMDAsImp0aSI6IjdmM2M5YTFlLWRlbW8iLCJuYW1lIjoiSmFuZSBFeGFtcGxlIiwicm9sZSI6ImVkaXRvciJ9.9Kvkds8sF4oOJCyOI4Qy3BTgGtWfDJecX5g3tX6f15M
- Header says how the token was signed. Decoded, the first part is:
{
"alg": "HS256",
"typ": "JWT"
}
- Payload holds the claims: statements about the user and the token itself. Decoded, the second part is:
{
"iss": "https://auth.example.com",
"sub": "user_1042",
"aud": "https://api.example.com",
"exp": 1790848800,
"nbf": 1790845200,
"iat": 1790845200,
"jti": "7f3c9a1e-demo",
"name": "Jane Example",
"role": "editor"
}
- Signature is an HMAC-SHA256 of the first two parts (exactly as they appear, dot included) computed with the secret. It's 32 raw bytes, which is why it's 43 characters. Change one character of the header or payload and the signature no longer matches.
The timestamps in this token put iat and nbf at 09:00 UTC on 1 October 2026 and exp one hour later, so pasting it into the decoder shows it as expired. The "Load example" button creates a fresh token with the same claims, dated to the moment you click, so you can see a live countdown instead. None of the three parts contains +, / or =, and none ever will: wherever those characters would appear, a JWT uses - and _ and drops the padding. The URL-safe Base64 decoder accepts that alphabet when you need to decode a single part by hand.
Registered claims reference
RFC 7519 reserves seven short claim names. None are mandatory, but when present they have fixed meanings that JWT libraries check for you:
| Claim | Name | What your server should do with it |
|---|---|---|
iss | Issuer | Compare against the exact issuer URL you trust. Reject anything else, even if the signature is valid. |
sub | Subject | Use as the user or client identifier, but only after verification. |
aud | Audience | Check it names this API. It can be a string or an array of strings. |
exp | Expiration time | Reject the token at or after this time, allowing a small clock-skew leeway (30–60 seconds is typical). |
nbf | Not before | Reject the token before this time. |
iat | Issued at | Optional age check, for example refusing tokens issued more than a day ago. |
jti | JWT ID | Store used or revoked IDs if you need one-time tokens or per-token logout. |
All three times are NumericDate values: whole seconds since 1 January 1970 UTC. A value with 13 digits is milliseconds from JavaScript's Date.now(), a common bug that makes tokens look valid for thousands of years; the decoder flags it. Everything else in the payload, like name and role above, is a custom claim your own application defines. To format and validate the JSON payload on its own, for example when building test fixtures, paste the copied payload there.
JWT is encoded, not encrypted
Base64URL is a way of writing bytes as text, not a way of hiding them. Anyone who gets a token, from a log file, a screenshot, a browser extension or a shared HAR file, can read every claim in it without any key. The signature protects integrity, not confidentiality: it stops people from changing the payload, not from reading it. Keep that in mind when designing tokens:
- Never put secrets in the payload. No passwords, API keys, card numbers or personal data you wouldn't print in a log. An opaque user ID in
subis enough. - Always verify the signature server-side, on every request, with a maintained library. Decoding a token and trusting its
roleclaim without verification lets anyone grant themselves admin. - Keep expiry short. A stolen JWT stays usable until
exp, because most servers can't revoke it early. Minutes for access tokens, with a separate refresh token, is the usual pattern. - Pin the algorithm. Tell your library exactly which
algto accept. Never acceptnone, and never let the token's own header choose between an HMAC secret and a public key. - Be careful where you paste tokens. A live production token in a random website is a leaked credential. This decoder runs entirely in your browser and sends nothing anywhere, but the safest token to debug with is still one from a test environment, or one that has already expired.
Common JWT errors and what they mean
Token expired
The current time is past exp. If tokens expire sooner than expected, compare the issuing and verifying servers' clocks; a few minutes of drift is enough. Fix drift with NTP and a small leeway, not by lengthening every token's lifetime.
Invalid signature
The signature doesn't match the header and payload. Usual causes: the API verifies with a different secret or key than the issuer signed with, keys were rotated and the kid in the header points to an old key, the algorithm is pinned to RS256 but the token says HS256, or the token was edited or truncated in transit.
Wrong audience or issuer
The signature is fine, but aud or iss doesn't match what the API is configured to expect. This often happens when a staging token is sent to production, or a token issued for one API is reused for another. Decode the token here and compare both values character by character, including trailing slashes.
Malformed token
The parser couldn't split or decode it. Typical reasons are a leftover Bearer prefix or surrounding quotes, a token cut off by a log line limit, a line break added by an email client, or a five-part encrypted JWE where a three-part JWT was expected. This decoder cleans up the first three cases and names the broken part for the rest.
If the token comes from a cookie or a header, it also helps to check a URL's response and security headers, such as whether an auth cookie is set with Secure and HttpOnly. The HTTP security headers guide explains which ones matter.
Code examples
JavaScript: decode without a library
Fine for reading claims in the browser, for example to show a session countdown. Never use it to make an authorization decision.
function decodeJwtPart(part) {
// Base64URL → Base64: swap the URL-safe characters back and restore the padding.
let base64 = part.replace(/-/g, '+').replace(/_/g, '/');
base64 += '='.repeat((4 - (base64.length % 4)) % 4);
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
return JSON.parse(new TextDecoder().decode(bytes)); // handles non-ASCII names
}
const [header, payload] = token.split('.').slice(0, 2).map(decodeJwtPart);
const expired = payload.exp * 1000 <= Date.now();
PHP: read the payload, then verify properly
Manual decoding is useful for logging or debugging only:
<?php
[$header, $payload] = explode('.', $jwt);
$claims = json_decode(base64_decode(strtr($payload, '-_', '+/')), true);
echo $claims['sub']; // NOT trustworthy yet: nothing has been verified
To actually trust a token, verify it with a library such as firebase/php-jwt (composer require firebase/php-jwt). JWT::decode() checks the signature, exp and nbf, and throws if any check fails. The Key object pins the algorithm:
<?php
use Firebase\JWT\ExpiredException;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\SignatureInvalidException;
JWT::$leeway = 60; // seconds of allowed clock skew
try {
$claims = JWT::decode($jwt, new Key($secret, 'HS256'));
} catch (ExpiredException $e) {
// 401: token expired
} catch (SignatureInvalidException $e) {
// 401: wrong key, or the token was modified
}
// The library doesn't know your issuer or audience: check them yourself.
if ($claims->iss !== 'https://auth.example.com' || $claims->aud !== 'https://api.example.com') {
// 401: token was issued for someone else
}
Laravel: where verification belongs
Verify tokens in one place, before any controller runs: a custom guard or a route middleware that reads $request->bearerToken(), verifies it as above, and sets the authenticated user. Controllers should then rely on auth()->user(), never decode the token themselves. If you use Laravel Passport, its auth:api guard already verifies its own JWT access tokens. Laravel Sanctum's API tokens aren't JWTs at all: they're random strings looked up in the database, so there's nothing to decode.
Frequently asked questions
The decoding runs entirely in your browser. The token is never sent to a server, saved, logged or added to the URL, and the input box is not part of any form. Even so, a JWT is a bearer credential: anyone holding a live one can use it until it expires. Prefer a token from a test environment, or decode a production token only after it has expired or been revoked.
No, on purpose. Verifying needs the signing secret (for HS256) or the issuer's public key (for RS256 and ES256), and a page that asks you to paste a signing secret is teaching a bad habit. Verification belongs in your API, using a maintained JWT library, on every request.
Decoding just reverses the Base64URL encoding so you can read the header and payload; it needs no key and proves nothing. Verifying recomputes the signature with the secret or public key and checks it matches, which proves the token was issued by the expected party and not edited since. A decoded payload is only trustworthy after verification.
They are timestamps in seconds since 1 January 1970 UTC. exp is the expiry time, after which the token must be rejected; nbf (not before) is the time before which it must be rejected; iat (issued at) records when it was created. The claims table above converts each one to a readable UTC and local time and shows whether it has passed.
JWTs use the URL-safe Base64 alphabet, which swaps + for - and / for _ and leaves off the = padding, so a token can travel in a URL, cookie or HTTP header without escaping. That is also why a standard Base64 decoder often rejects a token part until those characters are swapped back.
Yes. The header and payload are only encoded, not encrypted, so anyone can read them without any key. The secret is needed only to create a valid signature or to verify one, which is exactly why a JWT payload must never contain passwords, API keys or other secrets.
A normal signed JWT (JWS), the three-part kind this tool decodes, is not encrypted at all. An encrypted JWT (JWE) exists and has five dot-separated parts instead of three; its payload can only be read with the decryption key. If you paste a five-part token here, the tool tells you it is a JWE.
Built on the same URL-safe Base64 engine and JSON highlighter as Frost Rank's Base64 and JSON tools. The example token on this page is a real HS256 JWT, signed and decoded to produce every value shown, not a hand-assembled string.
Was this tool helpful?
Rate it and leave a comment — takes 10 seconds, and genuinely helps decide what to fix or build next.