Skip to main content

Integration basics

Ory provides headless APIs for ease of integration. This ensures that Ory is compatible with many software stacks across different programming languages.

This document provides the fundamentals required to get started with custom user interface integration.

info

If you want to learn how to integrate custom UI for social sign-in, passwordless, or two-factor authentication, read Advanced integration.

Flows in Ory

Flows are an important element of Ory's self-service APIs and are used to accomplish a variety of goals, such as:

  • User login
  • User logout
  • User registration
  • Changing account settings
  • Account verification
  • Account recovery

Self-service flows are standardized, which means that they run the same basic operations, regardless of which specific flow is integrated.

Additionally, the self-service API is split to integrate with two types of applications - browser and native. The reason for the API split is the security measures necessary for the different application types. For example, the API sets a CSRF cookie for browser applications. For native applications, CSRF cookie isn't required and isn't set.

tip

Make sure to call the right self-service API endpoints for your application To ensure that the integration between your application's UI and Ory APIs works correctly.

Flow overview

A flow in Ory consists of five operations:

  1. Creating the flow for the specific goal and application type, for example user login in a native app.
  2. Using the flow data to render the UI.
  3. Submitting the flow with user data, such as username and password.
  4. Potentially handling errors, such as invalid user input and going back to step 2.
  5. Handling the successful submission.

Flow overview

Methods in flows

This table shows the methods available for each flow:

FlowMethods
loginpassword, oidc, totp, webauthn, lookup_secret
registrationpassword, oidc, webauthn
settingsprofile, password, oidc, lookup_secret, webauthn, totp
recoverylink, code
verificationlink, code
note

Flows can consist of multiple methods. For example, a login flow can have the password and oidc methods. Flows can be completed by only one method at a time.

Browser vs native apps

The type of the application that consumes Ory APIs through its UI directly determines which API endpoints must be called and, as a result, what security measures are applied.

  • Browser applications are apps with which users interact through their web browsers. Two common types of browser applications are server-side rendered apps (SSR) and single-page apps (SPA). Since the application is rendered in the browser, the communication between the user that interacts with the app and Ory must be secured. This is achieved by setting a CSRF cookie and token.

    info

    All browser apps must call Ory self-service APIs at /self-service/{flow_type}/browser

  • Native applications, such as Android mobile apps or desktop applications, are not rendered in the browser. Since the application is not rendered in the browser, the CSRF cookie and token are not necessary.

    info

    All native apps must call Ory self-service APIs at /self-service/{flow_type}/api

Create a flow

Before any data can be sent to Ory, a flow must be initialized. When a flow is created it uses the current project configuration and returns a flow object that can be used to render the UI. Additional security measures are also applied to the flow to secure subsequent requests to Ory.

You can create a flow by sending a GET request to the /self-service/{flow_type}/{browser/api} endpoint.

create-login-browser.sh
curl -X GET \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-c cookies.txt \
https://{project.slug}.projects.oryapis.com/self-service/login/browser
tip

To ensure that requests work correctly for the browser flow, use the -c flag to store the cookies in a file. On subsequent requests, you can use the -b flag to read the cookies from the file.

Server-side (browser) application

In browser applications that are server-side rendered, the flow can be initialized with a redirect to Ory executed in the browser which in turn will redirect the browser back to the configured application URL, for example https://myapp.com/login?flow=<uuid>.

<form method="GET" action="https://{project.slug}.projects.oryapis.com/self-service/login/browser">
<button type="submit">Login</button>
</form>

On successful login, Ory sets the CSRF cookie and issues a session cookie that can be used to authenticate subsequent requests to Ory.

Single-page application (SPA)

Single-page applications (SPA) can initialize the flow by calling the endpoint using AJAX or fetch instead of redirecting the browser. The response sets the necessary cookies and contains all UI nodes required to render the form. You must set withCredentials: true in axios or credentials: "include" in fetch for AJAX and fetch requests to properly set cookies.

If the flow is initialized through a browser redirect, the application URL must to be set so that Ory can reach your application.

Native application

Native applications must use the API flows which don't set any cookies. The response contains all data required to render the UI. On successful login, Ory issues a session token that can be used to authenticate subsequent requests to Ory.

Fetching existing flows

If the flow already exists, you can get the flow data using the flow ID through a GET request to /self-service/<login|registration|settings|verification|recovery>/flows?id=<flowID>. This is useful in cases where Ory has already initialized the flow and redirected to your application. In such a case, the flow ID can be extracted from the URL ?flow= query parameter.

When does a flow already exist and is available to fetch?

  1. Ory has initialized the flow and redirects to the UI, for example /login?flow=<uuidv4>.
  2. The request was completed by the user but an error occurred and you need to retrieve the flow data to display the error.
  3. The UI has 'stored' the flow ID in the URL so that page refreshes can fetch the existing flow data.
note

The flow ID has an expiration time so it's important to check the response status code when retrieving the flow data using an existing flow ID. It's up to your application to handle this error case and create a new flow.

Take note of the type of flow as it determines the endpoint used to retrieve the flow. For example, the login flow will only be available at the /self-service/login/flows?id=<flowId> endpoint.

This is an example of fetching an existing login flow:

tip

To ensure that requests work correctly for the browser flow, use the -c flag to store the cookies in a file. On subsequent requests, you can use the -b flag to read the cookies from the file.

get-login-browser.sh
curl -X GET \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-c cookies.txt \
"https://{project.slug}.projects.oryapis.com/self-service/login/flows?id=<your-flow-id>"

Submitting flows

Flows must be initialized before any user data, such as username and password, can be submitted. Depending on the application type, the submission will be handled differently. The request will always be a POST request to the /self-service/login/<browser|api>?flow=<uuid> endpoint. The flow data will contain the correct URL to be used in the form under the flow.ui.action key.

{
ui: {
action: "http://{project.slug}.projects.oryapis.com/self-service/login?flow=<uuid>",
method: "POST",
},
}
<form action="{flow.ui.action}" method="{flow.ui.method}">...</form>
submit-login-browser.sh
curl -X POST \
-H 'Content-Type: application/json'\
-H 'Accept: application/json' \
-d '{"method":"password","csrf_token":"your-csrf-token","identifier":"[email protected]","password":"verystrongpassword"}' \
-b cookies.txt \
-c cookies.txt \
"https://{project.slug}.projects.oryapis.com/self-service/login?flow=<your-flow-id>"
tip

To ensure that requests work correctly for the browser flow, use the -c flag to store the cookies in a file. On subsequent requests, you can use the -b flag to read the cookies from the file.

Server-side (browser) application

In server-side rendered browser applications, the submission is handled by a native form POST to the Ory project URL.

For example, login is a POST request to https://{project.slug}.projects.oryapis.com/self-service/login/browser?flow=<id>.

Single-page application (SPA)

In client-side rendered browser applications such as SPAs, the submission is handled in the background through AJAX or fetch using a POST request to the Ory project URL.

For example, login is a POST request to https://{project.slug}.projects.oryapis.com/self-service/login/browser?flow=<id>.

Native application

In native applications send a POST request to the Ory project URL. For example, login is a POST request to https://{project.slug}.projects.oryapis.com/self-service/login/api?flow=<id>.

Login flow

The login flow is used to authenticate users and can be completed using different methods: password, oidc, and webauthn. This flow also manages two-factor authentication (2FA) through the totp, webauthn, and lookup_secrets methods.

This section covers only the password method since it is the easiest to get started with.

Create login flow

The following code examples show how to create a login flow and render the user interface (where applicable).

tip

Remember to use the correct API endpoints for your application type.

create-login-browser.sh
curl -X GET \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-c cookies.txt \
https://{project.slug}.projects.oryapis.com/self-service/login/browser
create-login-browser.json
{
"id": "4253b0f4-a6d9-4b0a-ba31-00e5cc1c14aa",
"oauth2_login_challenge": null,
"type": "browser",
"expires_at": "2023-01-24T13:53:30.344286509Z",
"issued_at": "2023-01-24T13:23:30.344286509Z",
"request_url": "https://{project.slug}.projects.oryapis.com/self-service/login/browser",
"ui": {
"action": "https://{project.slug}.projects.oryapis.com/self-service/login?flow=4253b0f4-a6d9-4b0a-ba31-00e5cc1c14aa",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "24I7BFl9PNOueG3K8mn7+ejY8OXMWQL2rp7M8HUiou8Q4datR0QbL4nOuXrpLGLqkGoOIgOgZmi1fjPwIpHGaQ==",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "text",
"value": "",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "current-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010001,
"text": "Sign in",
"type": "info",
"context": {}
}
}
}
]
},
"created_at": "2023-01-24T13:23:30.436056Z",
"updated_at": "2023-01-24T13:23:30.436056Z",
"refresh": false,
"requested_aal": "aal1"
}
tip

To ensure that requests work correctly for the browser flow, use the -c flag to store the cookies in a file. On subsequent requests, you can use the -b flag to read the cookies from the file.

login.go
package frontend

import (
"context"
"fmt"
"os"

"github.com/ory/client-go"
)

type oryMiddleware struct {
ory *ory.APIClient
}

func init() {
cfg := client.NewConfiguration()
cfg.Servers = client.ServerConfigurations{
{URL: fmt.Sprintf("https://%s.projects.oryapis.com", os.Getenv("ORY_PROJECT_SLUG"))},
}

ory = client.NewAPIClient(cfg)
}

func CreateLogin(ctx context.Context) (*client.LoginFlow, error) {
flow, _, err := ory.FrontendApi.CreateNativeLoginFlow(ctx).Execute()
if err != nil {
return nil, err
}

return flow, nil
}

Get login flow

When a login flow already exists, you can retrieve it by sending a GET request that contains the flow ID to the /self-service/login/flows?id=<flow-id> endpoint.

The flow ID is usually stored in the ?flow= URL query parameter in your application. The following example shows how to get flow data using the flow ID.

get-login-browser.sh
curl -X GET \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-c cookies.txt \
"https://{project.slug}.projects.oryapis.com/self-service/login/flows?id=<your-flow-id>"
get-login-browser.json
{
"id": "a42a9cb9-e436-483b-9845-b8aa8516e2e9",
"oauth2_login_challenge": null,
"type": "browser",
"expires_at": "2023-01-25T14:34:55.335611Z",
"issued_at": "2023-01-25T14:04:55.335611Z",
"request_url": "https://{project.slug}.projects.oryapis.com/self-service/login/browser",
"ui": {
"action": "https://{project.slug}.projects.oryapis.com/self-service/login?flow=a42a9cb9-e436-483b-9845-b8aa8516e2e9",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "fEbwhQFcC/q5AkS9RsdqiX65Az7PTrC1tecZH4wNevAV7N/jSvVron06mYYs7y5HVedX/W2QbqGx3KvJpsvhNg==",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "text",
"value": "",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "current-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010001,
"text": "Sign in",
"type": "info",
"context": {}
}
}
}
]
},
"created_at": "2023-01-25T14:04:55.443614Z",
"updated_at": "2023-01-25T14:04:55.443614Z",
"refresh": false,
"requested_aal": "aal1"
}
tip

To ensure that requests work correctly for the browser flow, use the -c flag to store the cookies in a file. On subsequent requests, you can use the -b flag to read the cookies from the file.

login.go
package frontend

import (
"context"
"fmt"
"os"

"github.com/ory/client-go"
)

type oryMiddleware struct {
ory *ory.APIClient
}

func init() {
cfg := client.NewConfiguration()
cfg.Servers = client.ServerConfigurations{
{URL: fmt.Sprintf("https://%s.projects.oryapis.com", os.Getenv("ORY_PROJECT_SLUG"))},
}

ory = client.NewAPIClient(cfg)
}

func GetLogin(ctx context.Context, flowId string) (*client.LoginFlow, error) {
flow, _, err := ory.FrontendApi.GetLoginFlow(ctx).Id(flowId).Execute()
if err != nil {
return nil, err
}

return flow, nil
}

Submit login flow

The last step is to submit the login flow with the user's credentials. To do that, send a POST request to the /self-service/login endpoint.

For browser applications you must send all cookies and the CSRF token the request body. The CSRF token value is a hidden input field called csrf_token.

submit-login-browser.sh
curl -X POST \
-H 'Content-Type: application/json'\
-H 'Accept: application/json' \
-d '{"method":"password","csrf_token":"your-csrf-token","identifier":"[email protected]","password":"verystrongpassword"}' \
-b cookies.txt \
-c cookies.txt \
"https://{project.slug}.projects.oryapis.com/self-service/login?flow=<your-flow-id>"
submit-login-browser.json
{
"id": "a42a9cb9-e436-483b-9845-b8aa8516e2e9",
"oauth2_login_challenge": null,
"type": "browser",
"expires_at": "2023-01-25T14:34:55.335611Z",
"issued_at": "2023-01-25T14:04:55.335611Z",
"request_url": "https://{project.slug}.projects.oryapis.com/self-service/login/browser",
"ui": {
"action": "https://{project.slug}.projects.oryapis.com/self-service/login?flow=a42a9cb9-e436-483b-9845-b8aa8516e2e9",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "fEbwhQFcC/q5AkS9RsdqiX65Az7PTrC1tecZH4wNevAV7N/jSvVron06mYYs7y5HVedX/W2QbqGx3KvJpsvhNg==",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "identifier",
"type": "text",
"value": "",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070004,
"text": "ID",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "current-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1010001,
"text": "Sign in",
"type": "info",
"context": {}
}
}
}
]
},
"created_at": "2023-01-25T14:04:55.443614Z",
"updated_at": "2023-01-25T14:04:55.443614Z",
"refresh": false,
"requested_aal": "aal1"
}
tip

To ensure that requests work correctly for the browser flow, use the -c flag to store the cookies in a file. On subsequent requests, you can use the -b flag to read the cookies from the file.

login.go
package frontend

import (
"context"
"fmt"
"os"

"github.com/ory/client-go"
)

type oryMiddleware struct {
ory *ory.APIClient
}

func init() {
cfg := client.NewConfiguration()
cfg.Servers = client.ServerConfigurations{
{URL: fmt.Sprintf("https://%s.projects.oryapis.com", os.Getenv("ORY_PROJECT_SLUG"))},
}

ory = client.NewAPIClient(cfg)
}

func SubmitLogin(ctx context.Context, flowId string, body client.UpdateLoginFlowBody) (*client.SuccessfulNativeLogin, error) {
flow, _, err := ory.FrontendApi.UpdateLoginFlow(ctx).Flow(flowId).UpdateLoginFlowBody(body).Execute()
if err != nil {
return nil, err
}

return flow, nil
}

Registration flow

The registration flow allows users to register in your application. This flow is highly customizable and allows you to define custom fields and validation rules through the identity schema

This flow also consists of a number of methods such as password, oidc and webauthn.

This section covers only the password method since it is the easiest to get started with.

Create registration flow

The following code examples show how to create a registration flow and render the user interface (where applicable).

tip

Remember to use the correct API endpoints for your application type.

create-registration-browser.sh
curl -X GET \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-c cookie.txt \
"https://{project.slug}.projects.oryapis.com/self-service/registration/browser"
create-registration-browser.json
{
"id": "a5b2fc09-5b1c-43e7-a43e-27b30eb0d8ab",
"oauth2_login_challenge": null,
"type": "browser",
"expires_at": "2023-01-25T16:20:08.959833418Z",
"issued_at": "2023-01-25T15:50:08.959833418Z",
"request_url": "https://{project.slug}.projects.oryapis.com/self-service/registration/browser",
"ui": {
"action": "https://{project.slug}.projects.oryapis.com/self-service/registration?flow=a5b2fc09-5b1c-43e7-a43e-27b30eb0d8ab",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "QOWlHpGr00Z2AS77UQ/v8g8AcI5IJFsG3fXhUws/mC6il6NNohD0k+vczrIXiWI/kfjQK3plliEafhjclwMJeQ==",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "traits.email",
"type": "email",
"required": true,
"autocomplete": "email",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070002,
"text": "E-Mail",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "password",
"type": "password",
"required": true,
"autocomplete": "new-password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070001,
"text": "Password",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "traits.tos",
"type": "checkbox",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070002,
"text": "Accept Tos",
"type": "info"
}
}
},
{
"type": "input",
"group": "password",
"attributes": {
"name": "method",
"type": "submit",
"value": "password",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1040001,
"text": "Sign up",
"type": "info",
"context": {}
}
}
}
]
}
}
tip

To ensure that requests work correctly for the browser flow, use the -c flag to store the cookies in a file. On subsequent requests, you can use the -b flag to read the cookies from the file.

registration.go
package frontend

import (
"context"
"fmt"
"os"

"github.com/ory/client-go"
)

type oryMiddleware struct {
ory *ory.APIClient
}

func init() {
cfg := client.NewConfiguration()
cfg.Servers = client.ServerConfigurations{
{URL: fmt.Sprintf("https://%s.projects.oryapis.com", os.Getenv("ORY_PROJECT_SLUG"))},
}

ory = client.NewAPIClient(cfg)
}

func CreateRegisteration(ctx context.Context) (*client.RegistrationFlow, error) {
flow, _, err := ory.FrontendApi.CreateNativeRegistrationFlow(ctx).Execute()
if err != nil {
return nil, err
}

return flow, nil
}

Get registration flow

When a registration flow already exists, you can retrieve it by sending a GET request that contains the flow ID to the /self-service/registration/flows?id=<flow-id> endpoint.

The flow ID is usually stored in the ?flow= URL query parameter in your application. The following example shows how to get flow data using the flow ID.

create-registration-browser.sh
curl -X GET \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-c cookies.txt \
"https://{project.slug}.projects.oryapis.com/self-service/registration/flows?id=<your-flow-id>"
create-registration-browser.json
{
"id": "3ddc7a62-9762-4e00-affd-ef9bac63125c",
"oauth2_login_challenge": null,
"type": "browser",
"expires_at": "2023-01-25T16:23:32.59678Z",
"issued_at": "2023-01-25T15:53:32.59678Z",
"request_url": "https://{project.slug}.projects.oryapis.com/self-service/registration/browser",
"ui": {
"action": "https://{project.slug}.projects.oryapis.com/self-service/registration?flow=3ddc7a62-9762-4e00-affd-ef9bac63125c",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "olICl0SM+24Yq8lVBvdftZFnhx0QI/kDSain+oY+UjzL+C3xDyWbNtyTFG5s3xt7ujnT3rL9JxdNkxUsrPjJ+g==",
"required": true