Blog
Web Security••11 min read

Decoding JWTs in the Browser: Structural Mechanics, Verification, and Token Storage Security

A comprehensive guide to JSON Web Tokens (RFC 7519). Learn how header and payload byte structures work, why client-side decoding differs from cryptographic signature verification, and how to safely handle session tokens.

JSON Web Tokens (JWT, defined in RFC 7519) have become the de-facto standard for stateless authentication and cross-service identity assertion across microservices and single-page applications (SPAs).

Yet, despite their ubiquity, frontend and backend developers frequently conflate decoding a JWT with cryptographically verifying a JWT. Furthermore, developers routinely paste sensitive production tokens into third-party online decoders, inadvertently leaking authorization bearer headers and active tenant permissions to unknown remote server logs.

In this guide, we dive deep into the binary and textual architecture of JWTs, explore the security mechanics of client-side inspection, and analyze the trade-offs of modern browser token storage.


1. Deconstructing the Three-Part Anatomy of a JWT

A compact JWT is a URL-safe string composed of three distinct sections separated by dot (.) delimiters:

$$\text{JWT} = \text{Header} ,., \text{Payload} ,., \text{Signature}$$

Each part serves an essential, decoupled role in identity transport.

1.1 The Header (Algorithm & Token Type)

The header is a JSON dictionary containing metadata about the cryptographic mechanism used to sign the token:

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "auth0-key-2026-q3"
}
  • alg (Algorithm): The cryptographic algorithm, such as symmetric HMAC (HS256, HS384, HS512) or asymmetric RSA / ECDSA (RS256, ES256, EdDSA).
  • typ (Type): Explicitly identifies the object as a JWT.
  • kid (Key ID): A hint indicating which public key in a JSON Web Key Set (JWKS) should be used to verify the signature.

1.2 The Payload (Registered & Custom Claims)

The payload contains the claims—statements about an entity (typically the user) and additional operational metadata:

{
  "iss": "https://auth.company.internal",
  "sub": "usr_998471203",
  "aud": "https://api.company.internal/v2",
  "exp": 1729849200,
  "nbf": 1729845600,
  "iat": 1729845600,
  "roles": ["developer", "admin"],
  "tenant_id": "org_enterprise_alpha"
}

Standard registered claims include:

  • iss (Issuer): Identifies the authorization server that issued the token.
  • sub (Subject): The principal user identifier.
  • aud (Audience): The recipient resource server for which the token is intended.
  • exp (Expiration Time): Unix epoch timestamp indicating when the token ceases to be valid.
  • nbf (Not Before): Timestamp before which the token must not be accepted.

1.3 The Signature (Cryptographic Integrity Proof)

The signature is produced by calculating a cryptographic hash or digital signature over the concatenated Base64URL-encoded header and payload using a secret key or private asymmetric key:

$$\text{Signature} = \text{HMAC-SHA256}(\text{Base64Url}(\text{Header}) + “.” + \text{Base64Url}(\text{Payload}), \text{SecretKey})$$

If a malicious actor alters a single byte in the payload (for example, elevating their role from "viewer" to "admin"), the resulting signature will no longer match the token’s signature, causing the receiving API gateway to reject the request with HTTP 401 Unauthorized.


2. Why Base64URL Encoding is Not Encryption

A common misconception among newer developers is assuming that a JWT protects the confidentiality of its contents.

Critical Security Axiom: A standard JWT is encoded and signed, but NOT encrypted.

Base64URL encoding is a bidirectional serialization algorithm designed exclusively to allow binary data to pass safely through HTTP headers and URLs without escaping issues. Anyone in possession of the token can read every claim inside the payload within milliseconds.

Pasting a JWT into an untrusted third-party website exposes:
- Active session identifiers
- Internal tenant IDs and database primary keys
- User email addresses and role assignments
- OAuth scopes and internal service endpoints

For this reason, never place unencrypted sensitive credentials—such as plain text passwords, API secret keys, credit card numbers, or personally identifiable information (PII)—inside a standard JWT payload. If confidentiality is required over the wire, JSON Web Encryption (JWE, RFC 7516) must be employed.


3. Client-Side Decoding vs. Server-Side Verification

Understanding the boundary between client and server responsibilities is critical when handling JWTs in modern SPAs (React, Vue, Astro, Svelte):

Responsibility Client-Side (SPA / Frontend) Server-Side (API Gateway / Backend)
Decode Payload ✅ Yes (To read UI state, user name, role) ✅ Yes
Check Expiration (exp) ✅ Advisory check (triggers silent refresh) 🔒 Mandatory strict enforcement
Verify Signature ❌ Never (Secret / Private keys cannot be exposed) 🔒 Mandatory cryptographic verification
Authorise Database Actions ❌ Never trust client claims 🔒 Enforce authorization after signature check

Safe Native Browser Decoding (Zero External Dependencies)

To decode a JWT client-side without heavy external libraries or server roundtrips, you can parse the second dot-separated slice using native browser APIs:

interface DecodedToken<T = Record<string, unknown>> {
  header: Record<string, unknown>;
  payload: T & { exp?: number; iat?: number };
}

function decodeJwt<T = Record<string, unknown>>(token: string): DecodedToken<T> {
  const parts = token.split('.');
  if (parts.length !== 3) {
    throw new Error('Invalid JWT: Token must consist of 3 dot-separated parts.');
  }

  // Base64Url to Base64 transformation:
  // Replace '-' with '+', '_' with '/', and add necessary '=' padding
  const base64UrlDecode = (str: string): string => {
    let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
    while (base64.length % 4) {
      base64 += '=';
    }
    // Safely decode UTF-8 JSON payloads
    const binaryString = atob(base64);
    const bytes = Uint8Array.from(binaryString, (char) => char.charCodeAt(0));
    return new TextDecoder().decode(bytes);
  };

  const header = JSON.parse(base64UrlDecode(parts[0]));
  const payload = JSON.parse(base64UrlDecode(parts[1]));

  return { header, payload };
}

4. The Token Storage Dilemma: localStorage vs. HttpOnly Cookies

When architecting browser authentication, engineering teams must evaluate the trade-offs between Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF):

Approach A: localStorage / sessionStorage

  • Advantage: Immune to traditional CSRF attacks; easily accessible in SPA JavaScript for immediate Authorization headers (Bearer <token>).
  • Vulnerability: Vulnerable to XSS. If an attacker injects malicious JavaScript via an unsanitized input or compromised third-party npm package, a single call to localStorage.getItem('token') can immediately exfiltrate the token to an external command-and-control server.
  • Advantage: 100% immune to JavaScript reading. Even if an XSS vulnerability exists on your domain, the attacker’s script cannot access document.cookie or exfiltrate the token directly.
  • Mitigation: Prevent CSRF by using the modern SameSite=Lax or SameSite=Strict cookie attribute, combined with custom headers (e.g. X-Requested-With) or anti-CSRF double-submit tokens for mutating HTTP methods (POST, PUT, DELETE).

5. Security Checklist for JWT Implementations

  1. Enforce alg Whitelisting on Backend: Ensure your authentication middleware explicitly rejects tokens with "alg": "none" or unexpected algorithm conversions (e.g. an attacker supplying an HMAC token signed with a public RSA key).
  2. Short-Lived Access Tokens: Keep access token lifespans short (5 to 15 minutes). Use long-lived, rotatable refresh tokens stored exclusively in HttpOnly cookies.
  3. Inspect Tokens Locally: Never paste live production authorization tokens into online services that transmit data to remote servers.

Need to debug a token safely? Use the CodeKivo Client-Side JWT Decoder. It runs 100% inside your browser memory, evaluates expiration deadlines, validates RFC claim compliance, and ensures zero network egress.

Tags:#JWT#Security#Authentication#OAuth2#Cryptography#REST API#Best Practices
← Back to all engineering guides

More Technical Guides

Continue exploring our architecture and systems engineering library.