Single Sign-On Continuation with Custom UI¶
When an end-user already has an active Authany session in their browser, your Custom UI can show a "Continue as user@example.com" option instead of asking them to enter their credentials again. This is powered by select_account, an identification option you can add to the identify step of login and signup_login flows.
select_account works through the same Authentication Flow API endpoints your Custom UI already calls. There is no new endpoint and no new token. When the browser has an eligible session, the identify step's response includes an extra option describing the account. When there is none, the response is unchanged, so a Custom UI that does not recognize the option keeps working as before.
If you are new to building a Custom UI, read the Authentication Flow API overview first.
Use cases¶
Single sign-on across apps in the same project. Two applications, App A and App B, are OAuth clients of the same Authany project and share the same Custom UI. A user signs in to App A, then opens App B for the first time in the same browser. Because the Authany session cookie is shared, the Custom UI can offer to continue as the same account. The user clicks once instead of typing their credentials again.
A single "Continue" entry point. Your Custom UI uses one combined screen (a signup_login flow) instead of separate sign-in and sign-up pages. When a session already exists, the screen shows "Continue as user@example.com" alongside the usual email and social login options. To decline, the user picks another option instead, and can even register a second account that way.
Prerequisites¶
- Your Custom UI must be same-site with your Authany endpoint. The browser only sends the Authany session cookie to the Authentication Flow API when your Custom UI and your Authany endpoint share a registrable domain (the eTLD+1, such as
example.com): for exampleauth.example.comandui.example.com. In practice this means setting up a Custom Domain for your project. A Custom UI hosted on an unrelated domain never sees theselect_accountoption. - Custom UI URI configured. Your OAuth client must have its Custom UI URI set in the Authany Portal (Applications > your application > Custom UI). Note that same-site is not the same as same-origin:
ui.example.comandauth.example.comare still cross-origin, so the browser applies CORS to these API calls. Authany allows cross-origin requests with credentials from origins registered as a Custom UI URI, so no extra CORS setup is needed on your side. - Send the session cookie from the browser. Calls to the Authentication Flow API made with
fetch()must usecredentials: 'include', otherwise the session cookie is not sent and the option never appears.
flowchart TD
appa["App A"] --> ui
appb["App B"] --> ui
subgraph samesite ["example.com — same registrable domain"]
ui["Shared Custom UI<br/>ui.example.com"]
authgear["Authgear<br/>auth.example.com"]
end
other["Custom UI on an unrelated domain<br/>ui.other-domain.io"]
ui -- "session cookie sent,<br/>select_account offered" --> authgear
other -. "cookie never sent,<br/>option never appears" .-> authgear
Note
If your Custom UI has separate sign-in and sign-up screens, its first call to create a flow must be of type login or signup_login, never signup directly. select_account only exists in those two flow types, so starting with signup means you never find out that a session exists. Create a separate signup flow afterward only if the user has no eligible session (or declines it) and wants a new account.
How it works¶
The select-account exchange sits inside the same OAuth flow every Custom UI already follows. The only new parts are the extra option in the identify response and the one-click input that completes it:
sequenceDiagram
participant App as Your App
participant CustomUI as Custom UI (ui.example.com)
participant Authgear as Authgear (auth.example.com)
Note over App,Authgear: The browser already holds an Authgear session cookie
App->>Authgear: GET /oauth2/authorize?client_id=...
Authgear-->>CustomUI: 302 to Custom UI URI (client_id, x_ref, ...)
CustomUI->>Authgear: POST /api/v1/authentication_flows<br/>fetch with credentials: 'include' — cookie sent (same-site)
Authgear-->>CustomUI: identify options include select_account<br/>(display_name, user_id)
Note over CustomUI: Render "Continue as user@example.com"
CustomUI->>Authgear: POST /api/v1/authentication_flows/states/input<br/>{ "identification": "select_account", "index": 0 }
Authgear-->>CustomUI: action.type: finished (finish_redirect_uri)
CustomUI->>Authgear: Top-level navigation to finish_redirect_uri
Authgear-->>App: 302 to redirect_uri with authorization code
App->>Authgear: POST /oauth2/token (exchange code for tokens)
The user never typed a credential; the only interaction was the click on "Continue as user@example.com". When there is no eligible session, the flow-creation response has no select_account entry and your UI proceeds as a normal login.
Step 1: Add select_account to your flow config¶
In the Authany Portal, go to Advanced > Edit Config and add select_account to the identify step of your login flow, under the authentication_flow section:
The select_account entry above has no nested steps, so choosing it completes the login immediately. To ask for something extra first (for example a 2FA code), give the entry its own nested authenticate step. See Require 2FA on continuation below.
Step 2: Detect the option in your Custom UI¶
Create the flow as usual, forwarding the query parameters Authany passed to your Custom UI URI (in particular client_id and x_ref) via url_query:
const response = await fetch("https://auth.example.com/api/v1/authentication_flows", {
method: "POST",
credentials: "include", // required: sends the Authgear session cookie
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "login",
name: "default",
url_query: window.location.search.substring(1),
}),
});
When the browser has an eligible session, the identify step's options include a select_account entry with the account's display name and user ID:
{
"result": {
"state_token": "authflowstate_VGHZ8SBCKGZK2KW84TCAKWGM8QZH0B69",
"type": "login",
"name": "default",
"action": {
"type": "identify",
"data": {
"type": "identification_data",
"options": [
{
"identification": "select_account",
"display_name": "user@example.com",
"user_id": "user_01J8ZC0M2N3P4Q5R6S7T8V9W0X"
},
{
"identification": "email"
}
]
}
}
}
}
Use display_name to render the "Continue as user@example.com" button. Unlike masked_display_name elsewhere in this API, it is returned unmasked because it identifies the account already bound to the caller's own session cookie.
When there is no eligible session, the select_account entry is absent and the rest of the response is unchanged.
Step 3: Submit the selection¶
When the user clicks "Continue as...", submit the option by its index, its position in the options array:
{
"state_token": "authflowstate_VGHZ8SBCKGZK2KW84TCAKWGM8QZH0B69",
"input": {
"identification": "select_account",
"index": 0
}
}
With the minimal config from Step 1, the flow finishes immediately:
{
"result": {
"state_token": "authflowstate_ABCJVB0IJKLQ2S1K2G34RX56R1C1E789",
"type": "login",
"name": "default",
"action": {
"type": "finished",
"data": {
"finish_redirect_uri": "https://auth.example.com/oauth2/consent?..."
}
}
}
}
Redirect the browser to finish_redirect_uri with a top-level navigation (not a fetch() call), exactly as for any other completed flow.
Note
user_id is informational and read-only. The input only ever carries index; the server re-resolves the account from its own session cookie at submission time, so a forged or stale user_id can never be used to select a different account.
Step 4: Handle the user declining¶
There is no dedicated "decline" input. If the user wants to sign in as someone else, submit any other option's input instead, for example:
{
"state_token": "authflowstate_VGHZ8SBCKGZK2KW84TCAKWGM8QZH0B69",
"input": {
"identification": "email",
"login_id": "another-user@example.com"
}
}
The flow then proceeds exactly as a normal login.
Using select_account in signup_login flows¶
In a signup_login flow, select_account declares a login_flow only; it can only ever continue an existing login, never a signup. Choosing it switches into the named login flow and replays the same identify input there, so that login flow must itself declare a select_account entry. If it does not, the replayed input matches no option in the target flow and the submission fails instead of completing the login:
Any steps configured after identify in the target login flow (for example terminate_other_sessions) still run; continuing via select_account does not skip them.
Require 2FA on continuation¶
To ask for a fresh second factor when continuing with an existing session, without adding that step to normal logins, give the select_account entry its own nested authenticate step:
Submitting select_account then returns an authenticate action instead of finishing:
{
"result": {
"state_token": "authflowstate_XYZ3JVB0IJKLQ2S1K2G34RX56R1C1E78",
"type": "login",
"name": "default",
"action": {
"type": "authenticate",
"authentication": "secondary_totp",
"data": {
"type": "authentication_data",
"options": [
{ "authentication": "secondary_totp" }
]
}
}
}
}
Submit the TOTP code as usual to complete the flow.
Enforcing login_hint or id_token_hint¶
If your application passes login_hint or id_token_hint in the authorization request, Authany forwards both parameters on the redirect to your Custom UI URI, alongside client_id and x_ref.
The Authentication Flow API deliberately does not filter the select_account option by these hints; the option is offered whenever an eligible session exists. If you want to offer continuation only when it matches the hint, implement the check in your Custom UI: resolve the hint yourself, compare it against the option's user_id, and hide the option on a mismatch, falling back to whatever your UI does when there is no eligible session. For id_token_hint, that means decoding the ID token and comparing its sub claim against the option's user_id. A Custom UI that does not care about hints can ignore user_id entirely.
Error handling¶
If the session changes between the option being shown and the input being submitted (for example, the user logged out or switched accounts in another tab), the API responds with:
{
"error": {
"name": "Unauthorized",
"reason": "SelectAccountSessionChanged",
"message": "session no longer matches the selected account",
"code": 401
}
}
Handle this by restarting the flow: create a new authentication flow and render whatever options the fresh response contains.
When the option will not appear¶
The select_account option is omitted, and the response is the same as if the feature were not configured, when any of the following holds:
- There is no active session, or the session cookie was not sent (missing
credentials: 'include', or the Custom UI is not same-site with Authany). - The flow config's
identifystep does not listselect_account. - The authorization request carries
prompt=login, or itsmax_agehas expired (which Authany treats asprompt=login). - The session was established with SSO disabled — for example an SDK configured with
isSSOEnabled: false, which suppresses the shared session cookie (see SSO with mobile apps / websites).
Warning
prompt=select_account in the authorization request is unrelated to this feature and has no effect, despite sharing the name.