Stop Building Custom Password Reset: Ory Kratos Recovery Flow Does It Declaratively
Kratos provides a declarative, self-service account recovery flow that eliminates custom password reset code. Two strategies (link or code), single-use tokens, automatic session issuance — all configured in YAML. Here's how to adopt it and where the limits are.
13 Sept 2025, 15:34 UTC

The Problem: Every Team Reimplements Password Reset
You've seen it: a forgotten-password form, a token generator, an email template, a validation endpoint, a new-password form, session creation — all custom code. Each piece introduces bugs: token reuse, missing rate limits, leaked tokens in referrer headers, expired links that still work. Kratos eliminates this entire surface area with a self-service recovery flow configured in YAML.
Two Recovery Strategies, One Flow Contract
Kratos exposes two methods under the same /self-service/recovery endpoint:
- Link-based: emails a single-use recovery link containing a token (default TTL 1 hour).
- Code-based: emails or SMSs a one-time code the user enters in your UI.
Both return a flow object with pre-rendered UI nodes — CSRF token, method selector, email field, submit button — that your frontend renders without knowing business logic. The flow ID ties everything together; you never handle raw tokens in application code.
Worked Example: Initializing Recovery from a SPA
Run Kratos locally to inspect the contract:
docker run -p 4433:4433 -p 4434:4434 oryd/kratos:latest serve --devThen hit the API initialization endpoint:
curl -s http://localhost:4433/self-service/recovery/api | jq .You'll get JSON like:
{
"id": "flow-uuid",
"type": "api",
"expires_at": "2026-10-13T03:33:00Z",
"ui": {
"action": "http://localhost:4433/self-service/recovery?flow=flow-uuid",
"method": "POST",
"nodes": [
{"type": "input", "attributes": {"name": "csrf_token", "type": "hidden", "value": "..."}},
{"type": "input", "attributes": {"name": "method", "type": "hidden", "value": "link"}},
{"type": "input", "attributes": {"name": "traits.email", "type": "email", "required": true}},
{"type": "input", "attributes": {"name": "method", "type": "submit", "value": "Submit"}}
]
}
}Your SPA renders these nodes verbatim. On submit, Kratos validates the email, sends the recovery email via its Courier service, and returns either a success UI (with a session token if configured) or error nodes you display.
Security Model: Tokens Are Single-Use and Short-Lived
Recovery tokens are bound to the flow ID, invalidated on use or expiry, and rate-limited at the API level. The default 1-hour TTL is configurable via selfservice.flows.recovery.lifespan. Link-based recovery puts the token in the URL — if your logs or referrer headers leak, the token can be hijacked. Code-based recovery mitigates this but adds a step for the user. Choose based on your threat model.
Configuration Is Declarative — No Go Plugins Required
All behavior lives in kratos.yml:
selfservice:
flows:
recovery:
lifespan: 1h
use: link # or code
after:
recovery:
hook: session # issues session cookie/token automatically
show: "" # empty = redirect to settings
methods:
link:
enabled: true
config:
base_url: https://app.example.com/recovery
code:
enabled: false
request_limiter:
max_requests: 5
window: 1hEmail templates live in courier/templates/ — mount a volume to override branding, localization, and phishing-resistant messaging. The default templates are minimal; production deployments must customize them.
Verified vs Unverified Addresses
If the email trait in your identity schema is marked verified: true, recovery proceeds. If not, the require_verified_address setting controls behavior: true forces verification first, false allows recovery to an unverified address. This prevents account takeover via unverified emails but adds friction for users who never verified.
Limitations You'll Hit in Production
- Password policy: Kratos does not enforce history or complexity on recovery. Define those in your identity schema (JSON Schema) and validate on the
updateflow, not recovery. - Rate limits: Global by default. Multi-tenant apps need per-IP or per-identity tuning via
selfservice.flows.recovery.request_limiter. - Mobile/native apps: Session issuance after recovery only works in the same browser context (cookie-based). Native apps must use the code flow with token response and manually establish the session.
- Ory Cloud constraints: Advanced UI node overrides (
selfservice.flows.recovery.ui.nodes) have limited expressiveness compared to a custom backend.
Verify It Works Before Shipping
- Start Kratos in dev mode (command above).
- Complete a recovery flow in a browser; inspect the
ory_kratos_sessioncookie orsession_tokenin the API response. - Check rendered emails via the Courier SMTP mock on port 4436.
- Run the integration tests:
go test ./internal/selfservice/flow/recovery/...in the Kratos source to confirm token expiry, reuse prevention, and verified-address enforcement.
Closing: Adopt the Flow, Stop Maintaining Glue
Kratos recovery isn't magic — it's a well-scoped, configurable flow that handles the boring, dangerous parts of account recovery. Your job reduces to: define the identity schema, configure the method and TTL, override email templates, and render the UI nodes Kratos returns. That's a few hundred lines of YAML and frontend code versus thousands of custom backend logic. Ship the declarative approach; keep your custom code for business logic that actually differentiates your product.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.