Auth: Troubleshooting
First diagnostic: does the ?flow= UUID rotate?
Stuck reloading on /login?flow=<id>? Watch the URL between redirects:
- UUID changes each time → cookie-forward gap: a server-side flow fetch isn't passing the browser's
Cookieheader. Fix in Auth: Flows & Recovery. - UUID stays the same (or no
?flow=at all) → post-login middleware/Oathkeeper loop. See the table below.
Symptom → cause → fix
| Symptom | Root cause | Fix |
|---|---|---|
Form submit fails with ERR_NAME_NOT_RESOLVED | Form action points at the in-cluster hostname (http://ory-kratos:4433/...) because Kratos serve.public.base_url is set to the in-cluster URL | Chart-side: set kratosPublicBaseUrl to the same-origin proxy URL. App-side: proxy rewrites any ory-kratos hostname in Location headers and JSON ui.action fields |
| Blank screen on submit | Same as above — the failed fetch throws inside the form's try/catch and the form unmounts before the error renders | Same fixes; check the Network tab for the unreachable host |
| Login silently fails — no error, no redirect, no cookie | Cookies dropped: cross-origin request, or missing credentials: 'include' | Route all browser traffic through the same-origin proxy; ensure the proxy strips Domain= from Set-Cookie; add credentials: 'include' to fetches |
| 400 on login that looks like "wrong password" | Consumed single-use flow (double-submit, React re-render) | Gate submission on a status flag; on session_already_available, redirect — the user is already in |
session_already_available on the login page | Stale ory_kratos_session cookie from a previous cluster run | Clear localhost cookies; in code, catch the error id and redirect to the post-login path |
307 loop, rotating ?flow= UUID | Server-side flow fetch missing the cookie parameter | Forward the browser Cookie on every toSession / flow-fetch call |
Post-login loop between /login and dashboard (UUID stable) | Middleware checks for the session cookie, but behind Oathkeeper the cookie isn't visible at that layer | Middleware checks the x-user-id header in production, or drop middleware and rely on Oathkeeper rules |
getUser() returns null in dev despite being logged in | Dev fallback (toSession) not receiving the browser Cookie header | Pass cookie through to toSession; confirm the cookie exists in dev tools |
500s on all auth after kilter db reset | Stale Ory nid cache | kubectl rollout restart deployment ory-kratos ory-keto ory-hydra -n <name>-dev (auto-recovers in ~90s via liveness probes) |
| Oathkeeper 401 on protected routes | Session cookie not forwarded or name mismatch | Verify ory_kratos_session exists in the browser; kilter logs oathkeeper |
| User in Kratos but missing from app DB | JIT provisioning hasn't run yet — users are created on first authenticated request | Expected; check ensureUser() for DB errors if it persists |
Debugging commands
kilter logs kratos # identity/session errors, courier (email) errors
kilter logs oathkeeper # access/deny decisions
kilter logs keto # permission checks
curl http://localhost:<kratos-admin-port>/admin/identities | jq '.[].traits.email'Ports come from kilter env — they differ per project.
Keto: when you actually need it
Most apps don't. The default kilter pattern — a role column in the app database, synced from Kratos metadata — covers simple role checks with zero extra infrastructure.
| Situation | Use |
|---|---|
| "Is this user an admin?" | profiles.role in the app DB |
| User owns a resource | App-level query — simpler than tuples |
| One or two membership types | App queries |
| Deep hierarchies (org → team → project → resource), many relation types | Keto |
If Keto is in play and a check returns 403 unexpectedly, the tuple is probably missing — list tuples on the read port (4466) filtered by namespace/object/subject, and write the missing one via the admin API on the write port (4467).
Start with roles in the DB
Adding Keto later is straightforward; removing it once ACL checks are scattered through the codebase is not. Reach for it only when authorization complexity has demonstrably outgrown app queries.