Quick answer
A JWT (JSON Web Token) is a compact token made of three Base64url parts, header, payload and signature, joined by dots. The header and payload are only encoded, not encrypted, so anyone can read them; the signature proves who issued the token and that nobody changed it. In Playxoft's JWT decoder, paste a token to see its claims with readable dates and expiry status, then add the secret or public key to verify the signature. Everything runs in your browser.
- Color-coded token parts
- Claims explained in plain English
- Live expiry status
- Verify HS, RS, PS, ES and EdDSA
- PEM, JWK and JWK Set keys
- Encode and sign tokens
- Runs locally with Web Crypto
How to use the JWT Decoder
- 1Paste a token into Encoded token. A
Bearerprefix, quotes and line breaks are ignored, and the three parts are color-coded. - 2Read the decoded Header and Payload, then the Claims table, which explains each claim and shows dates in UTC, local time and relative to now.
- 3Check the badges: Valid, Expired or Not valid yet come from the exp and nbf claims compared with your clock.
- 4To verify, enter the shared secret for HS256/384/512, or paste the public key as PEM, a JWK or a whole JWK Set for RS, PS, ES and Ed25519 tokens.
- 5Switch to Encode and sign to edit the header and payload as JSON and sign a new token with a secret or a freshly generated key pair.
What is a JWT, and is it encrypted?
A JSON Web Token (RFC 7519) is a set of claims, facts such as who the user is and when access ends, packed into a string that is safe to put in a URL or an HTTP header. The common form is a JWS (JSON Web Signature, RFC 7515): three Base64url parts joined by dots.
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.bdUV6fzqg02cO5s8BpihQT_xT_PCKCv21ZGrR8WLDAc
└──── header ──────┘ └──────── payload ────────┘ └─────────────── signature ───────────────┘That token decodes to the header {"alg":"HS256"} and the payload {"sub":"1234567890"}. Paste it into the decoder with the secret playxoft-demo-secret-not-for-prod-01 and the signature verifies; change a single character and it fails.
A JWT is signed, not encrypted. Base64url is an encoding that anyone can reverse, and that is all a decoder does before it checks the signature. The signature only proves that the token came from someone holding the key and that it has not been modified since. So never put passwords, payment details or other secrets in a JWT payload.
If the claims themselves must stay confidential, use an encrypted JWE (RFC 7516). A JWE has five parts instead of three; paste one here and the tool reads its header and tells you which algorithms it uses.
How does JWT signature verification work?
The signature covers the exact characters header.payload: the two Base64url strings and the dot between them. A verifier recomputes it with the key and compares. Change one character of the payload and the check fails, which is why nobody can edit a claim without the key.
A token is only trustworthy after all of these checks pass (RFC 8725, JWT Best Current Practices):
- Algorithm: accept only the algorithm you expect for that key. Never let the token's own
algdecide. - Signature: verify with the issuer's secret or public key, usually fetched from its JWKS endpoint and picked by
kid. - Time: reject the token if now is at or after
exp, or beforenbf, allowing only a small clock-skew leeway. - Issuer and audience:
issmust be the issuer you trust andaudmust name your API. - Your own rules: scopes, roles, the token type in
typ, or a revocation list keyed byjti.
This tool covers the first three. The badges show the validity window by your clock, and the Verify signature panel runs the cryptography with the browser's Web Crypto API, so the key never leaves the page.
HS256 vs RS256 vs ES256: which algorithm should I use?
| Algorithm | Key | Who can verify | Signature | Good fit |
|---|---|---|---|---|
| HS256, HS384, HS512 | One shared secret of at least 32, 48 or 64 bytes | Only holders of the secret, who can also create tokens | 32, 48 or 64 bytes | One service that issues and checks its own tokens |
| RS256, RS384, RS512 | RSA key pair, 2048 bits or more | Anyone with the public key | 256 bytes for a 2048-bit key | The most widely supported choice for OpenID Connect and public APIs |
| PS256, PS384, PS512 | The same RSA key pair | Anyone with the public key | Same as RS | RSA with modern PSS padding, when every consumer supports it |
| ES256, ES384, ES512 | EC key on P-256, P-384 or P-521 | Anyone with the public key | 64, 96 or 132 bytes | Compact tokens for mobile, IoT and high-volume APIs |
| Ed25519 (formerly EdDSA) | Ed25519 key pair | Anyone with the public key | 64 bytes | New systems that control both ends |
Choose an asymmetric algorithm (RS, PS, ES or Ed25519) as soon as a second service needs to verify tokens. With HMAC, every service that can verify a token can also forge one.
RFC 9864 (2025) deprecates the generic EdDSA alg in favor of the fully specified Ed25519. Many libraries still emit EdDSA, and this tool verifies and signs both.
If you do use HMAC, generate the secret randomly. A short, human-chosen secret like secret123 can be cracked offline from a single captured token, because the attacker can test guesses against the signature at full speed without ever touching your server. The Generate secret button in the encoder creates a random secret of the right length.
ECDSA has one interoperability trap: JOSE requires the raw R||S signature (64 bytes for ES256, 96 for ES384, 132 for ES512), while OpenSSL and Java's SHA256withECDSA produce ASN.1 DER by default (.NET's ECDsa.SignData already returns the raw form). This tool detects DER signatures, verifies them anyway and warns you, because strict libraries will reject them.
What do exp, iat, nbf and the other JWT claims mean?
RFC 7519 registers seven claims. All are optional, but most APIs require exp, iss and aud.
| Claim | Name | Meaning |
|---|---|---|
iss | Issuer | Who issued the token, usually the auth server's URL |
sub | Subject | The user or service the token is about |
aud | Audience | Which API the token is for (a string or an array) |
exp | Expiration time | Reject at or after this moment |
nbf | Not before | Reject before this moment |
iat | Issued at | When the token was created |
jti | JWT ID | A unique ID for replay detection and revocation |
The three time claims are NumericDate values: seconds, not milliseconds, since 1970-01-01 UTC. For example 1767225600 is 2026-01-01 00:00:00 UTC. A 13-digit value is almost always the bug of writing Date.now() instead of Math.floor(Date.now() / 1000), and the decoder flags it.
Identity providers add more, such as email, nonce and auth_time from OpenID Connect, or scope, client_id and roles from the JWT access-token profile (RFC 9068). The claims table explains the common ones and marks the rest as custom.
What are the most common JWT security mistakes?
- Accepting `alg: none`. An unsigned token proves nothing. Anyone can write one, so servers must reject it.
- Algorithm confusion. If a server lets the token choose
algand verifies with whatever key it has, an attacker can sign an HS256 token using the server's RSA *public* key as the HMAC secret. Pin one algorithm per key. - Weak HMAC secrets. Short or guessable secrets fall to offline brute force. Use random bytes at least as long as the hash.
- Skipping `aud` and `iss` checks. A token your identity provider issued for a different app would otherwise be accepted by yours.
- Trusting `jku`, `x5u` or an embedded `jwk`. Fetching the verification key from a URL inside the token lets an attacker supply their own key. Use a fixed JWKS URL.
- Long lifetimes without revocation. A stolen JWT works until it expires. Keep access tokens short-lived and rotate them with refresh tokens, or keep a denylist of revoked
jtivalues. - Sensitive data in the payload. Everything in it is readable by anyone who sees the token, including browser extensions and logs.
How do I decode and verify a JWT in code?
Use a maintained library and always pass the expected algorithm, issuer and audience:
// JavaScript / TypeScript (jose)
import { createRemoteJWKSet, jwtVerify } from "jose";
const jwks = createRemoteJWKSet(new URL("https://auth.example.com/.well-known/jwks.json"));
const { payload } = await jwtVerify(token, jwks, {
algorithms: ["RS256"],
issuer: "https://auth.example.com",
audience: "https://api.example.com",
});# Python (PyJWT)
import jwt
claims = jwt.decode(token, public_key_pem, algorithms=["RS256"],
audience="https://api.example.com", issuer="https://auth.example.com")// Go (github.com/golang-jwt/jwt/v5)
token, err := jwt.Parse(raw, func(t *jwt.Token) (any, error) { return rsaPublicKey, nil },
jwt.WithValidMethods([]string{"RS256"}),
jwt.WithIssuer("https://auth.example.com"),
jwt.WithAudience("https://api.example.com"))// C# (Microsoft.IdentityModel.JsonWebTokens)
var result = await new JsonWebTokenHandler().ValidateTokenAsync(token, new TokenValidationParameters {
ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256 },
ValidIssuer = "https://auth.example.com",
ValidAudience = "https://api.example.com",
IssuerSigningKey = new RsaSecurityKey(rsa),
});
if (!result.IsValid) { /* reject */ }Decoding without verifying, as jose's decodeJwt or PyJWT's verify_signature: False option do, is fine for debugging or for showing a user's name in a browser app. Never make an access decision on a token you have only decoded.
Frequently asked questions
Last updated by the Playxoft engineering team.