The Unencrypted Truth: Why Your JWT is an Open Book and How to Fix It + Video

Listen to this Post

Featured Image

Introduction

JSON Web Tokens (JWTs) have become the de facto standard for authentication and authorization in modern web applications, yet a dangerous misconception persists: that they are encrypted. In reality, JWTs are only Base64Url-encoded—a reversible encoding mechanism that offers zero confidentiality protection. This means anyone with access to a JWT can decode its payload in seconds using tools like jwt.io, exposing potentially sensitive user roles, email addresses, and other claims in plain text. Understanding this fundamental distinction between encoding and encryption is crucial for security professionals, developers, and architects building robust identity management systems.

Learning Objectives & Secrets

  • Objective 1: Understand the critical difference between Base64Url encoding and encryption, and identify which JWT algorithms provide integrity versus confidentiality.
  • Objective 2 Secret Tip: Implement algorithm whitelisting on the backend to prevent attackers from downgrading your JWT to the insecure `alg: none` variant—a common vector for privilege escalation attacks.
  • Objective 3 Secret Tip: Deploy PKCE (Proof Key for Code Exchange) in single-page applications to protect against authorization code interception, even when using HTTPS.

You Should Know

  1. JWT Structure Decoded: The Anatomy of a Token

A JWT consists of three Base64Url-encoded parts separated by dots: Header, Payload, and Signature. The header contains the algorithm used, the payload holds the claims (user data, roles, expiration), and the signature verifies integrity. Because Base64Url is an encoding scheme, decoding is trivial and requires no secret key.

What This Reveals:

When you paste a JWT into jwt.io, the payload appears instantly—email addresses, user roles, tenant IDs, and any custom claims are fully visible. Attackers with access to a token (via network sniffing, XSS, or browser DevTools) can read all this data without breaking any cryptography.

How to Test and Verify:

  • Linux/macOS: Use the `base64` command to decode JWT segments:
    echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" | cut -d"." -f2 | base64 -d 2>/dev/null || echo "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ==" | base64 -d
    
  • Windows PowerShell: Decode the payload using:
    
    

Critical Rule: Never store passwords, API keys, social security numbers, credit card data, or any sensitive Personally Identifiable Information (PII) in the JWT payload. Treat all JWT claims as public information.

2. The `alg: none` Attack: Algorithm Downgrade Exploitation

The JWT specification allows the `alg` header to be set to none, indicating that no digital signature or MAC is applied. If the backend does not explicitly whitelist acceptable algorithms, an attacker can modify the header to {"alg":"none"}, remove the signature, and trivially forge tokens with arbitrary payloads—gaining instant administrative privileges.

Step-by-Step Exploitation:

  1. Capture a legitimate JWT from an authenticated session.
  2. Decode the header and change `”alg”:”HS256″` to "alg":"none".
  3. Modify the payload to escalate privileges (e.g., change `”role”:”user”` to "role":"admin").
  4. Re-encode the header and payload, and drop the signature segment.
  5. Submit the forged token to the backend—if `alg: none` is accepted, your request succeeds.

Mitigation Configuration:

  • Python (Flask/PyJWT):
    import jwt
    Whitelist only secure algorithms
    jwt.decode(token, secret, algorithms=['HS256', 'RS256'])
    
  • Node.js (Express/jsonwebtoken):
    const jwt = require('jsonwebtoken');
    jwt.verify(token, secret, { algorithms: ['HS256', 'RS256'] });
    
  • Java (Spring Security):
    JwtParser parser = Jwts.parserBuilder()
    .setSigningKey(key)
    .setAllowedClockSkewSeconds(60)
    .build();
    // The parser automatically validates the algorithm
    

Verification Command: Use `jq` on Linux to inspect the header without decoding:

echo "YOUR_JWT" | cut -d"." -f1 | base64 -d | jq '.alg'
  1. HS256 vs. RS256: Algorithm Selection and Architecture Impact

The choice between HS256 (HMAC with SHA-256) and RS256 (RSA with SHA-256) determines your key management strategy and security posture.

  • HS256: Uses a single shared secret for both signing and verification. Ideal for monolithic applications where a single backend server handles all token operations. However, any service with access to the secret can both issue and validate tokens—making secret rotation and distribution challenging in microservices.

  • RS256: Uses a private key for signing and a public key for verification. This decoupled model is perfect for microservices architectures: only the authentication service holds the private key, while all other services verify tokens using the public key. This reduces the attack surface and simplifies key rotation.

Implementation Checklist:

  • For HS256, store the secret in a secure vault (e.g., HashiCorp Vault, AWS Secrets Manager) and rotate it regularly.
  • For RS256, generate RSA key pairs:
    openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
    openssl rsa -in private.pem -pubout -out public.pem
    
  • In Node.js, load keys and sign:
    const privateKey = fs.readFileSync('private.pem');
    const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
    
  • In Python:
    private_key = open('private.pem').read()
    token = jwt.encode(payload, private_key, algorithm='RS256')
    

4. PKCE: Protecting SPAs from Authorization Code Interception

Single-page applications (React, Vue, Angular) using OAuth 2.0 are vulnerable to authorization code interception attacks, especially when the callback URL is exposed in the browser. PKCE (Proof Key for Code Exchange) mitigates this by generating a per-request cryptographically random challenge.

How PKCE Works:

  1. The SPA creates a `code_verifier` (random high-entropy string).
  2. It generates a `code_challenge` by hashing the verifier (using SHA-256).

3. The authorization request includes `code_challenge` and `code_challenge_method=S256`.

  1. When exchanging the authorization code for tokens, the SPA sends the original code_verifier.
  2. The authorization server verifies the verifier matches the stored challenge—ensuring only the legitimate app can exchange the code.

Implementation in React:

import { generateCodeVerifier, generateCodeChallenge } from 'pkce-challenge';

// Generate PKCE verifier and challenge
const { codeVerifier, codeChallenge } = generateCodeVerifier();

// Store codeVerifier in sessionStorage (ephemeral, not LocalStorage)
sessionStorage.setItem('codeVerifier', codeVerifier);

// Include in authorization URL
const authUrl = <code>${authorizationEndpoint}?client_id=${clientId}&redirect_uri=${redirectUri}&code_challenge=${codeChallenge}&code_challenge_method=S256</code>;

Pro Tip: Always use HttpOnly, Secure, SameSite=Strict cookies for token storage in SPAs. LocalStorage is vulnerable to XSS—any injected script can read tokens and exfiltrate them.

5. Secure Token Storage: Cookies vs. LocalStorage

The storage location for your JWT directly impacts your application’s security posture. LocalStorage persists across sessions and is accessible to any JavaScript running on the same origin—a single XSS vulnerability can lead to token theft. HttpOnly cookies, however, are inaccessible to JavaScript and automatically included in HTTP requests, providing defense-in-depth against XSS.

Security Comparison:

| Storage Method | XSS Risk | CSRF Risk | JavaScript Access |

|-|-|–|-|

| LocalStorage | High (token accessible) | Low (not auto-sent) | Yes |
| SessionStorage | High (token accessible) | Low (not auto-sent) | Yes |
| HttpOnly Cookie | Low (JS inaccessible) | High (auto-sent) | No |

Recommended Configuration:

// Express.js setting secure cookie
res.cookie('jwt', token, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict',
maxAge: 3600000 // 1 hour
});

Additional Hardening:

  • Implement short-lived access tokens (5–15 minutes) paired with refresh tokens.
  • Store refresh tokens securely and rotate them on each use.
  • Use the `__Host-` prefix for cookies to enforce host-only binding.
  1. Hands-On Lab: Building a Secure JWT Authentication Flow

Step 1: Generate Secure Keys (Linux)

 Generate a 256-bit secret for HS256
openssl rand -base64 32

Generate RSA key pair for RS256
openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -in private.pem -pubout -out public.pem

Step 2: Implement Token Issuance (Node.js)

const jwt = require('jsonwebtoken');
const crypto = require('crypto');

function issueToken(user) {
const payload = {
sub: user.id,
email: user.email,
role: user.role,
jti: crypto.randomBytes(16).toString('hex'), // Unique token ID
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 900 // 15 minutes
};

return jwt.sign(payload, privateKey, { algorithm: 'RS256' });
}

Step 3: Validate and Decode Tokens (Python)

import jwt
from jwt.exceptions import InvalidTokenError

def verify_token(token):
try:
public_key = open('public.pem').read()
payload = jwt.decode(token, public_key, algorithms=['RS256'])
return payload
except InvalidTokenError as e:
 Log the error and reject the request
return None

Step 4: Implement Refresh Token Rotation

 Store refresh tokens with a unique ID in a secure database
refresh_tokens = {}  In production, use Redis or a database

def refresh_access(refresh_token):
if refresh_token not in refresh_tokens:
raise InvalidTokenError("Invalid refresh token")
 Invalidate the old refresh token
del refresh_tokens[bash]
 Issue a new access token and refresh token
new_access = issue_token(user)
new_refresh = generate_refresh_token()
refresh_tokens[bash] = user.id
return new_access, new_refresh

What Undercode Say:

  • Key Takeaway 1: JWTs are encoded, not encrypted—treat all payload claims as public information and never store sensitive data within them. Use separate secure channels for transmitting confidential information.
  • Key Takeaway 2: Always whitelist allowed algorithms on the backend to prevent `alg: none` downgrade attacks, and choose between HS256 and RS256 based on your architecture—microservices demand RS256 for secure key distribution.

Analysis:

The widespread adoption of JWTs has created a false sense of security among developers who mistakenly believe the token is inherently protected. The reality is that JWT’s primary value lies in integrity verification via signatures, not confidentiality. This misunderstanding leads to dangerous practices like embedding API keys, user passwords, or session identifiers in the payload—data that becomes immediately accessible to anyone with token access. The industry is moving toward more secure patterns: short-lived tokens with refresh rotation, PKCE for public clients, and strict storage policies that favor HttpOnly cookies over LocalStorage. As API security matures, we’re seeing increased adoption of token binding (sender-constrained tokens) and mutual TLS to further reduce token theft risks. The shift toward zero-trust architectures will likely accelerate these trends, with continuous authentication and fine-grained authorization becoming the norm. For practitioners, mastering JWT security basics is not optional—it’s foundational to building resilient, production-grade identity systems.

Prediction:

  • +1: Organizations will increasingly adopt JWT best practices as part of compliance frameworks (SOC2, ISO 27001), leading to a measurable reduction in authentication-related breaches.
  • -1: The proliferation of AI-assisted coding tools may inadvertently generate insecure JWT implementations, as developers rely on auto-completion without understanding underlying security implications.
  • +1: PKCE will become mandatory for all public OAuth clients by 2027, driven by both security standards bodies and major identity providers like Google and Microsoft.
  • -1: Legacy systems with hardcoded HS256 secrets and no rotation policies will continue to be exploited, particularly in enterprise mergers where disparate security postures collide.
  • +1: The rise of passkeys and WebAuthn may reduce reliance on bearer tokens for authentication, complementing JWT for authorization while reducing token theft vectors.
  • -1: Attackers will increasingly target refresh token endpoints as the weak link in JWT implementations, necessitating additional layers of protection like token binding and device fingerprinting.
  • +1: Tooling and libraries will improve, making secure JWT configurations the default rather than the exception—reducing the cognitive load on developers.
  • -1: The continued use of localStorage in production SPAs will remain a significant vulnerability, as developer education fails to keep pace with rapid framework adoption.
  • +1: Cryptographic agility will become a priority, allowing seamless migration to post-quantum signature algorithms as they mature.
  • +1: Adoption of JWT claims-based access control (RBAC, ABAC) will enable more granular security policies, improving overall system resilience.

▶️ Related Video (78% Match):

🎯Let’s Practice For Free:

🎓 Live Courses & Certifications:

Join Undercode Academy for Verified Certifications

🚀 Request a Custom Project:

Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands

IT/Security Reporter URL:

Reported By: https://lnkd.in/p/e83bC8yx – Hackers Feeds
Extra Hub: Undercode MoN
Basic Verification: Pass ✅

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow UndercodeTesting & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin | 🦋BlueSky