Advanced integration
This document goes into detail on advanced topics in specific use cases after you have already successfully integrated a basic user interface. The topics range from advanced redirects to passwordless and two-factor authentication. Read the Basic integration document first to learn about flows and methods.
Advanced redirects
Most applications that use Ory need advanced URL redirects. For example, the application might have a page where the user is redirected when their session expires. After the user re-authenticates, they should be redirected to the page they were using when their session expired.
To achieve this, the application can pass a ?return_to=<url>
query parameter to the endpoint that initializes a flow:
/self-service/{flow_type}/browser?return_to=https://myapp.com/protected
.
The return_to
URL is the redirect URL after the flow is completed. For example, in the case of a login flow the redirect happens
after a successful login.
This is a breakdown of how the redirect works:
- Initialize the flow with a
return_to
URL. - Use the flow data to render the UI.
- Submit the flow with user data.
- If the flow is successful, the user is redirected to the URL defined in the
return_to
parameter.
The return_to
query parameter doesn't automatically persist across different flows and must be added to new flows. For example,
if the user starts a login flow with return_to
URL set and then switches to a registration flow, the return_to
URL isn't used
for the registration flow. In such a case, your application can re-use the same return_to
from the login flow by extracting the
return_to
URL from the login flow's flow.return_to
and adding it to the registration flow.
sdk.getLoginFlow({ id: flow }).then(({ data: flow }) => {
const returnTo = flow.return_to
return sdk.createBrowserRegistrationFlow({ id: flow.id, return_to: returnTo })
})
The return_to
URL persists across flows only when a recovery flow transitions into a the settings flow.
Let's take a look at an example of how this works:
- Create a recovery flow with
return_to
. - Email is sent with a
link
orcode
method. - User completes the recovery flow by submitting the
code
or clicking thelink
. - The user gets a sessions and is redirected through the
settings
flow. - The user submits the
settings
flow with an updated password. - The user is redirected to the
return_to
URL.
Login
Refreshing user session
To re-authenticate an active user session and confirm the same user is logged in, the application implementing the UI can request
Ory to force the user to authenticate again through the ?refresh=true
query parameter. To do that, call the
/self-service/login/browser?refresh=true
endpoint. This is
only applicable to the login flow and requires the user to already have a session.
This is a common use case for refreshing a session:
- User logs in.
- User interacts with the application.
- User is inactive for a period of time.
- The user returns before session lifespan expires.
- The application needs to confirm that the user interacting with the application is the same user that logged in.
- The session is refreshed through a login flow with the
?refresh=true
query parameter.
sdk.createBrowserLoginFlow({ refresh: true })
Social sign-in
When using social sign-in, the user must be redirected using a native form POST. In single-page applications the page is never
redirected. For the application to then have a background POST on other methods such as password
, the application must separate
the flow data across two forms.
{/*This form will do a native post request to Ory for the selected provider*/}
<form action={flow.action} method={flow.method}>
<button type="submit" value="google">Sign in with Google</button>
<button type="submit" value="facebook">Sign in with Facebook</button>
</form>
<form action={flow.action} method={flow.method} onSubmit={submitHandler}>
{/*Map the other flow groups here such as `default` and `password`*/}
</form>
Upstream provider parameters
In some cases, it is possible to pass additional parameters to the upstream provider. For example, when using the Google provider,
it is possible to pass the login_hint
, hd
and prompt
parameters.
The login_hint
parameter suppresses the account chooser and either pre-fills the email box on the sign-in form, or selects the
proper session. The hd
parameter limits the login/registration process to a Google Organization, for example mycollege.edu
.
The prompt
parameter specifies whether the Authorization Server prompts the End-User for reauthentication and consent, for
example select_account
. To pass these parameters on login
, registration
and settings
flows, the application can use the
submit form body to pass the parameters. For example:
<form action="https://{project.slug}.projects.oryapis.com/self-service/login?flow=<flow-id>" method="POST">
<input type="submit" name="provider" value="google" />
<input type="hidden" name="upstream_parameters.login_hint" value="foo@bar.com" />
<input type="hidden" name="upstream_parameters.hd" value="bar.com" />
<input type="hidden" name="upstream_parameters.prompt" value="select_account" />
<input type="hidden" name="upstream_parameters.auth_type" value="reauthenticate" />
</form>
Supported parameters: login_hint
, hd
, prompt
, and auth_type
.
Two-factor authentication
Two-factor authentication (2FA) is a second step used to confirm the user's identity. When creating a login flow, you can specify
the ?aal=<level>
query parameter to specify the required level of authentication: aal1
or aal2
.
To initialize the second authentication factor, the user must already have a valid session cookie. The
/sessions/whoami
endpoint returns an error with the
session_aal2_required
ID if the user is required to complete a second factor.
Read this document to learn more about UI error messages.
sdk
.toSession()
.then(({ data: session }) => {
// we set the session data which contains the user Identifier and other traits.
})
.catch((err: AxiosError) => {
switch (error.response?.status) {
case 403: {
// the user might have a session, but would require 2FA (Two-Factor Authentication)
if (error.response?.data.error?.id === "session_aal2_required") {
navigate("/login?aal2=true", { replace: true })
return
}
}
}
})
Passwordless authentication
Passwordless authentication uses the WebAuthn specification to authenticate users with hardware keys, biometrics, or passkeys.
For an application to use WebAuthn, add the
/.well-known/ory/webauthn.js
WebAuthn JavaScript to the
page. Ory provides the on-click handler for the button to start the passwordless authentication flow.
The flow works as follows:
- Create login flow.
- Render the UI with the
webauthn
group. - User enters their identifier and clicks the
Sign in with security key
button. - The form is submitted which starts a new flow with the
webauthn
group only. - Render the new UI which prompts the user to insert their security key.
- The user inserts their security key and clicks the
Continue
button.
<head>
<script src="/.well-known/ory/webauthn.js"></script>
</head>
- React
import {
Configuration,
FrontendApi,
LoginFlow,
UiNodeInputAttributes,
UiNodeScriptAttributes,
} from "@ory/client"
import {
filterNodesByGroups,
isUiNodeInputAttributes,
} from "@ory/integrations/ui"
import { HTMLAttributeReferrerPolicy, useEffect, useState } from "react"
const frontend = new FrontendApi(
new Configuration({
basePath: "http://localhost:4000", // Use your local Ory Tunnel URL
baseOptions: {
withCredentials: true,
},
}),
)
function PasswordlessMapping() {
const [flow, setFlow] = useState<LoginFlow>()
useEffect(() => {
frontend.createBrowserLoginFlow().then(({ data: flow }) => setFlow(flow))
}, [])
// Add the WebAuthn script to the DOM
useEffect(() => {
const scriptNodes = filterNodesByGroups({
nodes: flow.ui.nodes,
groups: "webauthn",
attributes: "text/javascript",
withoutDefaultGroup: true,
withoutDefaultAttributes: true,
}).map((node) => {
const attr = node.attributes as UiNodeScriptAttributes
const script = document.createElement("script")
script.src = attr.src
script.type = attr.type
script.async = attr.async
script.referrerPolicy = attr.referrerpolicy as HTMLAttributeReferrerPolicy
script.crossOrigin = attr.crossorigin
script.integrity = attr.integrity
document.body.appendChild(script)
return script
})
// cleanup
return () => {
scriptNodes.forEach((script) => {
document.body.removeChild(script)
})
}
}, [flow.ui.nodes])
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: ["webauthn"],
attributes: ["hidden", "submit", "button"],
}).map((node) => {
if (isUiNodeInputAttributes(node.attributes)) {
const attrs = node.attributes as UiNodeInputAttributes
const nodeType = attrs.type
const submit: any = {
type: attrs.type as "submit" | "reset" | "button" | undefined,
name: attrs.name,
...(attrs.value && { value: attrs.value }),
}
switch (nodeType) {
case "button":
case "submit":
if (attrs.onclick) {
// This is a bit hacky but it wouldn't work otherwise.
const oc = attrs.onclick
submit.onClick = () => {
eval(oc)
}
}
return <button disabled={attrs.disabled} {...submit} />
default:
return (
<input
name={attrs.name}
type={attrs.type}
defaultValue={attrs.value}
required={attrs.required}
disabled={attrs.disabled}
/>
)
}
}
})}
</form>
) : (
<div>Loading...</div>
)
}
export default PasswordlessMapping
This is an example response from the login flow with passwordless authentication enabled. This is the first step of the flow where the user enters their identifier.
{
ui: {
nodes: [
{
type: "input",
group: "webauthn",
attributes: {
name: "method",
type: "submit",
value: "webauthn",
disabled: false,
node_type: "input",
},
messages: [],
meta: {
label: {
id: 1010001,
text: "Sign in with security key",
type: "info",
context: {},
},
},
},
],
},
}
After the user is redirected with a new flow, the response contains the webauthn
group only. In this response, the button
onclick
attribute calls the script added in the header/body of the page. The button then triggers the WebAuthn prompt for the
security key.
{
ui: {
nodes: [
{
type: "input",
group: "webauthn",
attributes: {
name: "webauthn_login_trigger",
type: "button",
value: "",
disabled: false,
onclick: 'window.__oryWebAuthnLogin({"publicKey":{"challenge":"LLoTYU0Xv7QkmIFiwKDOpr1XeNU5c0TGnCgzj6kqSIQ=","timeout":60000,"rpId":"auth.example.com","allowCredentials":[{"type":"public-key","id":"3DUZPEd3DwUCaRkyTy0L0MntQElA0AH4uAUydiVhBdGWTXHS1TDsY+lgHRhTj52cFUL/uua2Af+dgqD14a/rHA=="}],"userVerification":"discouraged"}})',
node_type: "input",
},
messages: [],
meta: {
label: {
id: 1010013,
text: "Continue",
type: "info",
},
},
},
],
},
}
SPAs and the '422' error
A response code 422
indicates that the browser needs to be redirected with a newly created flow. Since this is an SPA, the new
flow ID can be extracted from the payload and the response can be retrieved in the background instead of a redirect.
This is an example code 422
error:
{
error: {
id: "browser_location_change_required",
code: 422,
status: "Unprocessable Entity",
reason: "In order to complete this flow please redirect the browser to: /ui/login?flow=ad574ad7-1a3c-4b52-9f54-ef9e866f3cec",
message: "browser location change required",
},
redirect_browser_to: "/ui/login?flow=ad574ad7-1a3c-4b52-9f54-ef9e866f3cec",
}