Account recovery and password reset
Please read the Self-Service Flows overview before continuing with this document.
Account Recovery must be performed if access to an account needs to be recovered. Common use cases include:
- "Forgot password" flows
- "Lost MFA device" flows
- ...
There are two Recovery Flow types supported in Ory Kratos:
- Flows where the user sits in front of the Browser and the application is
- a server-side application (Node.js, Java, ...)
- a client-side application (React.js, Angular, ...)
- Flows where API interaction is required (Mobile app, Smart TV, ...)
The Recovery Flow can be summarized as the following state machine:
- Ory Cloud
- Ory Kratos
Account recovery can be configured in the "Customize/Account Recovery" screen of Ory Cloud.
You can configure which methods to use in the Ory Kratos config:
selfservice:
methods:
link:
enabled: true
config:
# Defines how long a recovery link is valid for (default 1h)
lifespan: 15m
# If the link should point to a domain (and path) that differs from the configured public base URL,
# set this value to the base URL you want:
base_url: https://my-example-domain.com
flows:
recovery:
enabled: true
# Defines how long a recovery flow (the UI interaction, not the link!)
# is valid for (default 1h)
lifespan: 15m
# ...
Methods
One recovery method is supported:
- The
link
method performs account recovery (also known as password reset) by sending an email containing a recovery link to the user.
Recovery link
method
There are two email types sent by this method:
This prevents account enumeration attacks as explained in this brilliant blog post by Troy Hunt.
The emails are using templates that can be customized as explained in Customizing E-Mail Templates. The template IDs are:
- Unknown email address:
recovery_invalid
- Known email address:
recovery_valid
You should also configure how long a session is privileged. The user will only be able to update his/her password (or any other credential) for the specified amount of time after clicking on the recovery link.
If the email address used for recovery purposes is the same as the email used for verification purposes and the account hasn't yet been activated (see Account Activation - if the email address isn't yet verified, the verification status of the corresponding object in Kratos will be set to verified upon successful flow finalization.
- Ory Cloud Platform
- Ory Kratos
Configuring the privileged session duration will be possible in the future.
You can configure the privileged session duration in the Ory Kratos config:
selfservice:
flows:
settings:
privileged_session_max_age: 15m
To specify that an identity's trait's a recovery email, use the following Identity Schema:
{
"$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
},
+ "recovery": {
+ "via": "email"
+ }
}
}
}
"additionalProperties": false
}
}
}
Initialize recovery flow
severity
Ory Kratos and your UI must be on the hosted on same top level domain! You can not host Ory Kratos and your UI on separate top level domains:
kratos.bar.com
andui.bar.com
will work;kratos.bar.com
andbar.com
will work;kratos.bar.com
andnot-ar.com
will not work.
The first step is to initialize the Recovery Flow. This sets up Anti-CSRF tokens and more. Each recovery flow has a state
parameter which follows the state machine:
where
choose_method
indicates that the user hasn't chosen a recovery method yet. This is useful whenlink
isn't the only recovery method active.sent_email
implies that the recovery email has been sent out.passed_challenge
is set when the user has clicked the recovery link and completed the account recovery.
Recovery for server-side browser clients
The Recovery Flow for browser clients relies on HTTP redirects between Ory Kratos, your Recovery UI, and the end-user's browser:
The Flow UI (your application!) is responsible for rendering the actual Login and Registration HTML Forms. You can of course implement one app for rendering all the Login, Registration, ... screens, and another app (think "Service Oriented Architecture", "Micro-Services" or "Service Mesh") is responsible for rendering your Dashboards, Management Screens, and so on.
To initialize the Recovery Flow, point the Browser to the initialization endpoint:
curl -s -i -X GET \
-H "Accept: text/html" \
https://playground.projects.oryapis.com/self-service/recovery/browser
HTTP/2 303
date: Fri, 09 Jul 2021 12:50:38 GMT
content-type: text/html; charset=utf-8
content-length: 120
location: https://playground.projects.oryapis.com/hosted/recovery?flow=33964b22-c364-4217-b6fc-5f381d142d65
cache-control: private, no-cache, no-store, must-revalidate
set-cookie: aHR0cHM6Ly9wbGF5Z3JvdW5kLnByb2plY3RzLm9yeWFwaXMuY29tL2FwaS9rcmF0b3MvcHVibGlj_csrf_token=WCOWjhwHy338874TGz6S7ICVp/hFFVHV8Ev7e1hZYSo=; Path=/api/kratos/public; Domain=playground.projects.oryapis.com; Max-Age=31536000; HttpOnly; Secure; SameSite=None
vary: Origin
vary: Cookie
strict-transport-security: max-age=15724800; includeSubDomains
<a href="https://playground.projects.oryapis.com/hosted/recovery?flow=33964b22-c364-4217-b6fc-5f381d142d65">See Other</a>.
The server responds with a HTTP 303 redirect to the Recovery UI, appending the ?flow=<flow-id>
query parameter (see the curl
example) to the URL configured here:
- Ory Cloud Platform
- Ory Kratos
The Ory Cloud Platform offers a default UI implementation. Pointing to another UI endpoint will be supported in the future.
You can configure which recovery URL to use in the Ory Kratos config:
selfservice:
flows:
recovery:
# becomes http://127.0.0.1:4455/recovery?flow=df607aa1-d555-4b2a-b3e4-0f5a1d2fe6f3
ui_url: http://127.0.0.1:4455/recovery
Recovery for client-side (AJAX) browser clients
The Recovery Flow for client-side browser clients relies on AJAX requests.
severity
This flow requires AJAX and you need to ensure that all cookies are sent using the appropriate CORS and includeCredentials
configurations. Additionally, Ory Kratos and your app must be hosted on the same domain.
To initialize the Recovery Flow, call the recovery initialization endpoint and set Accept: application/json
:
curl -v -s -X GET \
-H "Accept: application/json" \
https://playground.projects.oryapis.com/self-service/recovery/browser | jq
> GET /self-service/recovery/browser HTTP/2
> Host: playground.projects.oryapis.com
> User-Agent: curl/7.64.1
> Accept: application/json
< HTTP/2 200
< date: Fri, 09 Jul 2021 09:36:34 GMT
< content-type: application/json; charset=utf-8
< content-length: 1241
< cache-control: private, no-cache, no-store, must-revalidate
< set-cookie: aHR0cHM6Ly9wbGF5Z3JvdW5kLnByb2plY3RzLm9yeWFwaXMuY29tL2FwaS9rcmF0b3MvcHVibGlj_csrf_token=wSDoLSdDqNJv2uWVWdv5euaQo9UimCFS1GhXokTLU3o=; Path=/api/kratos/public; Domain=playground.projects.oryapis.com; Max-Age=31536000; HttpOnly; Secure; SameSite=None
< vary: Origin
< vary: Cookie
< strict-transport-security: max-age=15724800; includeSubDomains
{
"id": "d37e270d-eb84-472a-8032-3f0f01786210",
"type": "browser",
"expires_at": "2021-06-15T14:27:31.270617Z",
"issued_at": "2021-06-15T14:22:31.270617Z",
"request_url": "https://playground.projects.oryapis.com/self-service/recovery/browser",
"ui": {
"action": "http://127.0.0.1:4455/self-service/recovery?flow=d37e270d-eb84-472a-8032-3f0f01786210",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "17VNGchueAPEYWji/X3ORohTfHUGqA0HPqTJILpE4xYfMNmIvif7xPIIcJdf62809Y1xGRTwhZMz6SyMDtCSag==",
"required": true,
"disabled": false
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "link",
"attributes": {
"name": "email",
"type": "email",
"required": true,
"disabled": false
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "link",
"attributes": {
"name": "method",
"type": "submit",
"value": "link",
"disabled": false
},
"messages": [],
"meta": {
"label": {
"id": 1070005,
"text": "Submit",
"type": "info"
}
}
}
]
},
"state": "choose_method"
}
Recovery for API clients and clients without browsers
warning
Never use API flows to implement Browser applications! Using API flows in Single-Page-Apps as well as server-side apps opens up several potential attack vectors, including Login and other CSRF attacks.
The Recovery Flow for API clients doesn't use HTTP Redirects and can be summarized as follows:
To initialize the API flow, the client calls the API-flow initialization endpoint (REST API Reference) which returns a JSON response:
curl -s -X GET \
-H "Accept: application/json" \
https://playground.projects.oryapis.com/self-service/recovery/api | jq
{
"id": "660cc928-48f1-485b-8003-dc690f5afc35",
"type": "api",
"expires_at": "2021-07-09T13:52:56.270081773Z",
"issued_at": "2021-07-09T12:52:56.270081773Z",
"request_url": "http://playground.projects.oryapis.com/self-service/recovery/api",
"ui": {
"action": "https://playground.projects.oryapis.com/self-service/recovery?flow=660cc928-48f1-485b-8003-dc690f5afc35",
"method": "POST",
"nodes": [ /* */ ]
},
"state": "choose_method"
}
Recovery flow payloads
Fetching the Recovery Flow (REST API Reference) is usually only required for browser clients but also works for Recovery Flows initialized by API clients. All you need is a valid flow ID:
flowId=$(curl -s -X GET \
-H "Accept: application/json" \
https://playground.projects.oryapis.com/self-service/recovery/api | jq -r '.id')
curl -s -X GET \
-H "Accept: application/json" \
"https://playground.projects.oryapis.com/self-service/recovery/flows?id=$flowId" | jq
{
"id": "7c033350-a935-4807-a06e-85cfc2475c75",
"type": "api",
"expires_at": "2021-07-09T14:00:52.752217Z",
"issued_at": "2021-07-09T13:00:52.752217Z",
"request_url": "http://playground.projects.oryapis.com/self-service/recovery/api",
"active": "default",
"ui": {
"action": "https://playground.projects.oryapis.com/self-service/recovery?flow=7c033350-a935-4807-a06e-85cfc2475c75",
"method": "POST",
"nodes": [ /* ... */ ]
},
"state": "choose_method"
}
Send recovery link to email
The link
recovery mode will always open a link in the browser, even if it was initiated by an API client. This is because the
user clicks the link in his/her email client, which opens the browser.
When the link
method is enabled, it will be part of the methods
payload in the Recovery Flow:
curl -H "Accept: application/json" -s \
'https://playground.projects.oryapis.com/self-service/recovery/flows?id=199a2c74-08ca-4f22-b24c-3de3032682f8' | \
jq -r '.ui.nodes[] | select(.group=="link")'
{
"type": "input",
"group": "link",
"attributes": {
"name": "email",
"type": "email",
"required": true,
"disabled": false
},
"messages": null,
"meta": {}
}
{
"type": "input",
"group": "link",
"attributes": {
"name": "method",
"type": "submit",
"value": "link",
"disabled": false
},
"messages": null,
"meta": {
"label": {
"id": 1070005,
"text": "Submit",
"type": "info"
}
}
}
Recovery form validation
The form payloads are then submitted to Ory Kratos which follows up with:
- An HTTP 303 See Other redirect pointing to the Registration UI for Browser Clients;
- An
application/json
response for API Clients and Client-Side Browser applications (for example Single Page Apps).
Recovery link via email
To send the recovery email, the end-user fills out the form. There might be validation errors such as a malformed email:
- Browser UI
- Missing Email
curl -H "Accept: application/json" -s \
'https://playground.projects.oryapis.com/self-service/recovery/flows?id=199a2c74-08ca-4f22-b24c-3de3032682f8' | \
jq -r '.ui.nodes[] | select(.group=="link")'
{
"type": "input",
"group": "link",
"attributes": {
"name": "email",
"type": "email",
"required": true,
"disabled": false
},
"messages": [
{
"id": 4000002,
"text": "Property email is missing.",
"type": "error",
"context": {
"property": "email"
}
}
],
"meta": {}
}
{
"type": "input",
"group": "link",
"attributes": {
"name": "method",
"type": "submit",
"value": "link",
"disabled": false
},
"messages": null,
"meta": {
"label": {
"id": 1070005,
"text": "Submit",
"type": "info"
}
}
}
When validation errors happen, browser clients receive a HTTP 303 See Other redirect to the Recovery Flow UI, containing the Recovery Flow ID which includes the error payloads.
For API Clients, the server typically responds with HTTP 400 Bad Request and the Recovery Flow in the response payload as JSON.
Successful submission
On successful submission, an email will be sent to the provided address:
- Browser UI
- Email Sent
rl -H "Accept: application/json" -s \
'https://playground.projects.oryapis.com/self-service/recovery/flows?id=199a2c74-08ca-4f22-b24c-3de3032682f8' | \
jq
{
"id": "199a2c74-08ca-4f22-b24c-3de3032682f8",
"type": "browser",
"expires_at": "2021-04-28T13:47:17.913580042Z",
"issued_at": "2021-04-28T12:47:17.913580042Z",
"request_url": "https://playground.projects.oryapis.com/self-service/recovery/browser",
"active": "link",
"ui": {
"action": "https://playground.projects.oryapis.com/self-service/recovery?flow=199a2c74-08ca-4f22-b24c-3de3032682f8",
"method": "POST",
"messages": [
{
"id": 1060002,
"text": "An email containing a recovery link has been sent to the email address you provided.",
"type": "info",
"context": {}
}
],
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "Vax2/oPfhwTkUOHpE6UxDuM2ob+tr3RDTpKe3OHH9H6/+WWPed1/UuM3ttR5c2OihaEDcifiBkhBcbEZWa+96Q==",
"required": true,
"disabled": false
},
"messages": null,
"meta": {}
},
{
"type": "input",
"group": "link",
"attributes": {
"name": "email",
"type": "email",
"value": "foo@ory.sh",
"required": true,
"disabled": false
},
"messages": null,
"meta": {}
},
{
"type": "input",
"group": "link",
"attributes": {
"name": "method",
"type": "submit",
"disabled": false
},
"messages": null,
"meta": {
"label": {
"id": 1070005,
"text": "Submit",
"type": "info"
}
}
}
]
},
"state": "sent_email"
}
Unsuccessful recovery
If the recovery challenge (for example the link in the recovery email) is invalid or expired, the user will be HTTP 303 redirected to the Recovery UI.
When an invalid or expired challenge is used, Ory Kratos initializes a new Account Recovery flow automatically. This flow will always be a Browser-based flow because the challenge is completed by clicking a link!
The new Recovery Flow includes an error message such as the following:
- Browser UI
- Invalid Challenge
curl -H "Accept: application/json" -s \
'https://playground.projects.oryapis.com/self-service/recovery/flows?id=db163ca2-5e03-4961-8398-c537ed6c1cb9' | \
jq
{
"id": "db163ca2-5e03-4961-8398-c537ed6c1cb9",
"type": "browser",
"expires_at": "2021-04-28T13:54:51.358334636Z",
"issued_at": "2021-04-28T12:54:51.358334636Z",
"request_url": "https://playground.projects.oryapis.com/self-service/recovery?flow=199a2c74-08ca-4f22-b24c-3de3032682f8&token=XSpIPzkW4DgnJVhBG5Wjw3F123Z0ORmGrrq",
"ui": {
"action": "https://playground.projects.oryapis.com/self-service/recovery?flow=db163ca2-5e03-4961-8398-c537ed6c1cb9",
"method": "POST",
"messages": [
{
"id": 4060004,
"text": "The recovery token is invalid or has already been used. Please retry the flow.",
"type": "error",
"context": {}
}
],
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "TZ1eswBzgOQF6VC1PdPGR9vX9JfpRiwe6ZWAKpjNwgunyE3C+nF4sgKOB4hXBZTrvUBWWmMLXhXmdq/vIKWLnA==",
"required": true,
"disabled": false
},
"messages": null,
"meta": {}
},
{
"type": "input",
"group": "link",
"attributes": {
"name": "email",
"type": "email",
"required": true,
"disabled": false
},
"messages": null,
"meta": {}
},
{
"type": "input",
"group": "link",
"attributes": {
"name": "method",
"type": "submit",
"value": "link",
"disabled": false
},
"messages": null,
"meta": {
"label": {
"id": 1070005,
"text": "Submit",
"type": "info"
}
}
}
]
},
"state": "choose_method"
}
Please keep in mind that this part of the flow always involves a Browser!
Successful recovery
Completing account recovery always results in a HTTP 303 redirect with a Ory Kratos Login Session Cookie to the Settings UI with a Settings Flow prompting the user to update their password or credentials:
- Browser UI
- Update Credentials
curl -s -X GET \
-H "Authorization: Bearer $sessionToken" \
-H "Accept: application/json" \
'https://playground.projects.oryapis.com/self-service/settings/flows?id=9b527900-2199-4221-9252-7971b3362282' | \
jq
{
"id": "9b527900-2199-4221-9252-7971b3362282",
"type": "browser",
"expires_at": "2021-04-28T13:55:36.046466067Z",
"issued_at": "2021-04-28T12:55:36.046466067Z",
"request_url": "https://playground.projects.oryapis.com/self-service/recovery?flow=199a2c74-08ca-4f22-b24c-3de3032682f8&token=XSpIPzkW4DgnJVhBG5Wjw3FZ0ORmGrrq",
"ui": {
"action": "https://playground.projects.oryapis.com/self-service/settings?flow=9b527900-2199-4221-9252-7971b3362282",
"method": "POST",
"messages": [
{
"id": 1060001,
"text": "You successfully recovered your account. Please change your password or set up an alternative login method (for example social sign in) within the next 15.00 minutes.",
"type": "info",
"context": {
"privilegedSessionExpiresAt": "2021-04-28T13:10:36.050848814Z"
}
}
],
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "U3r/lgEfT8rA1Lg0Eeo06oGO8mX6T4TKoe/z7rbInhvYeacbRg0IW9zrqnpU1wmQJXKiekNzdLnypx5naHXoPg==",
"required": true,
"disabled": false
},
"messages": null,
"meta": {}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "traits.email",
"type": "email",
"value": "foo@ory.sh",
"disabled": false
},
"messages": null,
"meta": {
"label": {
"id": 1070002,
"text": "E-Mail",
"type": "info"
}
}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "traits.name.first",
"type": "text",
"value": "Foo",
"disabled": false
},
"messages": null,
"meta": {
"label": {
"id": 1070002,
"text": "First Name",
"type": "info"
}
}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "traits.name.last",
"type": "text",
"value": "Bar",
"disabled": false
},
"messages": null,
"meta": {
"label": {
"id": 1070002,
"text": "Last Name",
"type": "info"
}
}
},
{
"type": "input",
"group": "profile",
"attributes": {
"name": "method",
"type": "submit",
"value": "profile",
"disabled": false
},
"messages": null,
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"disabled": false
},
"messages": null,
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false
},
"messages": null,
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
}
}
]
},
"identity": {
"id": "b0a31645-0b5f-436a-b46d-03f15e8e89d9",
"schema_id": "default",
"schema_url": "https://playground.projects.oryapis.com/schemas/default",
"traits": {
"email": "foo@ory.sh",
"name": {
"first": "Foo",
"last": "Bar"
}
},
"verifiable_addresses": [
{
"id": "e3ea2845-0e07-4631-8955-9f4ecc5f73aa",
"value": "foo@ory.sh",
"verified": true,
"via": "email",
"status": "completed",
"verified_at": "2021-04-28T12:41:17.342366863Z"
}
],
"recovery_addresses": [
{
"id": "fd66ee7e-5751-4282-bf27-414290084c19",
"value": "foo@ory.sh",
"via": "email"
}
]
},
"state": "show_form"
}
Code examples for Node.js, React.js, Go, ...
The Recovery User Interface is a route (page / site) in your application (server, native app, single page app) that should render a recovery form.
In stark contrast to other Identity Systems, Ory Kratos doesn't render this HTML. Instead, you need to implement the HTML code in your application (for example Node.js + Express.js, Java, PHP, React.js, ...), which gives you extreme flexibility and customizability in your user interface flows and designs.
You will use the Recovery Flow JSON response to render the recovery form UI, which could looks as follows depending on your programming language and web framework:
- Browser UI
- Golang (API Flow)
- Express.js
- React.js
- React Native
- Recovery View
- Generic Form View
- Example Input Form Element
- Generic Form View
- Example Input Form Element