Paste a JSON Web Token to read its header and payload. Decoding is not verification. Anyone holding the token can read what you see here, so type the secret into the secret field when you also need to check the signature. Token and secret both stay in the browser.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c {
"alg": "HS256",
"typ": "JWT"
} {
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
} Read exp, iat, iss and aud to find out whether a token is expired, issued by who you think, and meant for your API. The payload shows exp and iat as Unix seconds; paste one into the Unix timestamp converter to read it as a date.
A token can have a valid signature and still fail because aud or iss does not match what the API expects. Compare those two claims with your server config first.
Type the secret and the tool checks HS256, HS384 or HS512 signatures in your browser. Invalid Signature means the secret is wrong or the token changed after signing.
const { createHmac, timingSafeEqual } = require("node:crypto");
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
// 1. Decode header and payload (no verification, works in the browser too)
const [h, p, s] = token.split(".");
const header = JSON.parse(Buffer.from(h, "base64url").toString());
const payload = JSON.parse(Buffer.from(p, "base64url").toString());
console.log(header, payload);
// 2. Verify an HS256 signature
const expected = createHmac("sha256", "your-256-bit-secret").update(`${h}.${p}`).digest("base64url");
const valid = expected.length === s.length && timingSafeEqual(Buffer.from(expected), Buffer.from(s));
console.log("signature valid:", valid);
// 3. Check expiry
if (payload.exp && payload.exp < Date.now() / 1000) console.log("token expired");
// Or with jsonwebtoken (npm i jsonwebtoken): jwt.verify(token, secret, { algorithms: ["HS256"] }) In the browser replace Buffer with atob() after swapping - and _ back to + and /.
<?php
$token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
function b64url_decode(string $s): string {
return base64_decode(strtr($s, '-_', '+/') . str_repeat('=', (4 - strlen($s) % 4) % 4));
}
[$h, $p, $s] = explode('.', $token);
$header = json_decode(b64url_decode($h), true);
$payload = json_decode(b64url_decode($p), true);
print_r($payload);
// Verify HS256
$expected = hash_hmac('sha256', "$h.$p", 'your-256-bit-secret', true);
$valid = hash_equals($expected, b64url_decode($s));
var_dump($valid); // bool(true)
if (isset($payload['exp']) && $payload['exp'] < time()) {
echo "Token expired\n";
}
// For RS256 and other algorithms use firebase/php-jwt: JWT::decode($token, new Key($secret, 'HS256')) # pip install PyJWT
import jwt
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
# Inspect only, no verification
print(jwt.decode(token, options={"verify_signature": False}))
# Verify signature and standard claims (exp, nbf)
try:
claims = jwt.decode(token, "your-256-bit-secret", algorithms=["HS256"])
print(claims)
except jwt.ExpiredSignatureError:
print("token expired")
except jwt.InvalidSignatureError:
print("wrong secret or tampered token") Always pass algorithms=[...] explicitly. Accepting whatever the header says is how alg=none and key confusion attacks work.
Never let the header tell you which algorithm to use. Early libraries accepted alg: none and skipped verification entirely. Several others would take an RS256 public key and treat it as an HS256 secret, so an attacker could sign tokens with material that was published on purpose. Modern libraries fix both, but only if you pin the algorithm list in your verify call.
Decoding a token tells you nothing about whether to trust it. Anyone can read the payload, so use a decoder like this one for debugging only. Real verification checks the signature with the right key, then exp and nbf against the clock, then iss and aud against what your service expects.
This tool follows the specifications below. Read them when you need the exact normative wording, for example to settle an edge case between two implementations.
Header (algorithm and token type), payload (the claims) and signature, separated by dots. The first two are Base64URL-encoded JSON; the signature proves they were not altered.
Yes, for HS256, HS384 and HS512 tokens. Enter the secret and the tool recomputes the signature and shows Signature Verified or Invalid Signature. RS256 and ES256 tokens need a public key, which this tool does not accept.
Read the exp claim in the payload. It is a Unix timestamp in seconds, so paste it into the Unix timestamp converter, or run new Date(exp * 1000) in a console, to see the date. If that date has passed, the token is expired. iat and nbf use the same format.
Decoding happens in your browser and nothing is transmitted. However, a token works as a credential until it expires, so avoid pasting production tokens on shared computers.
The payload is only Base64url-encoded, not encrypted, so it always decodes. An invalid signature means the secret is wrong or the token was modified after signing.
JWT generator builds test tokens. The signature itself is an HMAC, and each segment is Base64URL, so use those two tools to take a token apart by hand.