JWT Tokens Explained: Structure, Security & Decoding

Understand JSON Web Tokens from the inside out. Learn the header, payload, and signature structure, common claims, signing algorithms, and security best practices.

JSONTech TeamFebruary 1, 202510 min read

What Is a JWT?

A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe string that represents claims between two parties. It is the most common way to handle authentication and authorization in modern web applications.

When you log into a web app and the server gives you back a token instead of creating a server-side session, chances are that token is a JWT. Your browser sends it with every subsequent request, and the server verifies the signature without hitting a database.

Where JWTs Are Used

  • Authentication. After a user logs in, the server issues a JWT. The client stores it and sends it in the Authorization header on every request.
  • Single Sign-On (SSO). JWTs let users authenticate once and access multiple services. Identity providers like Auth0, Okta, and Keycloak issue JWTs that downstream services can verify independently.
  • API authorization. Microservices pass JWTs between each other to prove that a request was made on behalf of an authenticated user with specific permissions.
  • Information exchange. Because JWTs are signed, they can carry trusted data between parties without an additional verification step.

The Three Parts of a JWT

Every JWT consists of three Base64URL-encoded parts separated by dots:

header.payload.signature

Here is a real JWT (shortened for readability):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Let us decode each part.

Part 1: Header

The header describes the token type and the signing algorithm. Decoded from Base64URL:

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

alg tells the verifier which algorithm to use when checking the signature. typ confirms this is a JWT (as opposed to a JWE or other token type).

Part 2: Payload (Claims)

The payload contains the claims — the actual data the token carries. Decoded:

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

Claims are just key-value pairs. Some are standardized (called "registered claims"), and you can add any custom claims you need.

The payload is Base64URL-encoded, not encrypted. Anyone with the token can decode and read the payload. Never put secrets, passwords, or sensitive personal data in a JWT payload.

Part 3: Signature

The signature is computed by taking the encoded header, a dot, the encoded payload, and signing it with a secret key:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

The signature guarantees that the token has not been tampered with. If anyone changes a single character in the header or payload, the signature will not match, and the verifier will reject the token.

Try it yourself: Paste any JWT into our JWT Decoder to inspect all three parts instantly.

Common JWT Claims

The JWT specification (RFC 7519) defines a set of registered claims. None are mandatory, but they are widely adopted:

ClaimFull NameDescription
subSubjectWho the token is about (usually a user ID)
issIssuerWho issued the token (e.g., your auth server URL)
audAudienceIntended recipient (e.g., your API URL)
expExpiration TimeUnix timestamp after which the token is invalid
iatIssued AtUnix timestamp when the token was created
nbfNot BeforeToken is invalid before this unix timestamp
jtiJWT IDUnique identifier to prevent token reuse

Custom claims are anything else you add. Common examples include role, permissions, email, and org_id. Keep custom claims minimal — every extra byte gets sent with every request.

Signing Algorithms Compared

The algorithm choice determines how the signature is created and verified. Here are the three most common options:

AlgorithmTypeKeyBest For
HS256Symmetric (HMAC)Single shared secretSimple setups where issuer and verifier are the same service
RS256Asymmetric (RSA)Private key signs, public key verifiesDistributed systems, SSO — verifiers never need the private key
ES256Asymmetric (ECDSA)Private key signs, public key verifiesSame as RS256 but with smaller keys and faster operations

Symmetric (HS256) is simpler to set up: both the issuer and the verifier share the same secret. The downside is that every service that needs to verify tokens must have access to the secret, which widens the attack surface.

Asymmetric (RS256, ES256) is better for distributed architectures. The auth server signs tokens with a private key, and any service can verify them using the corresponding public key — which is safe to distribute. ES256 is the modern choice: equivalent security to RS256 with smaller tokens and faster verification.

Security Best Practices

JWTs are a powerful tool, but they are easy to misuse. Follow these rules:

  • Never store secrets in the payload. The payload is encoded, not encrypted. Anyone can decode it. If you need encrypted tokens, use JWE (JSON Web Encryption) instead.
  • Use short expiration times. Access tokens should expire in 5–15 minutes. Use refresh tokens for longer sessions. Short-lived tokens limit the damage if one is stolen.
  • Always use HTTPS. JWTs sent over HTTP can be intercepted in transit. HTTPS is non-negotiable.
  • Validate all claims. Always check exp, iss, and aud. Do not just verify the signature — an expired or misrouted token should be rejected.
  • Do not accept alg: none. Some libraries accept unsigned tokens if the header says "alg": "none". Explicitly reject this algorithm in your verifier configuration.
  • Use an allowlist for algorithms. Configure your verifier to accept only the specific algorithm(s) you expect (e.g., only RS256). This prevents algorithm confusion attacks.
  • Store tokens securely. HttpOnly cookies are more secure than localStorage because they are not accessible via JavaScript (mitigating XSS attacks). If you use localStorage, understand the trade-offs.

Common JWT Mistakes

These are patterns that show up in security audits repeatedly:

  • Putting the JWT in the URL. Query strings end up in server logs, browser history, and referrer headers. Use the Authorization header or a cookie instead.
  • Not validating the signature at all. Some developers decode the payload with a Base64 library and skip verification. This means an attacker can forge any token.
  • Using a weak secret for HS256. If your shared secret is "secret" or "password123", it can be brute-forced in seconds. Use a cryptographically random string of at least 256 bits.
  • Tokens that never expire. If a token has no exp claim, it is valid forever. If that token leaks, you have no way to revoke it without changing the signing key (which invalidates all tokens).
  • Storing too much data. JWTs are sent with every HTTP request. A 4KB token adds overhead to every API call. Keep the payload lean.

Token Refresh Flow

Short-lived access tokens expire quickly, so you need a way to get new ones without forcing the user to log in again. This is the refresh token flow:

  1. The user logs in. The server returns an access token (short-lived, e.g., 15 minutes) and a refresh token (long-lived, e.g., 7 days).
  2. The client sends the access token with every API request in the Authorization: Bearer header.
  3. When the access token expires, the API returns a 401 Unauthorized response.
  4. The client sends the refresh token to a dedicated /refresh endpoint.
  5. The server validates the refresh token, issues a new access token (and optionally a new refresh token — this is called refresh token rotation), and returns them.
  6. The client retries the original request with the new access token.

Refresh token rotation is a security best practice: each refresh token can only be used once. If an attacker steals a refresh token and the legitimate user also tries to use it, the server detects the reuse and invalidates the entire session.

// Simplified refresh flow (client-side)
const handleApiRequest = async (url, options) => {
  let response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      Authorization: `Bearer ${getAccessToken()}`,
    },
  });

  if (response.status === 401) {
    const refreshResponse = await fetch("/api/refresh", {
      method: "POST",
      body: JSON.stringify({ refresh_token: getRefreshToken() }),
    });

    if (refreshResponse.ok) {
      const { access_token, refresh_token } = await refreshResponse.json();
      saveTokens(access_token, refresh_token);

      response = await fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          Authorization: `Bearer ${access_token}`,
        },
      });
    } else {
      redirectToLogin();
    }
  }

  return response;
};

JWTs vs. Sessions: When to Use Which

This is one of the most debated topics in web development. Both approaches have legitimate use cases:

AspectJWTServer-Side Sessions
State storageClient (stateless server)Server (session store / database)
ScalabilityEasy — no shared state between serversRequires shared session store (Redis, DB)
RevocationHard — requires a blocklist or short expiryEasy — delete the session from the store
Payload sizeLarger (carries claims in every request)Smaller (just a session ID cookie)
Cross-domainWorks well (sent as Bearer token)Requires CORS cookie configuration
MicroservicesIdeal — each service verifies independentlyEach service must query the session store
SimplicityMore moving parts (signing, refresh flow)Simpler to implement correctly

Use JWTs when you need stateless authentication across distributed services, mobile apps, or third-party API consumers.

Use sessionswhen you need easy revocation (e.g., "log out all devices"), are running a single monolithic app, or want the simplest secure option for a traditional web app.

Try it yourself: Decode and inspect any JWT instantly with our JWT Decoder — no data leaves your browser.

Related Tools