Free tool
JWT decoder
Paste a JSON Web Token to read its header and payload, see when it expires in your own time zone, and verify the signature with a secret, a PEM key, a certificate or a JWKS. Everything runs in your browser.
Free · no sign-up · runs in your browser, nothing is uploaded
The header (algorithm, type, key ID) appears here.
The claims (sub, exp, iat...) appear here, with times in your time zone.
Decoding and verifying run in your browser with the Web Crypto API. Tokens and keys are never sent anywhere or put in the URL.
What is inside a JWT
A signed JWT is three Base64URL strings joined by dots: header.payload.signature. The decoder colors them the same way. The header and payload are JSON; the signature is raw bytes computed over the first two parts, exactly as they appear in the token.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 header {"alg":"HS256","typ":"JWT"}
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6... payload {"sub":"1234567890","name":"John Doe",...}
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQ signature HMAC-SHA256(header + "." + payload, secret)The header says how the token was signed:
| Parameter | Meaning |
|---|---|
| alg | Signing algorithm, such as HS256, RS256 or ES256. "none" means unsigned. |
| typ | Token type, usually JWT. RFC 8725 suggests more specific types like at+jwt for access tokens. |
| kid | Key ID. Picks the right public key from the issuer's JWKS. |
| jku, x5u | URLs of a key set or certificate. Never trust them blindly: an attacker can point them at their own key. |
| x5c | The certificate chain itself, as Base64 DER. |
Base64URL is Base64 with - and _ instead of + and /, and no = padding. To decode one part by hand, use the Base64 decoder, which accepts this alphabet as is.
Registered claims and what they mean
RFC 7519 defines seven registered claims. All are optional; which ones a server requires is up to the application. The time claims are NumericDate values: whole or fractional seconds since 1970-01-01 UTC, not milliseconds. A value with 13 digits is almost always a bug, and the decoder warns about it.
| Claim | Name | What a server does with it |
|---|---|---|
| iss | Issuer | Checks it is the issuer it expects, and that the signing key belongs to that issuer. |
| sub | Subject | Uses it as the user or client ID. |
| aud | Audience | Rejects the token if its own ID is not listed. A string or an array of strings. |
| exp | Expiration time | Rejects the token on or after this time. |
| nbf | Not before | Rejects the token before this time. |
| iat | Issued at | Can reject tokens older than a maximum age. |
| jti | JWT ID | Can remember used IDs to block replays. |
The claims view above also explains common OpenID Connect claims (nonce, azp, auth_time, email_verified) and OAuth ones (scope, client_id). To convert any timestamp by hand, use the Unix timestamp converter.
Decoding is not verifying
Anyone can decode a JWT, and anyone can make a new one with whatever claims they like. Only the signature tells you the token came from the issuer and was not changed. Paste the key in the verify box to check it. Which key you need depends on the algorithm in the header:
| alg | Algorithm | What to paste to verify |
|---|---|---|
| HS256 / HS384 / HS512 | HMAC with SHA-2 | The same shared secret the issuer used |
| RS256 / RS384 / RS512 | RSA PKCS#1 v1.5 | Public key PEM, certificate, JWK or JWKS |
| PS256 / PS384 / PS512 | RSA-PSS | Public key PEM, certificate, JWK or JWKS |
| ES256 / ES384 / ES512 | ECDSA on P-256, P-384, P-521 | Public key PEM, certificate, JWK or JWKS |
| EdDSA | Ed25519 (RFC 8037) | Public key PEM or OKP JWK (needs a recent browser) |
Most identity providers (Auth0, Okta, Google, Microsoft Entra ID, Keycloak, Clerk) publish their public keys at the jwks_uri listed in {issuer}/.well-known/openid-configuration. If the token has an https issuer, the fetch JWKS from iss button loads that key set for you. Only the issuer's public endpoints are contacted, and some issuers block browser requests; then copy the JWKS from the URL yourself.
The decoder also accepts PEM in every common shape: BEGIN PUBLIC KEY, BEGIN RSA PUBLIC KEY (PKCS#1) and BEGIN CERTIFICATE. Need a token to test with? Make one with the JWT generator.
Decode and verify a JWT in code
JavaScript with jose, which runs in Node, Deno, Bun, browsers and edge runtimes:
import { decodeJwt, decodeProtectedHeader, jwtVerify, createRemoteJWKSet } from 'jose';
decodeJwt(token); // payload, NOT verified
decodeProtectedHeader(token); // header
// HS256
await jwtVerify(token, new TextEncoder().encode(secret), { algorithms: ['HS256'] });
// RS256 / ES256 from an identity provider's JWKS
const JWKS = createRemoteJWKSet(new URL('https://auth.example.com/.well-known/jwks.json'));
const { payload } = await jwtVerify(token, JWKS, { issuer: 'https://auth.example.com', audience: 'my-api' });Node.js with jsonwebtoken:
const jwt = require('jsonwebtoken');
jwt.decode(token, { complete: true }); // { header, payload, signature }, NOT verified
jwt.verify(token, secret, { algorithms: ['HS256'] }); // throws TokenExpiredError, JsonWebTokenError...Python with PyJWT:
import jwt
jwt.decode(token, options={"verify_signature": False}) # NOT verified
jwt.decode(token, secret, algorithms=["HS256"], audience="my-api", leeway=30)Go with golang-jwt v5:
claims := jwt.MapClaims{}
_, _, err := jwt.NewParser().ParseUnverified(tokenString, claims) // NOT verified
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (any, error) {
return []byte(secret), nil
}, jwt.WithValidMethods([]string{"HS256"}))Bash, to peek at the payload (adds the padding base64 wants):
p=$(printf %s "$TOKEN" | cut -d. -f2 | tr '_-' '/+')
while [ $(( ${#p} % 4 )) -ne 0 ]; do p="$p="; done
printf %s "$p" | base64 -d | jq .Always pass the list of algorithms you accept when verifying. RFC 8725 asks for this so a token cannot switch itself to none or to HS256 signed with your public key.
Common JWT errors and what they mean
| Error | Library | Cause and fix |
|---|---|---|
| jwt expired | jsonwebtoken | Now is on or after exp. Get a new token, or allow a little clock skew with clockTolerance. |
| ExpiredSignatureError | PyJWT | Same as above; use leeway= for clock skew. |
| jwt not active | jsonwebtoken | Now is before nbf. Usually the issuer's and server's clocks differ. |
| invalid signature | jsonwebtoken | Wrong secret or key, or the token was edited. Check the secret encoding and the kid. |
| jwt malformed | jsonwebtoken | Not three dot-separated parts. Often the "Bearer " prefix or quotes were left in. |
| jwt audience invalid | jsonwebtoken | The aud claim does not include the audience you passed to verify. |
More free developer tools
Looking for a full product instead? See security tools on DevHunt.
FAQ
Can a JWT be decoded without the secret?+
Yes. The header and payload of a signed JWT are only Base64URL-encoded JSON, so anyone who has the token can read them. The secret or key is needed only to verify the signature, which proves the token was not changed. If the payload must stay private, the issuer has to use an encrypted JWE instead.
Is it safe to paste my JWT into an online decoder?+
Only if the site decodes it in your browser. This one does: decoding and verification run in JavaScript with the Web Crypto API, and the token is not sent to DevHunt or put in the URL. Still, a live access token works for anyone who holds it until it expires, so prefer expired or test tokens when you can.
How do I check if a JWT is expired?+
Read the exp claim: it is a NumericDate, the number of seconds since 1970-01-01 UTC. The token is expired when the current time is on or after exp. This decoder shows exp as a date in your time zone and as "expires in 12 minutes" or "expired 3 hours ago". In JavaScript: Date.now() / 1000 >= payload.exp.
What is the difference between decoding and verifying a JWT?+
Decoding turns the Base64URL parts back into JSON so you can read the claims. Verifying recomputes the signature with the secret or public key and checks it matches, which proves who issued the token and that nobody edited it. Servers must verify before trusting any claim; decoding alone proves nothing.
Why does my JWT say "invalid signature"?+
Usually the key is wrong: a different secret, the secret entered as text when it is really Base64, or a public key that does not match the kid in the header. It also fails if any character of the header or payload changed, including whitespace added when the token was copied. For ES256, the signature must be the 64-byte R and S pair, not a DER-encoded signature.
What does the kid in the JWT header mean?+
kid (key ID) tells the receiver which key signed the token. Issuers that rotate keys publish several public keys in a JWKS (JSON Web Key Set), each with its own kid. Paste the whole JWKS into the verify box and this decoder picks the key whose kid matches.
Can you decode a JWE (encrypted JWT)?+
Only the header. A JWE has five parts instead of three, and its payload is encrypted, so it cannot be read without the decryption key. This tool recognises a five-part token and tells you it is a JWE rather than showing an error.
What do iat, nbf and exp mean in a JWT?+
iat (issued at) is when the token was created, nbf (not before) is the time before which it must be rejected, and exp (expiration time) is the time on or after which it must be rejected. All three are seconds since 1970-01-01 UTC. RFC 7519 lets servers allow a few minutes of leeway for clock skew.
Sources: RFC 7519 (JWT) · RFC 7515 (JWS) · RFC 7518 (JWA algorithms) · RFC 8037 (EdDSA in JOSE) · RFC 8725 (JWT best practices) · RFC 7516 (JWE) · OpenID Connect Discovery · jsonwebtoken (Node.js) · PyJWT · jose (JavaScript) · golang-jwt · MDN: SubtleCrypto.verify(). Checked 2026-10-30.