Debugging OAuth2 flows
How to trace an OAuth2 flow end-to-end when something goes wrong
OAuth2 flows touch ~5 services. When something fails, isolating the failure point matters.
The five stops
- Your app initiates: redirects to
<hydra>/oauth2/auth?.... - Hydra receives, creates a login challenge, redirects to Hera.
- Hera displays login form (or auto-accepts if Kratos session exists), submits to Kratos.
- Kratos authenticates the user, returns to Hera. Hera accepts the Hydra login challenge.
- Hydra redirects to consent (Hera again, auto-grants), then redirects to your app's callback with code.
- Your app exchanges code at
/oauth2/token.
Each handoff is a potential failure point.
Tracing
Use browser DevTools → Network tab → Preserve log. Reproduce the failing flow. You should see:
1. GET https://your-app/login 302
2. GET https://ciam.<domain>/oauth2/auth?client_id=... 302
3. GET https://ciam.<domain>/login?login_challenge=... 302
4. GET https://ciam.<domain>/self-service/login/browser?... 200 (Hera renders form)
5. POST https://ciam.<domain>/self-service/login?flow=... 200 / 422 (Kratos validates)
6. GET https://ciam.<domain>/login?... 302 (Hera accepts challenge)
7. GET https://ciam.<domain>/consent?consent_challenge=... 302 (auto-grant)
8. GET https://your-app/callback?code=... 200 (your app handles code)If the chain breaks at any step, look at:
- The response status and Location header.
- The browser console for JS errors.
- The cookies sent and received.
Common breaks
Step 2, Hydra returns 400
error: invalid_client
error_description: Client authentication failedThe client_id is wrong or the client doesn't have the requested grant. Check Athena → OAuth2 Clients.
Step 5, Kratos returns 422
{ "error": { "id": "session_aal2_required" } }The user's session needs step-up. Follow Troubleshooting, Session AAL too low.
Step 5, Kratos returns 400 with CSRF violation
See Troubleshooting, Kratos CSRF violation.
Step 8, Your app's callback fails
Check what your callback received:
- Did
codearrive in the URL? If not, Hydra is rejecting the consent / redirect. - Did your
/oauth2/tokenexchange succeed? Look at the response. See Troubleshooting, OAuth2 invalid_grant. - Did PKCE verifier match? See Troubleshooting, OAuth2 pkce_required.
Server-side logs
In each step, the relevant service logs the action. Tail:
podman compose logs -f --since 10m ciam-hera ciam-kratos ciam-hydraLook for:
- Kratos:
flow_id,identity_id, error messages. - Hydra:
client_id,challenge_id, accept/reject decisions. - Hera: console.log of the flow handler.
OAuth2 playground
The Site app at https://your-domain/playground exercises a complete Authorization Code + PKCE flow against your CIAM Hydra. Useful for confirming the platform itself works when you're debugging a specific app's integration.
Decode the tokens
When you get tokens back, decode the ID token (it's a JWT):
echo "$ID_TOKEN" | cut -d. -f2 | base64 -d | jqCheck iss, aud, iat, exp are sane.
Hydra admin debugging
If a flow is stuck, look at Hydra's perspective:
# Inspect a login challenge
podman exec ciam-hydra hydra get login-request <challenge-id> --endpoint http://localhost:5003Shows whether the challenge is accepted, rejected, expired, etc.