Lion Cipher Lab
Authenticate OAuth clients with private_key_jwt from scratch (RFC 7523), then close the audience and replay gaps
- Identity & Access Management
- Advanced
- ~75 min
- Free · no account required
Why it matters
Every backend service that talks to an OAuth server has to prove who it is, and most still do it by sending the same shared secret on every request. Whoever copies that secret once becomes the service for as long as nobody rotates it. private_key_jwt replaces the secret with a short, single-use, signed statement, and the server stores only a public key. It is the client authentication that the OAuth Security Best Current Practice recommends, and it is required by high-assurance profiles like FAPI. It is also easy to get subtly wrong: in 2025 the OpenID Foundation disclosed that loose audience…
What you'll learn
- authenticating a confidential OAuth client with private_key_jwt (a signed JWT client assertion, RFC 7523) instead of a shared client secret
- hand-encoding a JWT and producing an ES256 signature in the raw R||S form JWS requires
- writing a token-endpoint verifier that pins the algorithm per client, checks the signature by kid, enforces aud = issuer identifier, caps lifetime and remembers every jti
- reproducing the 2025 OpenID Foundation private_key_jwt audience flaw on localhost and closing it from both the server and the client side
- measuring why a captured client_secret_basic header stays usable while a captured assertion does not
- choosing between client_secret_basic, private_key_jwt and mTLS client authentication (RFC 8705), and naming what each one does NOT stop
Steps in this lab
- The problem: a client secret is a password you send every time
- Set up Python and one package
- Write the shared module: keys, the JWT, and two verifiers
- Authenticate once, then try the same assertion again
- What the hardened verifier checks, and why in that order
- Run the tamper matrix against both verifiers
- The 2025 audience disclosure: what was actually found
- Reproduce the audience gap on localhost, then close it from both sides
- Measure it against client_secret_basic
- When to choose private_key_jwt, mTLS, or a secret
- Honest negatives: rotation, caching, clock skew and key theft
- How a pro does it for real