AuthN vs AuthZ, OAuth 2.1, OIDC, JWTs, mTLS
Separate authentication from authorization, validate a JWT correctly, and choose between bearer, DPoP, and mTLS.
Why this matters
You cannot reason about agent identity without rock-solid foundations. Most agent-IAM bugs in production aren't novel — they're the classic OAuth + JWT pitfalls re-skinned. Nail these now; everything later builds on them.
Learning objectives
- Cleanly separate authentication, authorization, and accounting.
- Know what OAuth 2.1 changes versus 2.0.
- Validate a JWT correctly (signature,
iss,aud,exp,nbf,jti). - Use OIDC to authenticate users; use OAuth to authorize agents.
- Understand when to use bearer tokens vs mTLS vs DPoP.
1. AuthN, AuthZ, AAA
- Authentication (AuthN) — who are you. Proven by credentials (password, passkey, certificate, OIDC ID token).
- Authorization (AuthZ) — what can you do. Decided per request, per resource, by policies, scopes, claims.
- Accounting / Audit — what did you do. Append-only logs of decisions and actions.
A surprising number of bugs come from conflating AuthN with AuthZ. "I'm logged in as Alice" does not mean "I'm allowed to refund this customer". Verify both at every trust boundary.
For agents you'll see four principal types: end-user, agent acting for that user, agent acting on its own behalf, and tool / downstream service. Each needs its own AuthN and its own AuthZ policy. (Identity-model details in lesson 1.2.)
2. OAuth 2.1 in one page
OAuth 2.1 is the IETF consolidation of OAuth 2.0 + Best Current Practices + PKCE for all clients. It does not introduce major new flows; it removes the bad ones.
Key changes from 2.0 → 2.1:
- Implicit flow — removed. (Tokens-in-URL fragment was a perennial XSS leak.)
- Resource Owner Password Credentials — removed. (Apps must never see passwords.)
- PKCE mandatory for all clients using the authorization-code flow (public and confidential).
- Redirect URI matching must be exact string match (no wildcards or path-prefix tricks).
- Refresh tokens for public clients must be sender-constrained (DPoP) or rotated on every use.
- Bearer tokens in URI query — forbidden.
The supported flows today:
| Flow | Use |
|---|---|
| Authorization Code + PKCE | Web, native, SPA, mobile, anything with a user |
| Client Credentials | Backend-to-backend, no user |
| Device Authorization Grant (RFC 8628) | CLIs, TVs, devices without browsers |
| Token Exchange (RFC 8693) | Delegation, impersonation, on-behalf-of |
| CIBA (Client-Initiated Backchannel Authentication) | Agents that need user approval out-of-band |
All five matter for agents. The classical "user → web app → API" picture is one of many.
3. OpenID Connect (OIDC) in one page
OIDC is a thin identity layer on top of OAuth 2.0/2.1. The big additions:
- ID token — a JWT proving who logged in (claims:
sub,email,name,auth_time,acr,amr). /userinfoendpoint for follow-up profile data.- Discovery —
.well-known/openid-configurationexposes endpoints + signing keys (jwks_uri). noncein the auth request, echoed in the ID token to bind it to a specific browser session.
Rule of thumb:
- OIDC = "the user is X" (authentication).
- OAuth access token = "the agent may do Y" (authorization).
Mixing them is the most common identity bug. The ID token is not for sending to APIs. The access token is not for identifying the user.
4. JWT validation — the part everyone gets wrong
A JWT (RFC 7519) is header.payload.signature, all base64url-encoded. For an access token in JWT form (most IdPs use JWT, though OAuth itself allows opaque tokens):
import jwt, requests
from jwt import PyJWKClient
ISSUER = "https://localhost:8443/realms/agents-dev"
AUDIENCE = "agent-api" # the resource you are
JWKS_URI = requests.get(f"{ISSUER}/.well-known/openid-configuration", verify=False).json()["jwks_uri"]
jwks = PyJWKClient(JWKS_URI)
def verify(token: str) -> dict:
signing_key = jwks.get_signing_key_from_jwt(token).key
return jwt.decode(
token,
signing_key,
algorithms=["RS256", "ES256"],
audience=AUDIENCE,
issuer=ISSUER,
options={"require": ["exp", "iat", "sub", "iss", "aud"]},
)Things you must check:
- Signature (don't accept
alg: none; pin algorithms). issmatches your trusted IdP exactly.audincludes your resource identifier.expis in the future;nbfis in the past.jtiif you implement replay protection (cache for token lifetime).- Algorithm allowlist — never trust the header's
alg; reject HS* if you expect RS*/ES*.
Pitfalls:
- Verifying with the wrong key because you trusted
kidblindly → fetch from the IdP's JWKS only. - Skipping audience check → token meant for another API accepted.
- Caching JWKS forever → fails when IdP rotates keys (cache 1 h with revalidation).
- Using
jwt.decode(..., verify=False)for "debugging" then forgetting → silent disaster.
5. Token formats: JWT, opaque, sender-constrained
JWT bearer
Self-contained, statelessly verifiable. Trade-off: revocation is hard (you wait for exp unless you maintain a revocation list).
Opaque
Random string; resource server calls IdP's /introspect endpoint (RFC 7662). Easy revocation; extra hop.
For agents: prefer short-lived JWTs (5-15 min) + refresh tokens + introspect on sensitive operations.
Sender-constrained tokens
A token tied to a key the client holds. Stealing the token alone isn't enough.
- mTLS-bound tokens (RFC 8705) — client TLS cert thumbprint embedded in token.
- DPoP (RFC 9449) — client signs each request with a private key; token carries the public-key thumbprint.
For agents calling sensitive APIs or holding refresh tokens for hours, DPoP is the modern answer. Increasingly required by financial APIs (FAPI 2.0).
6. mTLS for service-to-service
When two services you operate talk to each other, mutual TLS often beats bearer tokens:
- Each party presents a certificate.
- Identity = the SAN (Subject Alternative Name) on the cert.
- No bearer tokens to leak; no JWT validation pitfalls.
- Pairs naturally with SPIFFE/SPIRE (lesson 2.2) for short-lived workload certs.
You'll still use OAuth at the user-facing edge — but inside your VPC, mTLS is simpler and more secure for service-to-service. Use both layers; don't pick one or the other.
7. Scopes vs claims vs permissions
- Scope — a coarse string asked for at authorization time (
read:tickets,write:invoice). User consents to scopes; tokens carry them. - Claim — any key/value in the token (
sub,email,roles,tenant_id,org). - Permission — a fine-grained authorization decision the resource server makes, often using OPA/Cedar/OpenFGA with the token claims as input.
Scopes are blunt. Real authorization is policy + claims + resource context. (Lesson 6.1.)
Anti-pattern: dozens of scopes that mirror your DB schema (read:user.profile.email). Keep scopes few and coarse; do fine-grained checks at the resource server.
8. Token introspection + revocation
POST /introspect(RFC 7662): "is this token still valid? what claims does it have?".POST /revoke(RFC 7009): "kill this refresh token / access token".
For agents, introspect on:
- High-value actions (payments, deletes).
- The first call of a session.
- Any time a token has been idle > 5 min.
Don't introspect every request — round-trips kill latency. Cache the result for the token's remaining life or a short window, whichever is shorter.
9. PKCE in pictures
1. Agent generates verifier (random 64 bytes), challenge = SHA-256(verifier) base64url
2. Agent → IdP: /authorize?...&code_challenge=...&code_challenge_method=S256
3. User authenticates; IdP redirects with ?code=...
4. Agent → IdP: POST /token { code, code_verifier, ... }
5. IdP verifies SHA-256(verifier) == stored challenge
6. IdP returns access_token + id_token + refresh_tokenPKCE prevents a network attacker who steals the code from exchanging it — they don't have the verifier. Mandatory in OAuth 2.1 even for confidential clients (defence in depth).
10. Hands-on lab (2 h)
- Build a tiny FastAPI resource server
/methat:- Validates JWT via Keycloak JWKS (RS256).
- Requires
aud=agent-apiandissmatches your realm. - Returns
sub,email,roles.
- Call it three ways, confirming behaviour:
- Valid token → 200.
- Tampered signature → 401.
- Wrong
aud→ 401. - Expired token → 401.
- Add DPoP: generate an EC key in your client, sign each request with a
DPoPheader containinghtu,htm,iat,jti. Verify on the server. - Add mTLS option: turn on Uvicorn
--ssl-cert-reqs=2, issue a client cert withstep certificate create, call again.
11. Common pitfalls
- Treating ID token as access token (or vice versa).
- Not pinning algorithms —
alg: noneand HS/RS confusion attacks. - Wildcard / prefix-match redirect URIs.
- Long-lived bearer tokens with no DPoP — equivalent to a password you mailed to a contractor.
- Trusting
subacross IdPs (each IdP definessubseparately; namespace if you federate). - Skipping
noncein OIDC → ID token replay.
12. Self-check
- AuthN vs AuthZ in one sentence each.
- Three things OAuth 2.1 removes from OAuth 2.0.
- Five JWT checks that must always pass.
- Bearer vs DPoP vs mTLS — pick per scenario.
- Difference between scope and claim.
13. References
- OAuth 2.1 draft (IETF
draft-ietf-oauth-v2-1). - RFC 7519 (JWT), 7515 (JWS), 7517 (JWK), 7518 (JWA).
- RFC 8252 (Native apps), 8628 (Device flow), 8693 (Token exchange).
- RFC 9449 (DPoP).
- OpenID Connect Core 1.0 + Discovery 1.0.
- OWASP "JWT Best Current Practices".
- Auth0 "OAuth 2.1: What's New".
Sign in to save your progress and earn badges.