Building and Breaking My Own JWT Auth System — Part 1: Setting the Foundation
Part 1 of a series where I build a Flask API with JWT authentication, then deliberately exploit six real vulnerabilities in it — documenting why each one exists at the JWT specification level, not just in the code.
What this series is
JWTs are everywhere in modern authentication, and JWT misconfigurations are some of the most common — and most tested — vulnerabilities in identity and access management. Most write-ups on this topic show you the exploit and move on. This series does something slightly different: I'm building the auth system first, breaking it myself, and documenting why the spec allows the break before fixing it.
By the end of the series, one small Flask API will have been through:
alg: nonesignature bypassWeak HMAC secret cracking (offline, via hashcat)
kidheader parameter injectionJWKS URI spoofing
Token replay after expiry
RS256 → HS256 algorithm confusion
Each of these maps to a real, documented class of vulnerability that shows up repeatedly in JWT libraries and implementations in the wild — not contrived bugs.
Why build it instead of just reading about it
Reading a writeup of a JWT vulnerability and reproducing it yourself are two very different levels of understanding. If I can only explain an attack in the abstract, I don't actually understand where in the trust chain it breaks. Building the vulnerable version first — and watching my own token get forged — is a much sharper way to learn than starting from a hardened example and reading commentary on it.
Part 1: the boring (necessary) part — environment and foundation
Before any JWT code exists, this part covers the project's foundation: an isolated Python environment, a minimal Flask app, and git/GitHub set up correctly from the start.
This matters more than it looks like at first glance. A few decisions made here directly support the security reasoning later in the series:
A virtual environment isolates dependencies per-project. This becomes relevant again later when pinning exact library versions matters for reproducing a specific vulnerability — some JWT library CVEs are version-specific.
.gitignoreexcludesvenv/and a future.envfile from day one, before any real secret exists in the project. Leaked secrets in public repos are themselves a well-known vulnerability class — the habit of gitignoring secrets before writing any code that generates them is deliberate, not an afterthought.requirements.txtpins exact versions, so the environment is fully reproducible — important for anyone (including future me) trying to recreate a specific exploit later.
The first working piece of the API is just a single health-check route:
from flask import Flask
app = Flask(__name__)
@app.route("/health")
def health():
return {"status": "ok"}
if __name__ == "__main__":
app.run(debug=True)
Nothing security-relevant yet — just confirming the request/response cycle works end-to-end before adding authentication on top of it.
$ curl http://127.0.0.1:5000/health
{
"status": "ok"
}
What's next
Part 2 builds the actual login flow and issues a first JWT — and that's where the interesting part of the series begins: understanding exactly what's inside a JWT (header, payload, signature) and why that structure is what makes every later attack possible.
Follow along — the full code for each stage is on GitHub.
