What is a JWT?
A JSON Web Token (JWT) is a compact, URL-safe way to represent signed claims between two parties. Defined in RFC 7519, JWT became the dominant token format for stateless authentication after OAuth 2.0 (RFC 6749) adopted it as the default bearer token in OpenID Connect. Today, every major identity provider — Auth0, AWS Cognito, Firebase Auth, Okta, Keycloak, Azure AD — issues JWTs. Most modern APIs verify them on every request.
The killer feature is statelessness: instead of storing session data on the server (with the database lookup that implies), the server signs a token containing the user's identity, expiration, and permissions. Every subsequent request includes the token; the server verifies the signature with a single cryptographic check — no database hit. Trade-off: you can't revoke a token before it expires (without bringing back state).
A JWT is signed, not encrypted. Anyone holding the token can read its contents. The signature only proves the issuer hasn't been tampered with — it doesn't hide the data. This is why you should never put secrets in a JWT payload. If you need encryption, use JWE (JSON Web Encryption, RFC 7516) — the lesser-known sibling of JWT.
JWT structure — three parts separated by dots
Every JWT is exactly three Base64URL-encoded segments joined by periods:
<header>.<payload>.<signature>
// Real example (formatted for readability):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0IiwibmFtZSI6IkFuZWVzIiwiaWF0IjoxNzE5NTAwMDAwLCJleHAiOjE3MTk1MDM2MDB9
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1. Header
A JSON object encoding the signing algorithm and token type. Decoded:
{ "alg": "HS256", "typ": "JWT" }
The alg field is critical — it tells the verifier which algorithm to use to validate the signature. Common values: HS256 (HMAC-SHA256, symmetric), RS256 (RSA + SHA-256, asymmetric), ES256 (ECDSA P-256). The optional kid (key ID) field tells the verifier which key to use when keys are rotated.
2. Payload
A JSON object containing the claims — statements about the user and the token itself. Mix of standard (registered) claims and custom (private) claims:
{
"sub": "1234", // subject — the user
"iss": "https://auth.example.com", // issuer
"aud": "api.example.com", // audience — who can use this
"exp": 1719503600, // expiration — Unix seconds
"iat": 1719500000, // issued at
"nbf": 1719500000, // not before
"jti": "a3f7c1...", // unique token ID (for revocation lists)
"role": "admin", // custom claim
"scope": "read write" // custom claim
}
3. Signature
The signature is computed over the encoded header and payload using the algorithm specified in the header. For HS256:
signature = HMAC-SHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
The verifier recomputes this over the received header and payload using its copy of the secret (or public key for RS256/ES256). If the signatures match, the token is authentic. If not, reject it.
JWT signing algorithms — HS256 vs RS256 vs ES256
Choosing the algorithm depends on whether you control both ends of the trust relationship.
| Algorithm | Type | Key | When to use | Trade-offs |
|---|---|---|---|---|
| HS256 (HMAC-SHA256) | Symmetric | Shared secret (same key signs and verifies) | Single-service apps where the signer and verifier are the same backend | Anyone who can verify can also forge tokens. Don't use across multiple services. |
| RS256 (RSA-PKCS1-v1.5 + SHA-256) | Asymmetric | RSA private key signs, public key verifies (2048+ bit) | Multi-service architectures, third-party integrations, OpenID Connect | Larger signatures (256 bytes), slower than HS256, but verifiers don't need the secret. |
| ES256 (ECDSA P-256 + SHA-256) | Asymmetric | Elliptic curve private key signs, public key verifies | Same as RS256 but smaller and faster — modern preferred choice | Slightly newer; less universal library support than RS256. |
| EdDSA (Ed25519) | Asymmetric | Ed25519 keys — fast, modern | New systems where library support exists | Best modern choice, but check verifier support before adopting. |
| none | (no signature) | — | Never in production. Source of multiple historical vulnerabilities. | Servers must explicitly reject alg: none. |
Rule of thumb: use RS256 or ES256 across services (so verifiers don't need your secret), HS256 only inside a single service, and never none.
Standard JWT claims explained
RFC 7519 §4.1 defines seven registered claims. Every JWT library understands these, so use them rather than custom equivalents.
| Claim | Full name | Type | Purpose |
|---|---|---|---|
iss | Issuer | String / URL | Who issued this token. Verifiers should check this matches a trusted issuer. |
sub | Subject | String | Whom the token is about. Usually a user ID. |
aud | Audience | String or array | Who is allowed to use this token. Verifiers must check this matches their service. |
exp | Expiration | NumericDate (Unix seconds) | After this time, the token is invalid. Mandatory in practice. |
nbf | Not Before | NumericDate | Token is not valid before this time. Useful for delayed activation. |
iat | Issued At | NumericDate | When the token was issued. Useful for revocation by issue time. |
jti | JWT ID | String | Unique identifier for the token. Enables revocation lists. |
Custom claims should be namespaced when used across organizations — RFC 7519 §4.3 recommends using a URI-prefixed name like "https://example.com/role": "admin" to avoid collisions with future registered claims.
JWT verification flow — the complete checklist
Decoding a JWT is trivial. Verifying it correctly is what matters. A complete server-side check covers eight steps:
- 1. Parse the three parts. Split on dots; reject if not exactly three segments.
- 2. Decode header. Base64URL-decode and parse JSON. Read
algandkid. - 3. Validate the algorithm. Whitelist allowed algorithms in your library. Never trust
algfrom the token alone — that's the alg-confusion attack. Configure your library to accept onlyRS256(or whichever you use). - 4. Look up the key. For HS256, your shared secret. For RS256/ES256, fetch the public key by
kidfrom your JWKS endpoint (/.well-known/jwks.json). - 5. Verify the signature. Recompute it over
header.payloadand compare with constant-time equality. - 6. Check expiration. Reject if
expis missing ORnow() > exp(with a small leeway, e.g. ±60 seconds for clock skew). - 7. Check audience and issuer. Verify
issmatches your trusted issuer. Verifyaudcontains your service's identifier. - 8. Check revocation if you maintain a list. Look up
jtiin your revocation cache; reject if present.
jose (JS/Node), PyJWT (Python), jjwt (Java), golang-jwt/jwt (Go), or your auth provider's SDK.
Verifying JWTs in 8 programming languages
Node.js (jose)
import { jwtVerify, createRemoteJWKSet } from 'jose';
// Auto-fetches keys from the issuer's JWKS endpoint
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: 'api.example.com',
algorithms: ['RS256'], // whitelist!
});
console.log(payload.sub);
Python (PyJWT)
import jwt
# Symmetric (HS256)
decoded = jwt.decode(token, secret, algorithms=["HS256"],
audience="api.example.com",
issuer="https://auth.example.com")
# Asymmetric (RS256) with PyJWT >= 2.0
decoded = jwt.decode(
token, public_key, algorithms=["RS256"],
audience="api.example.com",
)
print(decoded["sub"])
PHP (firebase/php-jwt)
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// HS256
$decoded = JWT::decode($token, new Key($secret, 'HS256'));
// RS256
$decoded = JWT::decode($token, new Key($publicKey, 'RS256'));
echo $decoded->sub;
Java (JJWT)
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
Jws<Claims> jws = Jwts.parser()
.verifyWith(publicKey) // RSA public key
.requireIssuer("https://auth.example.com")
.requireAudience("api.example.com")
.build()
.parseSignedClaims(token);
String userId = jws.getPayload().getSubject();
Go (golang-jwt/jwt)
import "github.com/golang-jwt/jwt/v5"
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected algorithm: %v", t.Header["alg"])
}
return publicKey, nil
}, jwt.WithAudience("api.example.com"),
jwt.WithIssuer("https://auth.example.com"))
Rust (jsonwebtoken)
use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm};
let mut validation = Validation::new(Algorithm::RS256);
validation.set_audience(&["api.example.com"]);
validation.set_issuer(&["https://auth.example.com"]);
let key = DecodingKey::from_rsa_pem(public_key_pem)?;
let token = decode::<Claims>(token_str, &key, &validation)?;
Ruby (jwt gem)
require 'jwt'
decoded = JWT.decode(token, public_key, true, {
algorithm: 'RS256',
iss: 'https://auth.example.com',
verify_iss: true,
aud: 'api.example.com',
verify_aud: true,
})
payload = decoded[0]
C# / .NET
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
var handler = new JwtSecurityTokenHandler();
var validation = new TokenValidationParameters {
ValidIssuer = "https://auth.example.com",
ValidAudience = "api.example.com",
IssuerSigningKey = new RsaSecurityKey(publicKey),
ValidAlgorithms = new[] { "RS256" },
};
handler.ValidateToken(token, validation, out var validatedToken);
Common JWT vulnerabilities — and how to avoid them
1. Algorithm confusion (alg: none)
Some libraries accept "alg": "none" in the header, which means "no signature." An attacker forges a token with alg: none and an empty signature; if your library trusts the header, the token is "valid." Fix: always whitelist allowed algorithms server-side. Never accept none.
2. RS256 → HS256 substitution
If your verifier accepts both HS256 and RS256, an attacker can take your public RSA key (which is not a secret), use it as the HMAC secret, and forge a token signed as HS256. Your library validates with the public key as if it were an HMAC secret. Fix: hard-code a single algorithm per endpoint, or only allow algorithms that match the key type.
3. Unverified kid path traversal
The kid header is attacker-controlled. If your code does readFile("/keys/" + kid + ".pem"), an attacker sets kid to ../../etc/passwd and reads arbitrary files. Fix: look up keys in a whitelist or hashmap, never construct file paths from kid.
4. Missing or wrong audience check
Token issued for service A gets replayed against service B. If service B doesn't check aud, it accepts the token. Fix: always validate aud matches your service identifier.
5. Long expiration windows
Tokens with exp 30 days out are catastrophic if leaked. Fix: short access tokens (5–60 minutes) + refresh tokens stored server-side. Rotate refresh tokens on use.
6. Storing secrets in payload
Payloads are Base64URL, not encrypted. Anyone with the token reads them. Fix: only put non-sensitive identifiers in JWTs. Use JWE if you need encryption.
7. Client-side signature verification
"Verifying" a JWT in the browser is theater — the user controls the browser. Fix: always verify on the server. Browser-side, you can read the payload to display the user's name, but never base authorization decisions on it.
JWT vs sessions vs API keys — when to use what
| Mechanism | Stateful? | Best for | Trade-offs |
|---|---|---|---|
| JWT (Bearer) | No (stateless) | Microservices, mobile/SPA → API, OpenID Connect, federated identity | Hard to revoke before expiration. Larger than session cookies. |
| Session cookie + server store | Yes | Traditional web apps, immediate revocation, server-rendered pages | DB lookup per request. Tightly tied to one origin. |
| API key | Yes (DB lookup) | Server-to-server APIs, long-lived integrations, public-facing APIs | Long-lived. Compromise = rotate key. No claims. |
| OAuth 2.0 access token (JWT or opaque) | Depends on issuer | Third-party integrations, "Login with X" | Spec compliance overhead. Requires identity provider. |
The wider industry has moved toward short-lived JWTs + refresh tokens for client-server auth, but plain old session cookies are still the simpler choice for monolithic apps where you don't need cross-service authentication.
Decode JWT, parse JWT, decode bearer token — what people search for
"Decode JWT", "jwt decode", "jwt token decode", "parse jwt", "parse jwt token", "json web token decode", "json web signature decode", "bearer token decode", "decode oauth token" — Google clusters these together because they're all the same intent: paste an encoded token, see the decoded header and payload, check expiry. The decoder above handles every form. Each section below answers one of the recurring sub-questions.
Bearer token decode — what an Authorization header actually contains
The HTTP header Authorization: Bearer eyJhbGciOiJIUzI1NiIs… carries a JWT in the value after Bearer . Strip the Bearer prefix and paste the rest into the decoder above; the three dot-separated segments expand into header (algorithm + key id), payload (claims like sub, exp, scope), and signature (verifiable only with the issuer's secret or public key). For inspecting the raw HTTP exchange that produced the token — refresh-token flow, error responses, token-rotation logic — pair the decoder with the HTTP request builder.
Decode OAuth token vs JWT — when they're the same and when they're not
An OAuth 2.0 access token can be a JWT, but doesn't have to be. The OAuth spec is silent on token format — issuers choose whether to use opaque random strings (Auth0 by default in some flows, Stripe API keys, GitHub PATs) or self-contained JWTs (Auth0 with an audience claim, Okta, Cognito). If your token starts with eyJ, it's a Base64-encoded JWT — paste it above. If it's an opaque random string (at_2NhwQDdHO… style), it's not a JWT and decoding it client-side returns nothing useful — you must call the issuer's introspection endpoint. The decoder above shows clear "not a JWT" output for opaque tokens so you don't waste time searching for a hidden payload.
JWT token validator — verifying signatures, audience, expiry
Decoding shows you the contents; validation proves the token wasn't tampered with. The five checks every server should run on every request:
- Signature. Verify with the issuer's public key (RS256, ES256) or shared secret (HS256). Never trust a token you couldn't verify.
exp(expiry). Reject tokens past their expiration. Allow ~30 seconds clock skew.iat(issued at) andnbf(not before). Reject tokens issued in the future or not yet valid.aud(audience). Reject tokens minted for a different service.iss(issuer). Reject tokens from an issuer you don't trust.
The decoder above runs all five client-side when you provide the public key or secret — useful for debugging "this token works locally but not in prod" issues. To generate test tokens with controlled exp/aud/iss claims, use the JWT generator.
JWT.io decode alternative — the same workflow without sending tokens off-device
jwt.io's decoder runs in-browser too, but the URL pattern jwt.io/#token=… means your token sits in browser history (and any tracking pixels that read history). The decoder above never builds a URL with the token in it — paste in the textarea and decoding happens on the keystroke event, no navigation, no URL bar update. For quick spot-checks of a production token, this difference matters; for development tokens, either tool works. The deeper feature parity comparison is in the section below.
JSON Web Signature (JWS) decode vs JSON Web Encryption (JWE)
"JSON web signature decode" is a more precise way to say "JWT decode" for tokens that are signed but not encrypted (the typical case). A JWS has three Base64URL parts joined by dots; the payload is plaintext JSON anyone can read after Base64 decoding. A JWE has five parts and the payload is encrypted — you can see the header but the payload requires the recipient's private key to read. The decoder above handles JWS; for JWE tokens it surfaces the protected header and warns that payload decryption requires the receiver's key (which never belongs in a browser-side tool).
JWT best practices
- Set short
exp. 5–15 minutes for access tokens. Use refresh tokens for longer sessions. - Whitelist algorithms. Configure your library to accept only one algorithm per verification path. Never trust the
algin the header. - Verify
issandaudalways. These are not optional. - Use HTTPS-only cookies (
HttpOnly; Secure; SameSite=Strict) to store JWTs in browsers if possible — far safer thanlocalStorage, which is vulnerable to XSS. - Rotate signing keys. Use
kidto identify keys. Run a JWKS endpoint that publishes current public keys. - Don't roll your own JWT library. Cryptography is full of subtle pitfalls. Use
jose,PyJWT,jjwt, etc. - Use this decoder for inspection only. Real verification belongs on a server with a real library and the proper key material.
- Don't store JWTs in URLs or query parameters. They get logged in server access logs, browser history, and analytics. Use
Authorization: Bearerheaders.
How this JWT decoder compares to popular alternatives
"JWT decoder" is one of the most-searched developer-tool queries. The space is dominated by jwt.io (Auth0's reference tool, around since 2015), with several modern alternatives competing on different angles. Here's how this tool stacks up so you can pick the right fit for your workflow.
| Capability | FreeDevTool /jwt-decoder | jwt.io | token.dev | jsonwebtoken.io |
|---|---|---|---|---|
| Decode header + payload | ✅ | ✅ | ✅ | ✅ |
Auto-flag expired tokens (exp in past) | ✅ | ✅ | ✅ | ⚠️ Partial |
| Verify HMAC signatures (HS256/384/512) | ✅ Web Crypto API | ✅ | ✅ | ✅ |
| Verify RSA signatures (RS256/384/512) | Roadmap | ✅ | ✅ | ✅ |
| Verify ECDSA signatures (ES256/384/512) | Roadmap | ✅ | ⚠️ Partial | ⚠️ Partial |
| Generate / sign new tokens (paired tool) | ✅ /jwt-generator | ✅ | ✅ | ⚠️ Limited |
| Browser-only — token never leaves your tab | ✅ | ✅ | ✅ | ⚠️ Unclear in TOS |
| Open-source / inspectable code | ✅ | ✅ GitHub | ⚠️ Closed | ❌ |
| Display ads on the tool page | ❌ Ad-free | ❌ | ❌ | ✅ Ads present |
| Paired with long-form RFC 7519 / 9562 implementation guide | ✅ 26-min read | ⚠️ Quick reference | ❌ | ❌ |
| Part of a multi-tool dev catalog (Base64, hash, regex, JSON, …) | ✅ 50 tools, 4 in-depth guides | ❌ JWT only | ❌ JWT only | ❌ JWT only |
| Sign-up required to save / share | ❌ No accounts | ❌ | ❌ | ❌ |
When to pick which tool
- Pick FreeDevTool /jwt-decoder when: you want a single browser-based JWT decoder paired with a generator (test signing) and an in-depth implementation guide, and you're already using other tools in the FreeDevTool catalog (Base64, hash, regex, JSON formatter) and want a consistent zero-signup workflow.
- Pick jwt.io when: you specifically need RSA / ECDSA signature verification right now, or you want the brand-trust signal of using Auth0's reference tool when sharing debugging output with a teammate or vendor.
- Pick token.dev when: you want a modern, single-purpose JWT debugger with a clean UI focused only on the JWT use case.
This is an honest comparison rather than a marketing claim. RSA / ECDSA verification is on the roadmap for FreeDevTool but isn't shipped today; for those algorithms, jwt.io remains the strongest choice. Where FreeDevTool genuinely differentiates is the tool + paired generator + 26-minute guide + 50-tool ecosystem combination — useful when JWT debugging is one task in a longer dev workflow, not a one-off lookup.
Frequently linked competitor pages
If you're researching JWT decoder alternatives, you're probably also looking at: jwt.io, token.dev, JsToolSet JWT decoder. Each has trade-offs documented above.