JSON Web Token (JWT) is a compact claims format specified by RFC 7519. A JWT may be represented as a signed JWS or an encrypted JWE; Base64URL encoding alone provides neither confidentiality nor authenticity. A valid signature authenticates the issuer's statement, not the caller's authorization to every resource.

📋 Table of Contents

Key Takeaways

  • Structure: A JWT consists of three parts: a header, a payload, and a signature, separated by dots.
  • Claims are inputs to policy: sub, roles, and scopes do not replace tenant, object, or business authorization.
  • Verification is explicit: Pin algorithms and keys, validate issuer/audience/resource, time claims, token type, and required scopes.
  • State is optional, not absent: Local JWT verification can avoid a lookup for some requests, but revocation, sessions, key rotation, and authorization commonly require state.
  • Use cases are conditional: JWTs can fit API authentication and signed assertions, but an opaque token or server session may be simpler.
  • Implementation: Libraries for creating and verifying JWTs are widely available in languages like JavaScript, Python, and Java, simplifying integration.

Never paste a production token into an untrusted decoder. Inspect only synthetic, redacted fixtures and treat decoded claims as untrusted input.

The Anatomy of a JWT

A JWT is composed of three parts, separated by dots (.):

  1. Header: Metadata about the token.
  2. Payload: The claims or data.
  3. Signature: For verifying the token's integrity.

A typical JWT looks like this: xxxxx.yyyyy.zzzzz.

1. Header

The header specifies the token type (JWT) and the signing algorithm used, such as HMAC SHA256 or RSA.

json
{
  "alg": "HS256",
  "typ": "JWT"
}

This JSON is Base64Url encoded to form the first part of the JWT.

2. Payload

The payload contains the claims, which are statements about an entity (typically the user) and additional data. There are three types of claims:

  • Registered Claims: Predefined claims like iss (issuer), exp (expiration time), sub (subject), and aud (audience). These are recommended for interoperability.
  • Public Claims: Custom claims defined by developers, which should be registered in the IANA JSON Web Token Registry to avoid collisions.
  • Private Claims: Custom claims created for specific use cases between parties that have agreed on their meaning.

Example payload:

json
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022
}

The payload is also Base64Url encoded to form the second part of the JWT.

3. Signature

For a JWS, the signature covers the ASCII bytes of base64url(header) + "." + base64url(payload). The verifier chooses an allowed algorithm and trusted key; it must not accept an algorithm merely because the untrusted header names it.

javascript
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  trustedKey
)

The signature can detect changes and, with a trusted key-to-issuer mapping, authenticate the issuer. It does not grant object access or prove that the subject is the human currently using the client.

Code Examples

JavaScript (Node.js)

javascript
const jwt = require('jsonwebtoken');

// Encode (Sign)
const payload = { sub: 'synthetic-user', iss: process.env.JWT_ISSUER };
const privateKey = process.env.JWT_SIGNING_KEY;
const token = jwt.sign(payload, privateKey, {
  algorithm: 'RS256',
  expiresIn: '15m',
  audience: process.env.JWT_AUDIENCE
});

// Decode (Verify)
const decoded = jwt.verify(token, process.env.JWT_VERIFYING_KEY, {
  algorithms: ['RS256'],
  issuer: process.env.JWT_ISSUER,
  audience: process.env.JWT_AUDIENCE
});
// Apply scope, tenant, object, and business authorization after verification.

Python

python
import os
import jwt

# Encode
payload = {'sub': 'synthetic-user', 'iss': os.environ['JWT_ISSUER']}
private_key = os.environ['JWT_SIGNING_KEY']
token = jwt.encode(payload, private_key, algorithm='RS256',
                   headers={'kid': os.environ['JWT_KEY_ID']})

# Decode
decoded = jwt.decode(
    token,
    os.environ['JWT_VERIFYING_KEY'],
    algorithms=['RS256'],
    issuer=os.environ['JWT_ISSUER'],
    audience=os.environ['JWT_AUDIENCE'],
)

Java (JJWT 0.12.x-style API; verify against the version used by your project)

java
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Claims;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;

// Encode
PrivateKey signingKey = loadPrivateKeyFromApprovedKeyStore();
String token = Jwts.builder()
    .setSubject("synthetic-user")
    .setIssuer(System.getenv("JWT_ISSUER"))
    .setAudience(System.getenv("JWT_AUDIENCE"))
    .setIssuedAt(new Date())
    .setExpiration(new Date(System.currentTimeMillis() + 15 * 60 * 1000))
    .signWith(signingKey)
    .compact();

// Decode
PublicKey verificationKey = loadPublicKeyFromApprovedKeyStore();
Claims claims = Jwts.parser()
    .verifyWith(verificationKey)
    .requireIssuer(System.getenv("JWT_ISSUER"))
    .requireAudience(System.getenv("JWT_AUDIENCE"))
    .build()
    .parseSignedClaims(token)
    .getPayload();
// Also enforce an algorithm allowlist, time claims, scopes, and resource authorization.

The Java API changes between JJWT releases. Pin the dependency, read its migration guide, and test rejection of an unexpected algorithm, issuer, audience, and expired token. Do not copy a dependency version from an old article without checking its current security fixes.

JWT Security Best Practices

Validate the complete security context

Signature verification is only one gate. A resource server should select a trusted issuer configuration before parsing the token, then enforce an explicit algorithm allowlist and key-to-issuer mapping. Validate iss, aud (or the resource indicator), exp, nbf when present, iat freshness where required, typ, and kid rotation rules. Treat sub, roles, and scopes as claims to be checked against the requested operation, not as proof that the caller may access an arbitrary object or tenant.

Never use an untrusted kid as a file path, SQL fragment, or arbitrary URL. Resolve it through a bounded key set or a controlled JWKS cache with TLS, issuer pinning, size limits, refresh limits, and a failure policy. Reject duplicate or ambiguous claims according to the library and protocol profile used by the service.

Choose storage and transport deliberately

Use TLS for every hop and avoid putting tokens in URLs, query strings, logs, analytics events, or exception messages. An HttpOnly cookie blocks ordinary JavaScript reads, but it does not stop CSRF. For cookie-authenticated requests, define Secure, an appropriate SameSite policy, Origin/Referer checks where applicable, and a CSRF token or equivalent server-side defense. A browser application that keeps a token in memory has a different refresh and reload experience; a token in Web Storage is readable by injected scripts. The correct choice depends on the threat model, not on a universal “best” location.

Design refresh and revocation as stateful workflows

Access tokens should carry only the claims needed for their short validity window. Refresh tokens should be high-entropy, stored and transmitted under a separate policy, rotated on every successful use, and tracked as a token family. Reuse detection should revoke the affected family and require re-authentication. Record a revocation or session version when logout, credential reset, tenant removal, or incident response must take effect before exp. A JWT is locally verifiable, but the surrounding session and authorization system is not automatically stateless.

Keep data minimised and observable

Do not put passwords, payment data, long-lived secrets, or mutable authorization decisions into a token merely because the payload is convenient. Log only a redacted token identifier, issuer, key id, and decision reason. Monitor verification failures, clock-skew errors, refresh reuse, unexpected issuers, and key-rotation gaps without recording bearer credentials. Use synthetic fixtures in tests and include negative cases for tampering, wrong audience, wrong tenant, expired tokens, missing scopes, and replay.

Frequently Asked Questions about JWTs

1. Where should I store JWTs on the client-side? There is no universal storage answer. An HttpOnly; Secure cookie reduces token theft through direct script reads, but cookie authentication needs CSRF defenses and a carefully chosen SameSite policy. In-memory storage limits persistence but complicates reloads. Web Storage is readable by injected scripts. Choose after modelling XSS, CSRF, device sharing, logout, and recovery requirements.

2. How do I handle JWT expiration and token renewal? Use a short-lived access token and a separately governed refresh workflow when the product needs sessions to survive access-token expiry. Rotate the refresh token on every use, detect reuse, expire the token family, and provide server-side revocation for logout, credential changes, and incident response. Exact lifetimes must be calibrated to the application and risk.

3. What is the difference between JWT, JWS, and JWE?

  • JWT is the standard.
  • JWS (JSON Web Signature) is a signing representation; a JWT can be carried as a JWS.
  • JWE (JSON Web Encryption) is an encryption representation; a JWT can be carried as a JWE.

4. Is JWT an encryption method? No. A standard JWS is signed, not encrypted, so its payload is readable by anyone who obtains it. Use JWE only when the protocol and key-management design require payload confidentiality; it does not replace authorization or transport security.

5. Does a valid JWT signature authorize a request?

No. It authenticates a statement under a trusted issuer and key. The service still has to map the subject to the tenant and requested object, check scopes and business policy, and enforce account status and revocation rules.

6. Can a JWT be revoked?

Not by changing the already-issued bytes. Use short-lived access tokens plus a server-side session or revocation version, and rotate refresh-token families. A blacklist can be appropriate for high-risk or incident-response cases, but it introduces state and operational cost.

Conclusion

JWT is a compact claims format, not an authorization policy and not automatically an encryption scheme. A sound implementation pins algorithms and issuer keys, validates the complete claim context, separates authentication from tenant/object authorization, and treats browser storage, refresh, revocation, and key rotation as explicit design decisions. When those controls create more complexity than the use case needs, an opaque token with server-side session state can be the clearer and safer choice.