Skip to main content
Version: Next

Self-Service Flows

Ory Kratos implements flows that users perform themselves as opposed to administrative intervention. Facebook and Google both provide self-service registration and profile management features as you are able to make changes to your profile and sign up yourself.

Most believe that user management systems are easy to implement because picking the right password hashing algorithm and sending an account verification code is a solvable challenge. The real complexity however hides in the details and attack vectors of self-service flows. Most data leaks happen because someone is able to exploit

  • registration: with attack vectors such as account enumeration (), ...;
  • login: phishing, account enumeration, leaked password databases, brute-force, ...;
  • user settings: account enumeration, account takeover, ...;
  • account recovery: social engineering attacks, account takeover, spoofing, and so on.

There are also many other areas you need to think about, such as:

  • Mobile, Smart TV, ... authentication
  • Linking and unlinking social profiles (e.g. "Sign in with Google" or "Connect with Google") to existing accounts

Ory Kratos applies best practices established by experts (National Institute of Sciences NIST, Internet Engineering Task Force IETF, Microsoft Research, Google Research, Troy Hunt, ...) and implements the following flows:

Some flows break down into "flow methods" which implement some of the flow's business logic:

  • The password method implements the login and registration with "email or/and username and password" method, and "change your password" user settings method.
  • The oidc (OpenID Connect, OAuth2, Social Sign In) method implements "Sign in with ..." login and registration method and "un/link another social account" user settings method.
  • The profile method implements the "update your profile", "change your first/last name, ..." user settings method).
  • The link method implements the "click this link to reset your password" account recovery method.

Some flows additionally implement the ability to run hooks which allow users to be immediately signed in after registration, notify another system on successful registration (e.g. Mailchimp), and so on.

Performing Login, Registration, Settings, ... Flows#

There are two flow types supported in Ory Kratos:

  • Flows where the user sits in front of the Browser (e.g. website, single page app, ...)
  • Flows where API interaction is required (e.g. mobile app, Smart TV, ...)

All Self-Service Flows (User Login, User Registration, Profile Management, Account Recovery, Email or Phone verification) support these two flow types and use the same data models but do use different API endpoints.

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.

Browser Flows#

Browser-based flows make use of three core HTTP technologies:

  • HTTP Redirects
  • HTTP POST (application/x-www-urlencoded) and REST GET requests.
  • HTTP Cookies to prevent CSRF and Session Hijaking attack vectors.

The browser flow is the easiest and most secure to set up and integrated with. Ory Kratos takes care of all required session and CSRF cookies and ensures that all security requirements are fulfilled.

The browser flow has three stages:

  • Initialization and redirect to UI;
  • Form rendering;
  • Form submission and payload validation.
note

The payloads, examples, ports, and IPs shown here are the ones used if you run the Quickstart. If you have not checked it out yet, please do so before reading this document.

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.

Initialization and Redirect to UI#

First, the Browser opens the flow's initialization endpoint (e.g./self-service/login/browser, /self-service/registration/browser, ...):

curl -s -v -X GET \
-H "Accept: text/html" \
http://127.0.0.1:4433/self-service/login/browser
> GET /self-service/login/browser HTTP/1.1
> Host: 127.0.0.1:4433
> User-Agent: curl/7.64.1
> Accept: text/html
>
< HTTP/1.1 302 Found
< Cache-Control: 0
< Content-Type: text/html; charset=utf-8
< Location: http://127.0.0.1:4455/auth/login?flow=df607aa1-d555-4b2a-b3e4-0f5a1d2fe6f3
< Set-Cookie: csrf_token=y4Ocu6V83BapwJwbPw/pnlRHHw40DZbjq5iuDrxl0Ds=; Path=/; Domain=127.0.0.1; Max-Age=31536000; HttpOnly
<
<a href="http://127.0.0.1:4455/auth/login?flow=df607aa1-d555-4b2a-b3e4-0f5a1d2fe6f3">Found</a>.

The initialization endpoint creates a flow object and stores it in the database. The flow object has an ID and contains additional information about the flow such as the login methods (e.g. "username/password" and "Sign in with Google") and their form data.

Once stored, the Browser is HTTP 302 redirected to the flow's configured UI URL (e.g. selfservice.flows.login.ui_url), appending the flow ID as the flow URL Query Parameter. Also included will be might be some cookies such as Anti-CSRF cookies.

The response will look along the lines of:

$ curl -s -v -X GET \
-H "Accept: text/html" \
http://127.0.0.1:4433/self-service/login/browser
# Response Headers
< HTTP/1.1 302 Found
< Cache-Control: 0
< Content-Type: text/html; charset=utf-8
< Location: http://127.0.0.1:4455/auth/login?flow=f6209031-38d5-48bc-b6a5-118e8a24d1fe
< Set-Cookie: csrf_token=qhMCbVX6iDyw1x/701zlINVFFqfkZrq1t/Z27Z1uFDw=; Path=/.ory/kratos/public/; Domain=127.0.0.1; Max-Age=31536000; HttpOnly
< Vary: Cookie
< Date: Mon, 17 Aug 2020 13:54:52 GMT
< Content-Length: 97

Form Rendering#

The Browser opens the URL (here http://127.0.0.1:4455/auth/login?flow=f6209031-38d5-48bc-b6a5-118e8a24d1fe) which renders the HTML form which for example shows the "username and password" field, the "Update your email address" field, or other flow forms. This HTML form is rendered be the Self-Service UI which you fully control.

The endpoint responsible for the UI URL uses the flow URL Query Parameter (here ...?flow=f6209031-38d5-48bc-b6a5-118e8a24d1fe) to call the flow information endpoint (e.g. http://127.0.0.1:4434/self-service/login/flows?id=f6209031-38d5-48bc-b6a5-118e8a24d1fe) and fetch the flow data - so for example the login form and any validation errors. This endpoint is available at both Ory Kratos's Public and Admin Endpoints. For example, the Self-Service UI responsible for rendering the Login HTML Form would make a request along the lines of:

# The endpoint uses Ory Kratos' REST API to fetch information about the request
$ curl -s -X GET \
-H "Accept: application/json" \
"http://127.0.0.1:4434/self-service/login/flows?id=f6209031-38d5-48bc-b6a5-118e8a24d1fe" | jq

The result includes login methods, their fields, and additional information about the flow:

{
"id": "f6209031-38d5-48bc-b6a5-118e8a24d1fe",
"type": "browser",
"expires_at": "2021-04-28T09:00:57.10254633Z",
"issued_at": "2021-04-28T08:50:57.10254633Z",
"request_url": "http://127.0.0.1:4433/self-service/login/browser",
"ui": {
"action": "http://127.0.0.1:4433/self-service/login?flow=f6209031-38d5-48bc-b6a5-118e8a24d1fe",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "pUhD8lmSNNRnX/tuy6yAM317SfdiPOwetYUKnx23iTHnySErkBGJwaF1t3cEqNtxF3uC7W5A/dwP5l5VDYFRSA==",
"required": true,
"disabled": false
},
"messages": null,
"meta": {}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password_identifier",
"type": "text",
"value": "",
"required": true,
"disabled": false
},
"messages": null,
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"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": 1010001,
"text": "Sign in",
"type": "info",
"context": {}
}
}
}
]
},
"forced": false
}

For more details, check out the individual flow documentation.

The flow UI then renders the given methods. For the example above, a suitable HTML Form would look along the lines of:

<form
action="http://127.0.0.1:4433/self-service/login?flow=f6209031-38d5-48bc-b6a5-118e8a24d1fe"
method="POST"
>
<input
name="csrf_token"
type="hidden"
value="pUhD8lmSNNRnX/tuy6yAM317SfdiPOwetYUKnx23iTHnySErkBGJwaF1t3cEqNtxF3uC7W5A/dwP5l5VDYFRSA=="
/>
<fieldset>
<label>
<input name="password_identifier" type="text" value="" placeholder="ID" />
<span>ID</span>
</label>
</fieldset>
<fieldset>
<label>
<input name="password" type="password" value="" placeholder="Password" />
<span>Password</span>
</label>
</fieldset>
<button name="method" type="submit" value="password">Sign in</button>
</form>

Form Submission and Payload Validation#

The user completes the flow by submitting the form. The form action always points to Ory Kratos directly. The business logic for the flow method will then validate the form payload and return to the UI URL on validation errors. The UI then fetches the information about the flow again

# The endpoint uses Ory Kratos' REST API to fetch information about the request
$ curl -s -X GET \
-H "Accept: application/json" \
"http://127.0.0.1:4433/self-service/login/flows?id=f6209031-38d5-48bc-b6a5-118e8a24d1fe" | jq

which now includes validation errors and other potential messages:

{
id: 'f6209031-38d5-48bc-b6a5-118e8a24d1fe',
type: 'browser',
expires_at: '2021-04-28T09:00:57.10254633Z',
issued_at: '2021-04-28T08:50:57.10254633Z',
request_url: 'http://127.0.0.1:4433/self-service/login/browser',
ui: {
action: 'http://127.0.0.1:4433/self-service/login?flow=f6209031-38d5-48bc-b6a5-118e8a24d1fe',
method: 'POST',
nodes: [
// ...
{
type: 'input',
group: 'password',
attributes: {
name: 'password_identifier',
type: 'text',
value: '',
required: true,
disabled: false
},
messages: [
{
id: 4000001,
text: 'length must be >= 1, but got 0',
type: 'error'
}
],
meta: {
label: {
id: 1070004,
text: 'ID',
type: 'info'
}
}
},
{
type: 'input',
group: 'password',
attributes: {
name: 'password',
type: 'password',
required: true,
disabled: false
},
messages: [
{
id: 4000001,
text: 'length must be >= 1, but got 0',
type: 'error'
}
],
meta: {
label: {
id: 1070001,
text: 'Password',
type: 'info'
}
}
}
// ...
]
},
forced: false
}

If a system error (e.g. broken configuration file) occurs, the browser is redirected to the Error UI.

If the form payload is valid, the flow completes with a success. The result here depends on the flow itself - the login flow for example redirects the user to a specified redirect URL and sets a session cookie. The registration flow also redirects to a specified redirect URL but only creates the user in the database and might issue a session cookie if configured to do so.

API Flows#

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.

API flows have three stages:

  • Initialization without redirect
  • Form rendering using e.g. native iOS, Android, ... components
  • Form submission as application/json and payload validation

The high-level sequence diagram for API flows looks as follows:

Initialization#

The client (e.g. mobile application) makes a request to the flow's initialization endpoint (e.g./self-service/login/api, /self-service/registration/api, ...):

$ curl -s -X GET \
-H "Accept: application/json" \
http://127.0.0.1:4433/self-service/login/api | jq
{
"id": "cc24b30b-c0a8-46f5-a929-304371e92e46",
"type": "api",
"expires_at": "2021-04-28T10:02:34.102190388Z",
"issued_at": "2021-04-28T09:52:34.102190388Z",
"request_url": "http://127.0.0.1:4433/self-service/login/api",
"ui": {
"action": "http://127.0.0.1:4433/self-service/login?flow=cc24b30b-c0a8-46f5-a929-304371e92e46",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "",
"required": true,
"disabled": false
},
"messages": null,
"meta": {}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password_identifier",
"type": "text",
"value": "",
"required": true,
"disabled": false
},
"messages": null,
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"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": 1010001,
"text": "Sign in",
"type": "info",
"context": {}
}
}
}
]
},
"forced": false
}

The initialization endpoint creates a flow object and stores it in the database. The flow object has an ID and contains additional information about the flow such as the login methods (e.g. "username/password" and "Sign in with Google") and their form data.

Form Rendering#

While the Browser flow redirects the client and uses cookies to protect against CSRF and session hijacking attacks, the API flow answers with a fresh flow formatted as application/json right away. The client would then use that information to render the UI. In React Native, this may look like the following snippet:

import React, { useState, useEffect } from 'react'
import { Text, TextInput, View } from 'react-native'
import { PublicApi, LoginFlow } from '@ory/kratos-client'
const kratos = new PublicApi('http://127.0.0.1:4455/.ory/kratos/public')
export default function Login () {
const [flow, setFlow] = useState<LoginFlow | undefined>(undefined)
useEffect(() => {
kratos.initializeSelfServiceAPILoginFlow().then(({ data: flow }) => {
setFlow(flow)
})
}, [])
if (!flow) {
return null
}
// This is a non-functional, code example. It is missing state management and so on.
// The idea is to show how initializing such a flow would look like in an API scenario.
return (
<View>
<Text>Login</Text>
{flow.ui.nodes.map((node) => {
switch (node.type) {
case 'input':
return <TextInput value={node.attributes.value} /* placeholder, name, ... *//>
default:
// ...
}
})}
</>
)
}

If needed, the client can re-request the flow using the same HTTP Request as the browser flow at the Public API endpoint:

curl -s -X GET \
-H "Accept: application/json" \
"http://127.0.0.1:4433/self-service/login/flows?id=41ebf1e9-79f5-4b97-b643-04bfc405f8ad" | jq
{
"id": "41ebf1e9-79f5-4b97-b643-04bfc405f8ad",
"type": "api",
# ...

Form Submission and Payload Validation#

The request is then completed by sending the form formatted as application/json to the action endpoint

flow=$(curl -s -X GET -H "Accept: application/json" "http://127.0.0.1:4433/self-service/login/api")
actionUrl=$(echo $flow | jq -r '.ui.action')
echo $actionUrl
# http://127.0.0.1:4433/self-service/login?flow=6394ffa4-235f-4c1a-a200-e62f89862015
curl -s -X POST -H "Accept: application/json" -H "Content-Type: application/json" \
-d '{"password_identifier": "i-do-not-exist@user.org", "password": "the-wrong-password", "method": "password"}' \
"$actionUrl" | jq

which in this case fails with a 400 Bad Request error and the following payload:

{
"id": "6394ffa4-235f-4c1a-a200-e62f89862015",
"type": "api",
"expires_at": "2021-04-28T09:12:48.462374732Z",
"issued_at": "2021-04-28T09:02:48.462374732Z",
"request_url": "http://127.0.0.1:4433/self-service/login/api",
"ui": {
"action": "http://127.0.0.1:4433/self-service/login?flow=6394ffa4-235f-4c1a-a200-e62f89862015",
"method": "POST",
"messages": [
{
"id": 4000006,
"text": "The provided credentials are invalid, check for spelling mistakes in your password or username, email address, or phone number.",
"type": "error",
"context": {}
}
],
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "",
"required": true,
"disabled": false
},
"messages": null,
"meta": {}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password_identifier",
"type": "text",
"value": "i-do-not-exist@user.org",
"required": true,
"disabled": false
},
"messages": null,
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"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": 1010001,
"text": "Sign in",
"type": "info",
"context": {}
}
}
}
]
},
"forced": false
}

On success, that endpoint would typically return a HTTP 200 Status OK response with the success application/json response payload in the body.

Last updated on by aeneasr