KBkilterKB
dev

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 Cookie header. 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

SymptomRoot causeFix
Form submit fails with ERR_NAME_NOT_RESOLVEDForm action points at the in-cluster hostname (http://ory-kratos:4433/...) because Kratos serve.public.base_url is set to the in-cluster URLChart-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 submitSame as above — the failed fetch throws inside the form's try/catch and the form unmounts before the error rendersSame fixes; check the Network tab for the unreachable host
Login silently fails — no error, no redirect, no cookieCookies 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 pageStale ory_kratos_session cookie from a previous cluster runClear localhost cookies; in code, catch the error id and redirect to the post-login path
307 loop, rotating ?flow= UUIDServer-side flow fetch missing the cookie parameterForward 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 layerMiddleware checks the x-user-id header in production, or drop middleware and rely on Oathkeeper rules
getUser() returns null in dev despite being logged inDev fallback (toSession) not receiving the browser Cookie headerPass cookie through to toSession; confirm the cookie exists in dev tools
500s on all auth after kilter db resetStale Ory nid cachekubectl rollout restart deployment ory-kratos ory-keto ory-hydra -n <name>-dev (auto-recovers in ~90s via liveness probes)
Oathkeeper 401 on protected routesSession cookie not forwarded or name mismatchVerify ory_kratos_session exists in the browser; kilter logs oathkeeper
User in Kratos but missing from app DBJIT provisioning hasn't run yet — users are created on first authenticated requestExpected; 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.

SituationUse
"Is this user an admin?"profiles.role in the app DB
User owns a resourceApp-level query — simpler than tuples
One or two membership typesApp queries
Deep hierarchies (org → team → project → resource), many relation typesKeto

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.