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@example.com","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@example.com","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,
"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 GetRegistration(ctx context.Context, flowId string) (*client.RegistrationFlow, error) {
flow, _, err := ory.FrontendApi.GetRegistrationFlow(ctx).Id(flowId).Execute()
if err != nil {
return nil, err
}

return flow, nil
}

Submit registration 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/registration endpoint.

The registration payload depends on the identity schema you use. Take note of the flow.ui.nodes property in the response payload when creating or getting the registration flow. This property contains the registration form fields that you need to provide in the POST request payload. To learn more about customizing your identity schema please refer to Identity model documentation.

For more complex use cases you can pass additional data to the flow using the optional transient_payload field. This data gets forwarded to webhooks without being persisted by Ory like identity traits do. To learn how to configure and use a webhook, please have a look at the Webhooks documentation.

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

Examples in this section are based on the following identity schema snippet and submit the required traits traits.email and traits.tos. Also a sample transient_payload was added to demonstrate its usage.

partial-identity-schema.json
{
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "E-Mail",
"maxLength": 320
},
"tos": {
"type": "boolean",
"title": "Accept Tos"
}
}
}
}
}

Take note of the payload. This example is based on the identity schema snippet and has two required traits, traits.email and traits.tos.

submit-registration-browser.sh
curl -X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"method":"password","csrf_token":"your-csrf-token","traits.email":"email@example.com","password":"verystrongpassword","traits.tos":"true","transient_payload.consents":"newsletter,usage_stats"}' \
-b cookies.txt \
-c cookies.txt \
"https://{project.slug}.projects.oryapis.com/self-service/registration?flow=<your-flow-id>"
submit-registration-browser.json
{
"session": {
"id": "f8cbd4d3-c7b9-4a9b-a476-688c98f1301e",
"active": true,
"expires_at": "2023-01-28T15:57:56.806972553Z",
"authenticated_at": "2023-01-25T15:57:56.870901875Z",
"authenticator_assurance_level": "aal1",
"authentication_methods": [
{
"method": "password",
"aal": "aal1",
"completed_at": "2023-01-25T15:57:56.8070781Z"
}
],
"issued_at": "2023-01-25T15:57:56.806972553Z",
"identity": {
"id": "a1a84ea5-6fe7-49ae-a537-bbfc296c8c2e",
"schema_id": "dc9e56067fce00e628f70621eed24c5a2c9253bfebb14db405a05fe082a00bc638e6f171a05f829db95e4830f79d5e98004d8b229dad410a90122044dbd2fba0",
"schema_url": "https://{project.slug}.projects.oryapis.com/schemas/ZGM5ZTU2MDY3ZmNlMDBlNjI4ZjcwNjIxZWVkMjRjNWEyYzkyNTNiZmViYjE0ZGI0MDVhMDVmZTA4MmEwMGJjNjM4ZTZmMTcxYTA1ZjgyOWRiOTVlNDgzMGY3OWQ1ZTk4MDA0ZDhiMjI5ZGFkNDEwYTkwMTIyMDQ0ZGJkMmZiYTA",
"state": "active",
"state_changed_at": "2023-01-25T15:57:56.742862067Z",
"traits": {
"email": "email@example.com",
"tos": true
},
"verifiable_addresses": [
{
"id": "ebb8c0c9-8444-4727-9762-cdf01f1736b6",
"value": "email@example.com",
"verified": false,
"via": "email",
"status": "sent",
"created_at": "2023-01-25T15:57:56.75917Z",
"updated_at": "2023-01-25T15:57:56.75917Z"
}
],
"recovery_addresses": [
{
"id": "84139274-9161-46df-8656-2dac3df97a9a",
"value": "email@example.com",
"via": "email",
"created_at": "2023-01-25T15:57:56.767361Z",
"updated_at": "2023-01-25T15:57:56.767361Z"
}
],
"metadata_public": null,
"created_at": "2023-01-25T15:57:56.753273Z",
"updated_at": "2023-01-25T15:57:56.753273Z"
},
"devices": [
{
"id": "4b128690-aefd-4c4e-b69e-653d4026adda",
"ip_address": "",
"user_agent": "curl/7.81.0",
"location": "Munich, DE"
}
]
},
"identity": {
"id": "a1a84ea5-6fe7-49ae-a537-bbfc296c8c2e",
"schema_id": "dc9e56067fce00e628f70621eed24c5a2c9253bfebb14db405a05fe082a00bc638e6f171a05f829db95e4830f79d5e98004d8b229dad410a90122044dbd2fba0",
"schema_url": "https://{project.slug}.projects.oryapis.com/schemas/ZGM5ZTU2MDY3ZmNlMDBlNjI4ZjcwNjIxZWVkMjRjNWEyYzkyNTNiZmViYjE0ZGI0MDVhMDVmZTA4MmEwMGJjNjM4ZTZmMTcxYTA1ZjgyOWRiOTVlNDgzMGY3OWQ1ZTk4MDA0ZDhiMjI5ZGFkNDEwYTkwMTIyMDQ0ZGJkMmZiYTA",
"state": "active",
"state_changed_at": "2023-01-25T15:57:56.742862067Z",
"traits": {
"email": "email@example.com",
"tos": true
},
"verifiable_addresses": [
{
"id": "ebb8c0c9-8444-4727-9762-cdf01f1736b6",
"value": "email@example.com",
"verified": false,
"via": "email",
"status": "sent",
"created_at": "2023-01-25T15:57:56.75917Z",
"updated_at": "2023-01-25T15:57:56.75917Z"
}
],
"recovery_addresses": [
{
"id": "84139274-9161-46df-8656-2dac3df97a9a",
"value": "email@example.com",
"via": "email",
"created_at": "2023-01-25T15:57:56.767361Z",
"updated_at": "2023-01-25T15:57:56.767361Z"
}
],
"metadata_public": null,
"created_at": "2023-01-25T15:57:56.753273Z",
"updated_at": "2023-01-25T15:57:56.753273Z"
}
}
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 SubmitRegistration(ctx context.Context, flowId string, body client.UpdateRegistrationFlowBody) (*client.SuccessfulNativeRegistration, error) {
flow, _, err := ory.FrontendApi.UpdateRegistrationFlow(ctx).Flow(flowId).UpdateRegistrationFlowBody(body).Execute()
if err != nil {
return nil, err
}

return flow, nil
}

Recovery

The recovery flow allows users to recover access to their accounts and is used in conjunction with the settings flow. When a recovery flow is submitted successfully, a session is issued to the application. This allows the user to reset their password and update their profile information on the settings page. Browser applications are automatically redirected to the settings page, while native applications must take the user to the settings page.

The recovery flow has two methods: link and code. The link method requires the user to open the recovery link from their email client using a browser on the same device. This makes it difficult as the user might be switching between devices to recover their credentials.

The code method is more user friendly since the code can be entered on the same device where the user requested the code from. This is the preferred method as spam filters and email clients can invalidating recovery links.

note

Only one recovery method can be enabled at a time. The code method is enabled by default in all new Ory Network projects.

Create recovery flow

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

tip

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

submit-registration-browser.sh
curl -H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-c cookies.txt \
"https://{project.slug}.projects.oryapis.com/self-service/recovery/browser"
submit-registration-browser.json
{
"id": "051a673d-da0d-46c3-90a8-925135bd7570",
"type": "browser",
"expires_at": "2023-02-08T16:05:30.05865234Z",
"issued_at": "2023-02-08T15:35:30.05865234Z",
"request_url": "https://{project.slug}.projects.oryapis.com/self-service/recovery/browser",
"active": "code",
"ui": {
"action": "https://{project.slug}.projects.oryapis.com/self-service/recovery?flow=051a673d-da0d-46c3-90a8-925135bd7570",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "Bo8DUDrRA5sB3ZVWi+hesBXCc+0VAPlYpHPEYbSZI9WVuGRW/r3PU3iNaVIjG4mRKNC0zNhlo6dx+SJB8NQt+Q==",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "code",
"attributes": {
"name": "email",
"type": "email",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070007,
"text": "Email",
"type": "info"
}
}
},
{
"type": "input",
"group": "code",
"attributes": {
"name": "method",
"type": "submit",
"value": "code",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070005,
"text": "Submit",
"type": "info"
}
}
}
]
},
"state": "choose_method"
}
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.

recovery.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 CreateRecovery(ctx context.Context) (*client.RecoveryFlow, error) {
flow, _, err := ory.FrontendApi.CreateNativeRecoveryFlow(ctx).Execute()
if err != nil {
return nil, err
}

return flow, nil
}

Get recovery 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/recovery/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-recovery-browser.sh
curl -H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-c cookies.txt \
-b cookies.txt \
"https://{project.slug}.projects.oryapis.com/self-service/recovery/flows?id=<your-flow-id>"
get-recovery-browser.json
{
"id": "fb395045-429b-406b-b7cc-cd878b2d453f",
"type": "browser",
"expires_at": "2023-02-08T16:47:37.742765Z",
"issued_at": "2023-02-08T16:17:37.742765Z",
"request_url": "https://{project.slug}.projects.oryapis.com/self-service/recovery/browser",
"active": "code",
"ui": {
"action": "https://{project.slug}.projects.oryapis.com/self-service/recovery?flow=fb395045-429b-406b-b7cc-cd878b2d453f",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "mNBAW/7SFWLH+G/goy7nBayFlEiDBXjN+55qvL/yfHi7/kRpqEhpD4GBEjhXc2OdenuLCIBje3dsHUqvls+wOw==",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "code",
"attributes": {
"name": "email",
"type": "email",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070007,
"text": "Email",
"type": "info"
}
}
},
{
"type": "input",
"group": "code",
"attributes": {
"name": "method",
"type": "submit",
"value": "code",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070005,
"text": "Submit",
"type": "info"
}
}
}
]
},
"state": "choose_method"
}
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.

recovery.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 GetRecovery(ctx context.Context, flowId string) (*client.RecoveryFlow, error) {
flow, _, err := ory.FrontendApi.GetRecoveryFlow(ctx).Id(flowId).Execute()
if err != nil {
return nil, err
}

return flow, nil
}

Submit recovery flow

The last step is to submit the recovery flow with the user's email. To do that, send a POST request to the /self-service/recovery 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.

The recovery flow can have a second submit step if the recovery method is set to code. In such a case, the recovery flow shows a field to submit the received code the user gets after they submit their email.

submit-recovery-browser.sh
curl -X POST -H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"method":"code","email":"email@example.com","csrf_token":"your-csrf-token"}' \
-b cookies.txt \
"https://{project.slug}.projects.oryapis.com/self-service/recovery?flow=<your-flow-id>"
submit-recovery-browser.json
{
"id": "292cb0d4-7e69-4ecb-8245-294dde4f0836",
"type": "browser",
"expires_at": "2023-02-08T17:04:55.818765Z",
"issued_at": "2023-02-08T16:34:55.818765Z",
"request_url": "https://{project.slug}.projects.oryapis.com/self-service/recovery/browser",
"active": "code",
"ui": {
"action": "https://{project.slug}.projects.oryapis.com/self-service/recovery?flow=292cb0d4-7e69-4ecb-8245-294dde4f0836",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "ToYpvWDs+EBlfVwCGfFh4NFKdLdPfC41qo5tc2Wl4lnLiZQp1NgX2e9KyxfeHK21tw0xTF4QqcaGfqRTXBQRFQ==",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "code",
"attributes": {
"name": "code",
"type": "text",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070006,
"text": "Verify code",
"type": "info"
}
}
},
{
"type": "input",
"group": "code",
"attributes": {
"name": "method",
"type": "hidden",
"value": "code",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "code",
"attributes": {
"name": "method",
"type": "submit",
"value": "code",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070005,
"text": "Submit",
"type": "info"
}
}
},
{
"type": "input",
"group": "code",
"attributes": {
"name": "email",
"type": "submit",
"value": "email@example.com",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070007,
"text": "Resend code",
"type": "info"
}
}
}
],
"messages": [
{
"id": 1060003,
"text": "An email containing a recovery code has been sent to the email address you provided.",
"type": "info",
"context": {}
}
]
},
"state": "sent_email"
}
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.

recovery.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 SubmitRecovery(ctx context.Context, flowId string, body client.UpdateRecoveryFlowBody) (*client.RecoveryFlow, error) {
flow, _, err := ory.FrontendApi.UpdateRecoveryFlow(ctx).Flow(flowId).UpdateRecoveryFlowBody(body).Execute()
if err != nil {
return nil, err
}

return flow, nil
}

Verification flow

The verification flow is used to verify the user's account through their email address.

The flow has two methods: link and code. The link method requires the user to open the verification link from their email client using a browser on the same device. This makes it difficult as the user might be switching between devices to verify their account.

The code method is more user friendly since the code can be entered on the same device where the user requested the code from. This is the preferred method as spam filters and email clients can invalidating verification links.

Once the verification flow is submitted successfully, the session has the identity.verifiable_addresses[0].verified: true value.

note

Only one verification method can be enabled at a time. The code method is enabled by default in all new Ory Network projects.

Create verification flow

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

tip

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

create-verification-browser.sh
curl -H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-c cookies.txt \
https://{project.slug}.projects.oryapis.com/self-service/verification/browser
create-verification-browser.json
{
"id": "7b500ad3-f5d0-46ae-acb7-d39eda5c60d8",
"type": "browser",
"expires_at": "2023-02-08T17:34:35.934427073Z",
"issued_at": "2023-02-08T17:04:35.934427073Z",
"request_url": "https://{project.slug}.projects.oryapis.com/self-service/verification/browser",
"active": "code",
"ui": {
"action": "https://{project.slug}.projects.oryapis.com/self-service/verification?flow=7b500ad3-f5d0-46ae-acb7-d39eda5c60d8",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "d872Ev0U6xLze+Z1cuN3Z1OirIEyY6XjxihYq4jWZ2O+W6AmKRltfYgGBzN8FS0n2/PvYTlvDH76aziU0D3GEQ==",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "code",
"attributes": {
"name": "email",
"type": "email",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070007,
"text": "Email",
"type": "info"
}
}
},
{
"type": "input",
"group": "code",
"attributes": {
"name": "method",
"type": "submit",
"value": "code",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070005,
"text": "Submit",
"type": "info"
}
}
}
]
},
"state": "choose_method"
}
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.

verification.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 CreateVerification(ctx context.Context) (*client.VerificationFlow, error) {
flow, _, err := ory.FrontendApi.CreateNativeVerificationFlow(ctx).Execute()
if err != nil {
return nil, err
}

return flow, nil
}

Get verification flow

When a verification flow already exists, you can retrieve it by sending a GET request that contains the flow ID to the /self-service/verification/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-verification-browser.sh
curl -H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-b cookies.txt \
-c cookies.txt \
"https://{project.slug}.projects.oryapis.com/self-service/verification/flows?id=<your-flow-id>"
get-verification-browser.json
{
"id": "7b500ad3-f5d0-46ae-acb7-d39eda5c60d8",
"type": "browser",
"expires_at": "2023-02-08T17:34:35.934427Z",
"issued_at": "2023-02-08T17:04:35.934427Z",
"request_url": "https://{project.slug}.projects.oryapis.com/self-service/verification/browser",
"active": "code",
"ui": {
"action": "https://{project.slug}.projects.oryapis.com/self-service/verification?flow=7b500ad3-f5d0-46ae-acb7-d39eda5c60d8",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "d872Ev0U6xLze+Z1cuN3Z1OirIEyY6XjxihYq4jWZ2O+W6AmKRltfYgGBzN8FS0n2/PvYTlvDH76aziU0D3GEQ==",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "code",
"attributes": {
"name": "email",
"type": "email",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070007,
"text": "Email",
"type": "info"
}
}
},
{
"type": "input",
"group": "code",
"attributes": {
"name": "method",
"type": "submit",
"value": "code",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070005,
"text": "Submit",
"type": "info"
}
}
}
]
},
"state": "choose_method"
}
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.

verification.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 GetVerification(ctx context.Context, flowId string) (*client.VerificationFlow, error) {
flow, _, err := ory.FrontendApi.GetVerificationFlow(ctx).Id(flowId).Execute()
if err != nil {
return nil, err
}

return flow, nil
}

Submit verification flow

The last step is to submit the verification flow with the user's email. To do that, send a POST request to the /self-service/verification 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.

The verification flow can have a second submit step if the recovery method is set to code. In such a case, the verification flow shows a field to submit the received code the user gets after they submit their email.

submit-verification-browser.sh
curl -X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-b cookies.txt \
-d '{"method":"code","email":"email@example.com","csrf_token":"your-csrf-token"}' \
"https://{project.slug}.projects.oryapis.com/self-service/verification?flow=<your-flow-id>"
submit-verification-browser.json
{
"id": "7b500ad3-f5d0-46ae-acb7-d39eda5c60d8",
"type": "browser",
"expires_at": "2023-02-08T17:34:35.934427Z",
"issued_at": "2023-02-08T17:04:35.934427Z",
"request_url": "https://{project.slug}.projects.oryapis.com/self-service/verification/browser",
"active": "code",
"ui": {
"action": "https://{project.slug}.projects.oryapis.com/self-service/verification?flow=7b500ad3-f5d0-46ae-acb7-d39eda5c60d8",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "code",
"attributes": {
"name": "code",
"type": "text",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070006,
"text": "Verify code",
"type": "info"
}
}
},
{
"type": "input",
"group": "code",
"attributes": {
"name": "method",
"type": "hidden",
"value": "code",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
},
{
"type": "input",
"group": "code",
"attributes": {
"name": "method",
"type": "submit",
"value": "code",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070005,
"text": "Submit",
"type": "info"
}
}
},
{
"type": "input",
"group": "code",
"attributes": {
"name": "email",
"type": "submit",
"value": "email@example.com",
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {
"label": {
"id": 1070007,
"text": "Resend code",
"type": "info"
}
}
},
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "FH3SnueiKNY6/xbipx7F6pSEkwY90OwFLjLeNUHAgO3d6ISqM6+uuUGC96Sp6J+qHNXQ5jbcRZgScb4KGSshnw==",
"required": true,
"disabled": false,
"node_type": "input"
},
"messages": [],
"meta": {}
}
],
"messages": [
{
"id": 1080003,
"text": "An email containing a verification code has been sent to the email address you provided.",
"type": "info",
"context": {}
}
]
},
"state": "sent_email"
}
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.

verification.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 SubmitVerification(ctx context.Context, flowId string, body client.UpdateVerificationFlowBody) (*client.VerificationFlow, error) {
flow, _, err := ory.FrontendApi.UpdateVerificationFlow(ctx).Flow(flowId).UpdateVerificationFlowBody(body).Execute()
if err != nil {
return nil, err
}

return flow, nil
}

Logout flow

Depending on the application type, the logout flow has a different behavior. With a browser application, the logout flow needs to be initialized to create a logout URL which is associated with the current session cookie. The application can then call the logout URL through a redirect or a background AJAX request.

Browser applications must send a GET request to initialize the logout URL by calling /self-service/logout/browser and a GET request to /self-service/logout to complete the logout flow.

Native applications don't initialize the logout flow and only need to send a DELETE request with the session token in the request body to the /self-service/logout/api endpoint.

get-logout-browser.sh
curl -H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-c cookies.txt \
-b cookies.txt \
"https://{project.slug}.projects.oryapis.com/self-service/logout/browser"
get-logout-browser.json
{
"logout_url": "https://{project.slug}.projects.oryapis.com/self-service/logout?token=J6yIQf7ABx1BBiPOj036dJKSQmKwGnX6",
"logout_token": "J6yIQf7ABx1BBiPOj036dJKSQmKwGnX6"
}
submit-logout-browser.sh
curl -H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-c cookies.txt \
-b cookies.txt \
"https://{project.slug}.projects.oryapis.com/self-service/logout?token=J6yIQf7ABx1BBiPOj036dJKSQmKwGnX6"
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.

Session Checks

This example shows how to use the SDK to call the /sessions/whoami endpoint in an Express.js middleware.

middleware.ts
import { Configuration, FrontendApi } from "@ory/client"
import { NextFunction, Request, Response } from "express"

const frontend = new FrontendApi(
new Configuration({
basePath: process.env.ORY_SDK_URL,
}),
)

// a middleware function that checks if the user is logged in
// the user is loading a page served by express so we can extract the cookie from the
// request header and pass it on to Ory
// the cookie is only available under the same domain as the server e.g. *.myapp.com
function middleware(req: Request, res: Response, next: NextFunction) {
// frontend is an instance of the Ory SDK
frontend
.toSession({ cookie: req.header("cookie") })
.then(({ status, data: flow }) => {
if (status !== 200) {
next("/login")
}
next()
})
// ...
}

Error handling

Ory provides errors at different levels of the flow:

  1. Request level errors, for example when the request expires
  2. Method level errors, for example concerning login with password method
  3. Field level errors, for example when password field contains forbidden characters

Request level errors happen when the request has failed due to an invalid state. For example, the login flow is created, but the session already exists.

Method level errors happen when the method being requested fails.

Each level's error message structure is the same as shown below:

{
id: 1234,
// This ID will not change and can be used to translate the message or use your own message content.
text: "Some default text",
// A default text in english that you can display if you do not want to customize the message.
context: {},
// A JSON object which may contain additional fields such as `expires_at`. This is helpful if you want to craft your own messages.
}

Here is a Typescript example using the Ory SDK of each:

  // call the sdk
// sdk...
.then(({data: flow}) => {
// error message can also exist inside the flow data even when the request is successful.
// It is usually the case when we retrieved the login flow using the flow ID and the flow was submitted previously
// but still contains errors.

// method level error
// messages is an array of error messages
err.response?.data?.ui.messages.forEach((message) => {
console.log(message)
})

// field level error
err.response?.data?.ui.nodes.forEach(({ messages }) => {
// messages is an array of error messages
messages.forEach((message) => {
console.log(message)
})
})
})
.catch((err: AxiosError) => {
// if the request failed but the response is not only an error
// it will contain flow data which can be used to display error messages
// this is common when submitting a form (e.g. login or registration)
// and getting a response status 400 `Bad Request` back.

if (err.response.status === 400) {
// method level error
// messages is an array of error messages
err.response?.data?.ui.messages.forEach((message) => {
console.log(message)
})

// field level error
err.response?.data?.ui.nodes.forEach(({ messages }) => {
// messages is an array of error messages
messages.forEach((message) => {
console.log(message)
})
})
} else {
// request level error
if (err.response?.data?.error) {
console.log(err.response.data.error)
}
}
})

Debugging

CORS errors

To solve Cross-Origin Resource Sharing (CORS) errors, use Ory Tunnel for local development. In production, add your domain to the Ory Project so that all requests from your frontend can be made to Ory under the same domain.

Ory has a "deny by default" policy which means that the Access-Control-Allow-Origin header is only set on domains we own.

tip

To learn more about CORS, read Mozilla CORS documentation.

CSRF errors

Ory provides CSRF protection for all flows. This means that you must send a CSRF token in the body and CSRF cookie back when submitting a flow. The cookie should be sent by default by your browser, but you must add the CSRF token manually to the request body. This can be a JSON object or a native form POST.

When mapping UI nodes, take note of input fields with the name csrf_token with the hidden attribute.

An example of mapping the UI nodes for CSRF protection could look like this:

import {
Configuration,
FrontendApi,
LoginFlow,
UiNodeInputAttributes,
} from "@ory/client"
import {
filterNodesByGroups,
isUiNodeInputAttributes,
} from "@ory/integrations/ui"
import { useEffect, useState } from "react"

const frontend = new FrontendApi(
new Configuration({
basePath: "http://localhost:4000", // Use your local Ory Tunnel URL
baseOptions: {
withCredentials: true,
},
}),
)

function CsrfMapping() {
const [flow, setFlow] = useState<LoginFlow>()

useEffect(() => {
frontend.createBrowserLoginFlow().then(({ data: flow }) => setFlow(flow))
}, [])

return flow ? (
<form action={flow.ui.action} method={flow.ui.method}>
{filterNodesByGroups({
nodes: flow.ui.nodes,
// we will also map default fields here but not oidc and password fields
groups: ["default"],
attributes: ["hidden"], // only want hidden fields
}).map((node) => {
if (
isUiNodeInputAttributes(node.attributes) &&
(node.attributes as UiNodeInputAttributes).type === "hidden" &&
(node.attributes as UiNodeInputAttributes).name === "csrf_token"
) {
return (
<input
type={node.attributes.type}
name={node.attributes.name}
value={node.attributes.value}
/>
)
}
})}
</form>
) : (
<div>Loading...</div>
)
}

export default CsrfMapping