Auth, Kratos integration
How Auth talks to Kratos for identity flows
Auth renders Kratos's self-service flows. The integration is mostly client-side: the user's browser interacts with Kratos via Auth's pages. Auth's server side rarely calls Kratos's admin API.
Browser-driven flows
For login, registration, recovery, verification, settings:
Browser
→ GET /login # Auth renders form
→ POST /api/kratos/<flow>/submit # Proxied to Kratos
→ GET /api/kratos/flows?id=... # Get current flow stateThe proxy routes under /api/kratos/ forward to Kratos's public endpoint, preserving cookies (Kratos session and CSRF). The browser sees Auth's domain; Kratos sees the same request via the proxy.
Server-side Kratos calls
Auth's server code calls Kratos for:
- Reading the current session during page render, to know if the user is logged in.
- Initiating the recovery flow when the user requests a password reset (the form post triggers this).
- Accepting Hydra challenges (login/consent) by sending the Kratos identity ID to Hydra.
// src/lib/kratos.ts
import { KratosClient } from "@/lib/kratos-client";
export async function getCurrentSession(cookie: string) {
const response = await fetch(`${kratosPublicUrl}/sessions/whoami`, {
headers: { cookie }
});
if (!response.ok) return null;
return await response.json();
}Flow lifecycle
The Kratos flow ID is the linchpin:
- Browser hits
/login. Auth redirects to/self-service/login/browser(Kratos initiates flow, redirects back with flow ID). - Auth at
/login?flow=ABCfetches the flow state from Kratos. - Auth renders the form.
- User submits. The form POSTs to
flow.ui.action(a Kratos endpoint). - Kratos validates, returns updated flow state (success or error).
- Auth re-renders with the new state (still at the same flow ID).
- On success, Kratos redirects to
selfservice.flows.<flow>.after.password.hooks[].redirect_urlor whatever's configured.
Adding pre/post checks
Ionize adds two pre-Kratos checks: brute-force and captcha. They run server-side before Auth forwards the submission to Kratos:
// Pseudocode for the login POST handler
async function handleLoginSubmit(data, cookies) {
// 1. Captcha first
const captchaOk = await verifyCaptcha(data.cf_turnstile_token);
if (!captchaOk) return error("captcha_failed");
// 2. Brute-force check
const locked = await isLockedOut("ciam", data.identifier);
if (locked) return error("rate_limited");
// 3. Forward to Kratos
const response = await forwardToKratos(data, cookies);
// 4. Record outcome (whether Kratos succeeded or failed)
await recordLoginAttempt("ciam", data.identifier, response.ok);
return response;
}Cookies
Auth's pages share Kratos's session cookie scope (ory_kratos_session). The cookie domain must match, see Security, Session cookies.
Auth does not set its own session cookie. Dashboard does (its own dashboard-session); Auth doesn't need one because user identity is encoded in Kratos's cookie.
Errors
Kratos returns errors as flow state with ui.messages. Auth renders them as user-visible alerts.
For internal errors (Kratos unreachable, malformed flow), Auth shows a generic "/error" page with a request ID. The full error details log to stdout for the operator.
Hooks not done in Auth
Some things Kratos can do via hooks that Auth doesn't:
- Webhook on registration success.
- Custom validation logic.
These belong in kratos.yml configuration, not Auth code. Auth shouldn't grow business logic that Kratos already supports.
Schema reload
When the identity schema changes, Kratos reloads via the sidecar (see Operate, Reload API key rotation). Auth doesn't need a reload, it re-fetches the schema with every flow.