Integration basics
Ory provides headless APIs for ease of integration. This ensures that Ory is compatible with software stacks across different programming languages.
This document provides the fundamentals required to get started with custom user interface integration.
If you want to learn how to integrate custom UI for social sign-in, passwordless, or two-factor authentication, read Advanced integration.
Flows
Flows are an important element of Ory's self-service APIs and are used for 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.
The self-service API is split to integrate with two types of applications - browser and native. The reason for the API split's 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.
Make sure to call the correct self-service API endpoints for your server-side browser, single-page, or native application to ensure that the integration between your application's UI and Ory APIs works.
Flow overview
A flow in Ory consists of five operations:
- Creating the flow for the specific goal and application type, for example user login in a native app.
- Using the flow data to render the UI.
- Submitting the flow with user data, such as username and password.
- Handling errors, such as invalid user input and going back to step 2.
- Handling the successful submission.
Methods in flows
This table shows the methods available for each flow:
Flow | Methods |
---|---|
login | password , oidc , totp , webauthn , lookup_secret |
registration | password , oidc , webauthn |
settings | profile , password , oidc , lookup_secret , webauthn , totp |
recovery | link , code |
verification | link , code |
Flows can consist of multiple methods. For example, a login flow can have the password
and oidc
methods. Flows can be
completed by just one method at a time.
Browser vs native apps
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.
infoAll browser apps must call Ory self-service APIs at
/self-service/{flow_type}/browser
-
Native applications, such as Android mobile apps or desktop applications, aren't rendered in the browser. Since the application isn't rendered in the browser, the CSRF cookie and token aren't necessary.
infoAll 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. Security measures are also applied to the flow to secure following requests to Ory.
You can create a flow by sending a GET request to the
/self-service/{flow_type}/{browser/api}
endpoint.
- cURL (Browser flow)
- cURL (Native flow)
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
To ensure that requests work correctly for the browser flow, use the -c
flag to store the cookies in a file.
On following requests, you can use the -b
flag to read the cookies from the file.
curl -X GET \
-H 'Content-Type: application-json' \
https://$PROJECT_SLUG.projects.oryapis.com/self-service/login/api
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 following requests to Ory.
Single-page application
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 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 following requests to Ory.
Fetch 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?
- Ory has initialized the flow and redirects to the UI, for example
/login?flow=<uuidv4>
. - The request was completed by the user but an error occurred and you need to retrieve the flow data to display the error.
- The UI has 'stored' the flow ID in the URL so that page refreshes can fetch the existing flow data.
The flow ID has an expiry 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 flow type as it determines the endpoint used to retrieve the flow. For example, the login
flow will be
available at the /self-service/login/flows?id=<flowId>
endpoint.
This is an example of fetching an existing login flow:
- cURL (Browser flow)
- cURL (Native flow)
To ensure that requests work correctly for the browser flow, use the -c
flag to store the cookies in a file.
On following requests, you can use the -b
flag to read the cookies from the file.
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>"
curl -X GET \
-H 'Content-Type: application/json' \
"https://$PROJECT_SLUG.projects.oryapis.com/self-service/login/flows?id=<your-flow-id>"
Submit 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>
- cURL (Browser flow)
- cURL (Native flow)
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>"
To ensure that requests work correctly for the browser flow, use the -c
flag to store the cookies in a file.
On following requests, you can use the -b
flag to read the cookies from the file.
curl -X POST \
-H 'Content-Type: application/json' \
-d '{"method":"password","identifier":"[email protected]","password":"verystrongpassword"}' \
"https://$PROJECT_SLUG.projects.oryapis.com/self-service/login?flow=<your-flow-id>"
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
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 just the password
method since it's 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).
Remember to use the correct API endpoints for your application type.
- Browser applications must use
/self-service/login/browser
. - Native applications must use
/self-service/login/api
.
- cURL (Browser flow)
- cURL (Native flow)
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
{
"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"
}
To ensure that requests work correctly for the browser flow, use the -c
flag to store the cookies in a file.
On following requests, you can use the -b
flag to read the cookies from the file.
curl -X GET \
-H 'Content-Type: application-json' \
https://$PROJECT_SLUG.projects.oryapis.com/self-service/login/api
{
"id": "db748b69-a059-464f-870c-2d4f4a3a2f26",
"oauth2_login_challenge": null,
"type": "api",
"expires_at": "2023-01-25T13:36:37.647917454Z",
"issued_at": "2023-01-25T13:06:37.647917454Z",
"request_url": "https://$PROJECT_SLUG.projects.oryapis.com/self-service/login/api",
"ui": {
"action": "https://$PROJECT_SLUG.projects.oryapis.com/self-service/login?flow=db748b69-a059-464f-870c-2d4f4a3a2f26",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "",
"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-25T13:06:37.732557Z",
"updated_at": "2023-01-25T13:06:37.732557Z",
"refresh": false,
"requested_aal": "aal1"
}
- Go
- TypeScript (Native)
- Express.js (Browser)
- React
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
}
import { Configuration, FrontendApi } from "@ory/client"
const frontend = new FrontendApi(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
}),
)
export async function createLogin(aal: string, refresh: boolean) {
return await frontend.createNativeLoginFlow({
aal,
refresh,
})
}
import {
filterNodesByGroups,
getNodeLabel,
isUiNodeAnchorAttributes,
isUiNodeImageAttributes,
isUiNodeInputAttributes,
isUiNodeScriptAttributes,
isUiNodeTextAttributes,
} from "@ory/integrations/ui"
import express, { Request, Response } from "express"
import { engine } from "express-handlebars"
import { Configuration, FrontendApi, UiNode } from "@ory/client"
const apiBaseUrlInternal =
process.env.KRATOS_PUBLIC_URL ||
process.env.ORY_SDK_URL ||
"http://localhost:4000"
export const apiBaseUrl = process.env.KRATOS_BROWSER_URL || apiBaseUrlInternal
// Sets up the SDK
let sdk = new FrontendApi(
new Configuration({
basePath: apiBaseUrlInternal,
}),
)
const getUrlForFlow = (flow: string, query?: URLSearchParams) =>
`${apiBaseUrl}/self-service/${flow}/browser${
query ? `?${query.toString()}` : ""
}`
const isQuerySet = (x: any): x is string =>
typeof x === "string" && x.length > 0
const getUiNodePartialType = (node: UiNode) => {
if (isUiNodeAnchorAttributes(node.attributes)) {
return "ui_node_anchor"
} else if (isUiNodeImageAttributes(node.attributes)) {
return "ui_node_image"
} else if (isUiNodeInputAttributes(node.attributes)) {
switch (node.attributes && node.attributes.type) {
case "hidden":
return "ui_node_input_hidden"
case "submit":
return "ui_node_input_button"
case "button":
return "ui_node_input_button"
case "checkbox":
return "ui_node_input_checkbox"
default:
return "ui_node_input_default"
}
} else if (isUiNodeScriptAttributes(node.attributes)) {
return "ui_node_script"
} else if (isUiNodeTextAttributes(node.attributes)) {
return "ui_node_text"
}
return "ui_node_input_default"
}
// This helper function translates the html input type to the corresponding partial name.
export const toUiNodePartial = (node: UiNode, comparison: string) => {
console.log("toUiNodePartial", node, comparison)
return getUiNodePartialType(node) === comparison
}
const app = express()
app.set("view engine", "hbs")
app.engine(
"hbs",
engine({
extname: "hbs",
layoutsDir: `${__dirname}/../views/layouts/`,
partialsDir: `${__dirname}/../views/partials/`,
defaultLayout: "main",
helpers: {
onlyNodes: (nodes: any) => filterNodesByGroups({ nodes: nodes }),
toUiNodePartial,
getNodeLabel: getNodeLabel,
},
}),
)
app.get("/login", async (req: Request, res: Response) => {
const { flow, aal = "", refresh = "", return_to = "" } = req.query
const initFlowQuery = new URLSearchParams({
aal: aal.toString(),
refresh: refresh.toString(),
return_to: return_to.toString(),
})
const initFlowUrl = getUrlForFlow("login", initFlowQuery)
// The flow is used to identify the settings and registration flow and
// return data like the csrf_token and so on.
if (!isQuerySet(flow)) {
res.redirect(303, initFlowUrl)
return
}
// hightlight-start
return sdk
.getLoginFlow({
id: flow,
cookie: req.header("cookie"),
})
.then(({ data: flow }) => {
const initRegistrationQuery = new URLSearchParams({
return_to: return_to.toString(),
})
if (flow.requested_aal === "aal2") {
return sdk
.createBrowserLogoutFlow({
cookie: req.header("cookie"),
})
.then(({ data: logoutFlow }) => {
res.render("auth", {
...flow,
signUpUrl: getUrlForFlow("registration", initRegistrationQuery),
recoveryUrl: getUrlForFlow("recovery"),
logoutUrl: logoutFlow.logout_url,
})
return
})
}
// Render the data using a view (e.g. Jade Template):
res.render("auth", {
...flow,
signUpUrl: getUrlForFlow("registration", initRegistrationQuery),
recoveryUrl: getUrlForFlow("recovery"),
})
})
.catch((err) => {
if (err.response?.status === 410) {
res.redirect(303, initFlowUrl)
return
}
})
// hightlight-end
})
const port = Number(process.env.PORT) || 3001
app.listen(port, () => console.log(`Listening on http://0.0.0.0:${port}`))
This example uses the Ory SDK (@ory/client
) to create a login flow and render the user interface.
The @ory/integrations
package is used as a helper to filter the flow data and render just the password
method form fields.
import {
Configuration,
FrontendApi,
LoginFlow,
UiNode,
UiNodeInputAttributes,
} from "@ory/client"
import {
filterNodesByGroups,
isUiNodeInputAttributes,
} from "@ory/integrations/ui"
import { useEffect, useState } from "react"
import { useSearchParams } from "react-router-dom"
const frontend = new FrontendApi(
new Configuration({
basePath: "http://localhost:4000", // Use your local Ory Tunnel URL
baseOptions: {
withCredentials: true, // we need to include cookies
},
}),
)
export const Login = () => {
const [flow, setFlow] = useState<LoginFlow>()
const [searchParams] = useSearchParams()
useEffect(() => {
// check if the login flow is for two factor authentication
const aal2 = searchParams.get("aal2")
// we can redirect the user back to the page they were on before login
const returnTo = searchParams.get("return_to")
frontend
.createBrowserLoginFlow({
returnTo: returnTo || "/", // redirect to the root path after login
// if the user has a session, refresh it
refresh: true,
// if the aal2 query parameter is set, we get the two factor login flow UI nodes
aal: aal2 ? "aal2" : "aal1",
})
.then(({ data: flow }) => {
// set the flow data
setFlow(flow)
})
.catch((err) => {
// Couldn't create login flow
// handle the error
})
}, [])
const mapUINode = (node: UiNode, key: number) => {
// other node types are also supported
// if (isUiNodeTextAttributes(node.attributes)) {
// if (isUiNodeImageAttributes(node.attributes)) {
// if (isUiNodeAnchorAttributes(node.attributes)) {
if (isUiNodeInputAttributes(node.attributes)) {
const attrs = node.attributes as UiNodeInputAttributes
const nodeType = attrs.type
switch (nodeType) {
case "button":
case "submit":
return (
<button
type={attrs.type as "submit" | "reset" | "button" | undefined}
name={attrs.name}
value={attrs.value}
/>
)
default:
return (
<input
name={attrs.name}
type={attrs.type}
autoComplete={
attrs.autocomplete || attrs.name === "identifier"
? "username"
: ""
}
defaultValue={attrs.value}
required={attrs.required}
disabled={attrs.disabled}
/>
)
}
}
}
return flow ? (
<form action={flow.ui.action} method={flow.ui.method}>
{filterNodesByGroups({
nodes: flow.ui.nodes,
// we will also map default fields here such as csrf_token
// this only maps the `password` method
// other methods can also be mapped such as `oidc` or `webauthn`
groups: ["default", "password"],
}).map((node, idx) => mapUINode(node, idx))}
</form>
) : (
<div>Loading...</div>
)
}
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 stored in the ?flow=
URL query parameter in your application. The following example shows how to get flow data
using the flow ID.
- cURL (Browser flow)
- cURL (Native flow)
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>"
{
"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"
}
To ensure that requests work correctly for the browser flow, use the -c
flag to store the cookies in a file.
On following requests, you can use the -b
flag to read the cookies from the file.
curl -X GET \
-H 'Content-Type: application/json' \
"https://$PROJECT_SLUG.projects.oryapis.com/self-service/login/flows?id=<your-flow-id>"
{
"id": "9597b523-ddfe-4b1a-aeaa-205b3cd106a0",
"oauth2_login_challenge": null,
"type": "api",
"expires_at": "2023-01-25T15:03:08.972446Z",
"issued_at": "2023-01-25T14:33:08.972446Z",
"request_url": "https://$PROJECT_SLUG.projects.oryapis.com/self-service/login/api",
"ui": {
"action": "https://$PROJECT_SLUG.projects.oryapis.com/self-service/login?flow=9597b523-ddfe-4b1a-aeaa-205b3cd106a0",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "",
"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:33:09.050657Z",
"updated_at": "2023-01-25T14:33:09.050657Z",
"refresh": false,
"requested_aal": "aal1"
}
- Go
- TypeScript
- React
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
}
import { Configuration, FrontendApi } from "@ory/client"
const frontend = new FrontendApi(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
}),
)
export async function getLogin(id: string) {
return await frontend.getLoginFlow({
id,
})
}
This example uses the Ory SDK (@ory/client
) to get a login flow and render the user interface.
The @ory/integrations
package is used as a helper to filter the flow data and render just the password
method form fields.
import {
Configuration,
FrontendApi,
LoginFlow,
UiNode,
UiNodeInputAttributes,
} from "@ory/client"
import {
filterNodesByGroups,
isUiNodeInputAttributes,
} from "@ory/integrations/ui"
import { useEffect, useState } from "react"
import { useSearchParams } from "react-router-dom"
const frontend = new FrontendApi(
new Configuration({
basePath: "http://localhost:4000", // Use your local Ory Tunnel URL
baseOptions: {
withCredentials: true, // we need to include cookies
},
}),
)
export const Login = () => {
const [flow, setFlow] = useState<LoginFlow>()
const [searchParams] = useSearchParams()
useEffect(() => {
const id = searchParams.get("flow")
frontend
.getLoginFlow({
id,
})
.then(({ data: flow }) => {
// set the flow data
setFlow(flow)
})
.catch((err) => {
// Couldn't retrieve the login flow
// handle the error
})
}, [])
const mapUINode = (node: UiNode, key: number) => {
// other node types are also supported
// if (isUiNodeTextAttributes(node.attributes)) {
// if (isUiNodeImageAttributes(node.attributes)) {
// if (isUiNodeAnchorAttributes(node.attributes)) {
if (isUiNodeInputAttributes(node.attributes)) {
const attrs = node.attributes as UiNodeInputAttributes
const nodeType = attrs.type
switch (nodeType) {
case "button":
case "submit":
return (
<button
type={attrs.type as "submit" | "reset" | "button" | undefined}
name={attrs.name}
value={attrs.value}
key={key}
/>
)
default:
return (
<input
name={attrs.name}
type={attrs.type}
autoComplete={
attrs.autocomplete || attrs.name === "identifier"
? "username"
: ""
}
defaultValue={attrs.value}
required={attrs.required}
disabled={attrs.disabled}
key={key}
/>
)
}
}
}
return flow ? (
<form action={flow.ui.action} method={flow.ui.method}>
{filterNodesByGroups({
nodes: flow.ui.nodes,
// we will also map default fields here such as csrf_token
// this only maps the `password` method
// you can also map `oidc` or `webauhthn` here as well
groups: ["default", "password"],
}).map((node, idx) => mapUINode(node, idx))}
</form>
) : (
<div>Loading...</div>
)
}
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
.
- cURL (Browser flow)
- cURL (Native flow)
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>"
{
"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"
}
To ensure that requests work correctly for the browser flow, use the -c
flag to store the cookies in a file.
On following requests, you can use the -b
flag to read the cookies from the file.
curl -X GET \
-H 'Content-Type: application/json' \
"https://$PROJECT_SLUG.projects.oryapis.com/self-service/login/flows?id=<your-flow-id>"
{
"id": "9597b523-ddfe-4b1a-aeaa-205b3cd106a0",
"oauth2_login_challenge": null,
"type": "api",
"expires_at": "2023-01-25T15:03:08.972446Z",
"issued_at": "2023-01-25T14:33:08.972446Z",
"request_url": "https://$PROJECT_SLUG.projects.oryapis.com/self-service/login/api",
"ui": {
"action": "https://$PROJECT_SLUG.projects.oryapis.com/self-service/login?flow=9597b523-ddfe-4b1a-aeaa-205b3cd106a0",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "",
"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:33:09.050657Z",
"updated_at": "2023-01-25T14:33:09.050657Z",
"refresh": false,
"requested_aal": "aal1"
}
- Go
- TypeScript
- React
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
}
import { Configuration, FrontendApi, UpdateLoginFlowBody } from "@ory/client"
const frontend = new FrontendApi(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
}),
)
export async function submitLogin(id: string, body: UpdateLoginFlowBody) {
return await frontend.updateLoginFlow({
flow: id,
updateLoginFlowBody: body,
})
}
import {
Configuration,
FrontendApi,
LoginFlow,
UiNode,
UiNodeInputAttributes,
UpdateLoginFlowBody,
} from "@ory/client"
import {
filterNodesByGroups,
isUiNodeInputAttributes,
} from "@ory/integrations/ui"
import { AxiosError } from "axios"
import { useEffect, useState } from "react"
import { useNavigate, useSearchParams } from "react-router-dom"
const frontend = new FrontendApi(
new Configuration({
basePath: "http://localhost:4000", // Use your local Ory Tunnel URL
baseOptions: {
withCredentials: true, // we need to include cookies
},
}),
)
export const Login = () => {
const [flow, setFlow] = useState<LoginFlow>()
const [searchParams] = useSearchParams()
const navigate = useNavigate()
useEffect(() => {
const id = searchParams.get("flow")
frontend
.getLoginFlow({
id,
})
.then(({ data: flow }) => {
// set the flow data
setFlow(flow)
})
.catch((err) => {
// Couldn't retrieve the login flow
// handle the error
})
}, [])
const submit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const form = event.currentTarget
const formData = new FormData(form)
// map the entire form data to JSON for the request body
let body = Object.fromEntries(formData) as unknown as UpdateLoginFlowBody
// We need the method specified from the name and value of the submit button.
// when multiple submit buttons are present, the clicked one's value is used.
if ("submitter" in event.nativeEvent) {
const method = (
event.nativeEvent as unknown as { submitter: HTMLInputElement }
).submitter
body = {
...body,
...{ [method.name]: method.value },
}
}
frontend
.updateLoginFlow({
flow: flow.id,
updateLoginFlowBody: body,
})
.then(() => {
navigate("/", { replace: true })
})
.catch((err: AxiosError) => {
// handle the error
if (err.response.status === 400) {
// user input error
// show the error messages in the UI
setFlow(err.response.data)
}
})
}
const mapUINode = (node: UiNode, key: number) => {
// other node types are also supported
// if (isUiNodeTextAttributes(node.attributes)) {
// if (isUiNodeImageAttributes(node.attributes)) {
// if (isUiNodeAnchorAttributes(node.attributes)) {
if (isUiNodeInputAttributes(node.attributes)) {
const attrs = node.attributes as UiNodeInputAttributes
const nodeType = attrs.type
switch (nodeType) {
case "button":
case "submit":
return (
<button
type={attrs.type as "submit" | "reset" | "button" | undefined}
name={attrs.name}
value={attrs.value}
key={key}
/>
)
default:
return (
<input
name={attrs.name}
type={attrs.type}
autoComplete={
attrs.autocomplete || attrs.name === "identifier"
? "username"
: ""
}
defaultValue={attrs.value}
required={attrs.required}
disabled={attrs.disabled}
key={key}
/>
)
}
}
}
return flow ? (
<form action={flow.ui.action} method={flow.ui.method} onSubmit={submit}>
{filterNodesByGroups({
nodes: flow.ui.nodes,
// we will also map default fields here such as csrf_token
// this only maps the `password` method
// you can also map `oidc` or `webauhthn` here as well
groups: ["default", "password"],
}).map((node, key) => mapUINode(node, key))}
</form>
) : (
<div>Loading...</div>
)
}
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 methods such as password
, oidc
and webauthn
.
This section covers just the password
method since it's 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).
Remember to use the correct API endpoints for your application type.
- Browser applications must use
/self-service/registration/browser
. - Native applications must use
/self-service/registration/api
.
- cURL (Browser flow)
- cURL (Native flow)
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"
{
"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": {}
}
}
}
]
}
}
To ensure that requests work correctly for the browser flow, use the -c
flag to store the cookies in a file.
On following requests, you can use the -b
flag to read the cookies from the file.
curl -H 'Content-Type: application/json' \
-H 'Accept: application/json' \
https://$PROJECT_SLUG.projects.oryapis.com/self-service/registration/browser
{
"id": "2b5bab03-fa89-4e2d-a575-c614e2ab8f56",
"oauth2_login_challenge": null,
"type": "api",
"expires_at": "2023-02-08T14:41:26.280202015Z",
"issued_at": "2023-02-08T14:11:26.280202015Z",
"request_url": "https://$PROJECT_SLUG.projects.oryapis.com/self-service/registration/api",
"ui": {
"action": "https://$PROJECT_SLUG.projects.oryapis.com/self-service/registration?flow=2b5bab03-fa89-4e2d-a575-c614e2ab8f56",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "",
"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": {}
}
}
}
]
}
}
- Go
- TypeScript
- Express.js (Browser)
- React
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
}
import { Configuration, FrontendApi } from "@ory/client"
const frontend = new FrontendApi(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
}),
)
export async function createRegistration() {
return await frontend.createNativeRegistrationFlow()
}
import {
filterNodesByGroups,
getNodeLabel,
isUiNodeAnchorAttributes,
isUiNodeImageAttributes,
isUiNodeInputAttributes,
isUiNodeScriptAttributes,
isUiNodeTextAttributes,
} from "@ory/integrations/ui"
import express, { Request, Response } from "express"
import { engine } from "express-handlebars"
import { Configuration, FrontendApi, UiNode } from "@ory/client"
const apiBaseUrlInternal =
process.env.KRATOS_PUBLIC_URL ||
process.env.ORY_SDK_URL ||
"http://localhost:4000"
export const apiBaseUrl = process.env.KRATOS_BROWSER_URL || apiBaseUrlInternal
// Sets up the SDK
let sdk = new FrontendApi(
new Configuration({
basePath: apiBaseUrlInternal,
}),
)
const getUrlForFlow = (flow: string, query?: URLSearchParams) =>
`${apiBaseUrl}/self-service/${flow}/browser${
query ? `?${query.toString()}` : ""
}`
const isQuerySet = (x: any): x is string =>
typeof x === "string" && x.length > 0
const getUiNodePartialType = (node: UiNode) => {
if (isUiNodeAnchorAttributes(node.attributes)) {
return "ui_node_anchor"
} else if (isUiNodeImageAttributes(node.attributes)) {
return "ui_node_image"
} else if (isUiNodeInputAttributes(node.attributes)) {
switch (node.attributes && node.attributes.type) {
case "hidden":
return "ui_node_input_hidden"
case "submit":
return "ui_node_input_button"
case "button":
return "ui_node_input_button"
case "checkbox":
return "ui_node_input_checkbox"
default:
return "ui_node_input_default"
}
} else if (isUiNodeScriptAttributes(node.attributes)) {
return "ui_node_script"
} else if (isUiNodeTextAttributes(node.attributes)) {
return "ui_node_text"
}
return "ui_node_input_default"
}
// This helper function translates the html input type to the corresponding partial name.
export const toUiNodePartial = (node: UiNode, comparison: string) => {
console.log("toUiNodePartial", node, comparison)
return getUiNodePartialType(node) === comparison
}
const app = express()
app.set("view engine", "hbs")
app.engine(
"hbs",
engine({
extname: "hbs",
layoutsDir: `${__dirname}/../views/layouts/`,
partialsDir: `${__dirname}/../views/partials/`,
defaultLayout: "main",
helpers: {
onlyNodes: (nodes: any) => filterNodesByGroups({ nodes: nodes }),
toUiNodePartial,
getNodeLabel: getNodeLabel,
},
}),
)
app.get("/registration", async (req: Request, res: Response) => {
const { flow, return_to = "" } = req.query
const initFlowQuery = new URLSearchParams({
return_to: return_to.toString(),
})
const initLoginFlow = new URLSearchParams({
return_to: return_to.toString(),
})
const initFlowUrl = getUrlForFlow("registration", initFlowQuery)
// The flow is used to identify the settings and registration flow and
// return data like the csrf_token and so on.
if (!isQuerySet(flow)) {
res.redirect(303, initFlowUrl)
return
}
// hightlight-start
return sdk
.getRegistrationFlow({
id: flow,
cookie: req.header("cookie"),
})
.then(({ data: flow }) => {
// Render the data using a view (e.g. Jade Template):
const loginUrl = getUrlForFlow("login", initLoginFlow)
res.render("auth", {
...flow,
loginUrl: loginUrl,
})
})
.catch((err) => {
if (err.response?.status === 410) {
res.redirect(303, initFlowUrl)
return
}
})
// hightlight-end
})
const port = Number(process.env.PORT) || 3001
app.listen(port, () => console.log(`Listening on http://0.0.0.0:${port}`))
import {
Configuration,
FrontendApi,
RegistrationFlow,
UiNode,
UiNodeInputAttributes,
} from "@ory/client"
import {
filterNodesByGroups,
isUiNodeInputAttributes,
} from "@ory/integrations/ui"
import { useEffect, useState } from "react"
import { useSearchParams } from "react-router-dom"
const frontend = new FrontendApi(
new Configuration({
basePath: "http://localhost:4000", // Use your local Ory Tunnel URL
baseOptions: {
withCredentials: true, // we need to include cookies
},
}),
)
export const Registration = () => {
const [flow, setFlow] = useState<RegistrationFlow>()
const [searchParams] = useSearchParams()
useEffect(() => {
// we can redirect the user back to the page they were on before login
const returnTo = searchParams.get("return_to")
frontend
.createBrowserRegistrationFlow({
returnTo: returnTo || "/", // redirect to the root path after login
})
.then(({ data: flow }) => {
// set the flow data
setFlow(flow)
})
.catch((err) => {
// Couldn't create login flow
// handle the error
})
}, [])
const mapUINode = (node: UiNode, key: number) => {
// other node types are also supported
// if (isUiNodeTextAttributes(node.attributes)) {
// if (isUiNodeImageAttributes(node.attributes)) {
// if (isUiNodeAnchorAttributes(node.attributes)) {
if (isUiNodeInputAttributes(node.attributes)) {
const attrs = node.attributes as UiNodeInputAttributes
const nodeType = attrs.type
switch (nodeType) {
case "button":
case "submit":
return (
<button
type={attrs.type as "submit" | "reset" | "button" | undefined}
name={attrs.name}
value={attrs.value}
/>
)
default:
return (
<input
name={attrs.name}
type={attrs.type}
autoComplete={
attrs.autocomplete || attrs.name === "identifier"
? "username"
: ""
}
defaultValue={attrs.value}
required={attrs.required}
disabled={attrs.disabled}
/>
)
}
}
}
return flow ? (
<form action={flow.ui.action} method={flow.ui.method}>
{filterNodesByGroups({
nodes: flow.ui.nodes,
// we will also map default fields here such as csrf_token
// this only maps the `password` method
// other methods can also be mapped such as `oidc` or `webauthn`
groups: ["default", "password"],
}).map((node, idx) => mapUINode(node, idx))}
</form>
) : (
<div>Loading...</div>
)
}
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 stored in the ?flow=
URL query parameter in your application. The following example shows how to get flow data
using the flow ID.
- cURL (Browser flow)
- cURL (Native flow)
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>"
{
"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": {}
}
}
}
]
}
}
To ensure that requests work correctly for the browser flow, use the -c
flag to store the cookies in a file.
On following requests, you can use the -b
flag to read the cookies from the file.
curl -H 'Content-Type: application/json' \
-H 'Accept: application/json' \
"https://$PROJECT_SLUG.projects.oryapis.com/self-service/registration/flows?id=<your-flow-id>"
{
"id": "2b5bab03-fa89-4e2d-a575-c614e2ab8f56",
"oauth2_login_challenge": null,
"type": "api",
"expires_at": "2023-02-08T14:41:26.280202Z",
"issued_at": "2023-02-08T14:11:26.280202Z",
"request_url": "https://$PROJECT_SLUG.projects.oryapis.com/self-service/registration/api",
"ui": {
"action": "https://$PROJECT_SLUG.projects.oryapis.com/self-service/registration?flow=2b5bab03-fa89-4e2d-a575-c614e2ab8f56",
"method": "POST",
"nodes": [
{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "",
"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": {}
}
}
}
]
}
}
- Go
- TypeScript
- React
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
}
import { Configuration, FrontendApi } from "@ory/client"
const frontend = new FrontendApi(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
}),
)
export async function getRegistration(id: string) {
return await frontend.getRegistrationFlow({
id,
})
}
import {
Configuration,
FrontendApi,
RegistrationFlow,
UiNode,
UiNodeInputAttributes,
} from "@ory/client"
import {
filterNodesByGroups,
isUiNodeInputAttributes,
} from "@ory/integrations/ui"
import { useEffect, useState } from "react"
import { useSearchParams } from "react-router-dom"
const frontend = new FrontendApi(
new Configuration({
basePath: "http://localhost:4000", // Use your local Ory Tunnel URL
baseOptions: {
withCredentials: true, // we need to include cookies
},
}),
)
export const Registration = () => {
const [flow, setFlow] = useState<RegistrationFlow>()
const [searchParams] = useSearchParams()
useEffect(() => {
const id = searchParams.get("flow")
frontend
.getRegistrationFlow({
id: id,
})
.then(({ data: flow }) => {
// set the flow data
setFlow(flow)
})
.catch((err) => {
// Couldn't create login flow
// handle the error
})
}, [])
const mapUINode = (node: UiNode, key: number) => {
// other node types are also supported
// if (isUiNodeTextAttributes(node.attributes)) {
// if (isUiNodeImageAttributes(node.attributes)) {
// if (isUiNodeAnchorAttributes(node.attributes)) {
if (isUiNodeInputAttributes(node.attributes)) {
const attrs = node.attributes as UiNodeInputAttributes
const nodeType = attrs.type
switch (nodeType) {
case "button":
case "submit":
return (
<button
type={attrs.type as "submit" | "reset" | "button" | undefined}
name={attrs.name}
value={attrs.value}
/>
)
default:
return (
<input
name={attrs.name}
type={attrs.type}
autoComplete={
attrs.autocomplete || attrs.name === "identifier"
? "username"
: ""
}
defaultValue={attrs.value}
required={attrs.required}
disabled={attrs.disabled}
/>
)
}
}
}
return flow ? (
<form action={flow.ui.action} method={flow.ui.method}>
{filterNodesByGroups({
nodes: flow.ui.nodes,
// we will also map default fields here such as csrf_token
// this only maps the `password` method
// you can also map `oidc` or `webauhthn` here as well
groups: ["default", "password"],
}).map((node, idx) => mapUINode(node, idx))}
</form>
) : (
<div>Loading...</div>
)
}
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 more 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 show its usage.
{
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "E-Mail",
"maxLength": 320
},
"tos": {
"type": "boolean",
"title": "Accept Tos"
}
}
}
}
}
- cURL (Browser flow)
- cURL (Native flow)
Take note of the payload. This example is based on the identity schema snippet and has two required traits, traits.email
and traits.tos
.
curl -X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"method":"password","csrf_token":"your-csrf-token","traits.email":"[email protected]","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>"
{
"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 protected]",
"tos": true
},
"verifiable_addresses": [
{
"id": "ebb8c0c9-8444-4727-9762-cdf01f1736b6",
"value": "[email protected]",
"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 protected]",
"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 protected]",
"tos": true
},
"verifiable_addresses": [
{
"id": "ebb8c0c9-8444-4727-9762-cdf01f1736b6",
"value": "[email protected]",
"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 protected]",
"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"
}
}
To ensure that requests work correctly for the browser flow, use the -c
flag to store the cookies in a file.
On following requests, you can use the -b
flag to read the cookies from the file.
curl -X POST -H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"method":"password","traits.email":"[email protected]","traits.tos":"true","password":"verystrongpassword","transient_payload.consents":"newsletter,usage_stats"}' \
"https://$PROJECT_SLUG.projects.oryapis.com/self-service/registration?flow=<your-flow-id>"