Skip to content

Hosted login

The sign-in screens Authio serves for each client: the steps they ask for, how they look, the languages they speak and how the sign-in returns to your app.

Every client gets a set of screens that Authio hosts and renders on the server: sign-in, the second factor, forgotten password, terms, consent, sign-out and the account pages. Your app never collects a password. It sends the browser to the realm’s authorization endpoint, and Authio walks the person through whatever the client is configured to ask for.

This page covers how a sign-in runs, the steps you can compose per client, what the person sees, branding and languages, and how to run the sign-in in a popup instead of a full redirect.

How a sign-in runs

  1. Your app sends an authorization request

    A standard authorization code request with PKCE, sent to the realm’s authorization endpoint. Any OpenID Connect library builds it for you:

    Authorization requestPlain text
    https://authio.api.hyperio.tech/realms/acme/protocol/openid-connect/auth
      ?client_id=acme-web
      &response_type=code
      &redirect_uri=http://localhost:3000/api/auth/callback/authio
      &scope=openid profile email
      &state=STATE
      &code_challenge=CODE_CHALLENGE
      &code_challenge_method=S256
  2. Authio looks for a session it can reuse

    If the client takes part in single sign-on and the browser already holds a valid sign-in to the realm, the person goes straight through, or picks an account from the chooser. Otherwise the browser lands on the client’s sign-in screen, /realms/acme/clients/acme-web/login, carrying the whole request with it.

  3. The hosted login runs the client’s steps

    Password, passkey, a provider button, then whatever second factor the client still owes. After the credential is proven, Authio applies the realm’s checks in order: suspension, account status and access to this client, the sign-in address, password expiry, and only then the second factor.

  4. Terms and consent, when the client asks for them

    With the account settled, the request goes back through the authorization endpoint. The terms screen comes first if the account has not accepted the current version, then the consent screen if the client asks for consent and a scope is still ungranted.

  5. The browser returns to your app

    Authio redirects to the registered redirect URI with code, state and iss. Your app exchanges the code for tokens as usual.

The request is never lost between steps

Authio keeps the original authorization request for the whole sign-in, including steps driven by script such as a passkey ceremony or a push approval, and resumes it when the last step completes. The return address is always the one registered on the client, never one taken from the address bar.

Sign-in steps

Each client chooses, in order, which steps its sign-in is made of. Clients of the same realm can differ: one asks for a password only, another for a passkey, a third for a password plus a phone approval.

StepWhat it asks forNotes
loginUsername and password.The only step that can admit an account with nothing enrolled. The provider buttons appear under it.
mfaA 6-digit code sent to the account’s e-mail address.Asked after a first factor; the same code also reaches the account’s paired phones. See Multi-factor.
passkeyA passkey on this device: fingerprint, face, screen lock or a security key.Works as the first step or as a second factor. See Passkeys.
pushAn approval in the Authio app, matching a number shown on the screen.Always a second factor. See Multi-factor.
terms_acceptanceAcceptance of the current version of the client’s terms.Always asked after sign-in, wherever it sits in the list. See Sessions and SSO.

Requirement and registration

The passkey and push steps carry two settings. Requirement says how strongly the step is asked for; Registration says what happens to an account that does not have the factor yet.

SettingValueEffect
RequirementPreferred (default)Asked only of accounts that can already answer it: a registered passkey or a paired phone.
RequirementOptionalNever blocks a sign-in. An authorization request with acr_values can raise it to Preferred.
RequirementRequiredAsked of every account, including one with nothing enrolled.
RegistrationOffer (default)An account without the factor is offered to set it up and can decline.
RegistrationNeverNothing is offered during sign-in; the factor is set up from the account pages.
RegistrationRequireThe account has to set the factor up before it can continue.

Combinations that would lock people out are refused when you save the client, with the reason on the field:

  • A Required step with registration Never on a client that has a login step: an account without the factor could never get in.
  • An Optional step with registration Require: the account would enrol a factor that is never asked for.
  • An mfa step marked Optional: the code would never be sent. Remove the step instead.
  • A step that offers registration on a client with no login step: there is no session to register from, so set registration to Never.

Configure the steps

  1. Open the client’s login layout

    In Clients, open the client and choose Edit, then the Login layout step. The steps come first, top to bottom in the order the screen asks for them.

  2. Add, remove and order the steps

    Move each step up or down, and set Requirement and Registration for passkey and push. Terms acceptance is a separate switch marked After sign-in; its documents and version are set in the client’s Configuration.

  3. Save

    The new steps apply from the next sign-in. A client that uses the authorization code flow needs at least one step; the save is refused without it.

What the person sees

Around the steps, the sign-in screen offers:

  • Remember me, which keeps the session after the browser closes.
  • Forgot password?, which opens the password reset for this client.
  • One Continue with … button per identity provider the client offers, under the sign-in button.
  • Passkey autofill in the username field, when the client also has a passkey step.
  • Start over, which drops what was entered so far and reopens the first step with the same request.
  • The account chooser, when the browser already holds more than one reusable sign-in.

An unknown username and a wrong password get the same answer, so the screen never reveals which accounts exist. Every other refusal is shown only after the password was accepted, and says what to do next:

The person seesWhyHow it is resolved
Those details did not matchWrong username or password.Try again; repeated failures count toward the realm’s lockout.
Too many attemptsThe account is locked by the realm’s lockout rules.The message gives the duration; resetting the password gets in sooner.
Address not confirmed yetThe account’s e-mail address was never confirmed.The screen offers to send the confirmation e-mail again.
No access to this applicationThe client admits only linked accounts and this one is not linked.An administrator grants access from the client’s users.
Confirm this locationFirst sign-in from a new address.The screen waits and continues by itself once the address is approved. See address approval.
Account suspendedA suspension is in force.The message says when it ends, and the reason if the administrator chose to show it.
Your password has expiredThe realm requires periodic renewal.The person is taken to renew it by e-mail.
This account is not activeThe subscription behind the application has lapsed.Sign-in resumes when the subscription is renewed.

Failures that no retry can fix, such as a request whose client_id names a different client than the screen, replace the form with an error card that carries a reference code support can find in the logs.

Branding

All hosted screens of a client wear its appearance: sign-in, recovery, terms, consent, errors, device verification and the account pages. You set it in the same Login layout step.

SettingWhat it controlsLimits
ArrangementPanel beside (brand panel next to the form, the default), Centered (one column, card in the middle) or Cover (brand across the page, card raised over it).—
Title and subtitleThe words above the form.Up to 200 and 500 characters.
ColorsPrimary, secondary and background, once for the light theme and once for the dark one.Hexadecimal #RGB or #RRGGBB.
Logo and background imageImages loaded from your own URLs. Without a logo, the platform’s is shown.Up to 500 characters; the background image must use https.
Custom CSSYour own rules on top of the screens.Up to 4,000 characters, sanitized (see below).

Custom CSS cannot contain <, expression(, javascript:, vbscript:, behavior: or -moz-binding, and cannot load url() or @import over plain http. It is checked when you save and sanitized again when the screen is drawn.

A plan feature

Customizing the appearance needs a plan that includes the custom sign-in layout (features.AuthLayoutCustom). On other plans the console shows the Login layout step locked, with the reason and a way to the plans, and the API refuses appearance changes with AuthLayoutNotIncluded. See Plans and billing.

When an authorization request names an organization with organization, that organization’s logo, primary color and background color replace the client’s; the arrangement, the texts and the CSS stay the client’s.

Light and dark

Pass theme=light or theme=dark on the authorization request to choose the palette people start with. The screens also have a toggle; a person’s own choice is kept in a cookie and wins until the next request that names a theme.

Languages

The hosted screens are written in English (en) and Brazilian Portuguese (pt-BR). The language is settled once per sign-in and kept across every step:

  1. ui_locales on the authorization request, a space-separated list in order of preference.
  2. The browser’s Accept-Language header, respecting its weights.
  3. English.

Errors are worded from their code in the visitor’s language. Every hosted page, the account pages included, also accepts ui_locales and theme directly in its address.

Parameters the screens read

Besides the standard OAuth parameters, these change what the hosted login shows:

ParameterValuesEffect
ui_localesen, pt-BRLanguage of every screen in the sign-in.
themelight, darkThe palette the screens start in.
displaypage (default), popuppopup draws the screens centered, to fit a small window.
response_modequery (default), web_messageweb_message hands the response to the window that opened the popup.
promptlogin, select_account, consent, noneAsk for credentials again, show the account chooser, show the consent screen, or never show a screen.
max_ageSecondsA sign-in older than this is asked for again.
acr_values1, 2, 3Asks for a stronger sign-in, best effort. See step-up.
organizationAn organization aliasUses that organization’s branding and identity providers.

To keep your page where it is, open the authorization request in a popup window. Add display=popup so the screens fit the window, and response_mode=web_message so that, at the end, Authio posts the response to the window that opened the popup and closes it, instead of navigating the popup to your redirect URI.

The message is an object whose type is authorization_response and whose response holds code, state and iss (or error). It is posted only to the origin of the redirect URI the request named, so the page that opens the popup has to be on that origin. If no window is waiting — the popup was opened as a tab, or its opener navigated away — the popup falls back to an ordinary redirect.

sign-in-popup.tsTypeScript
const ISSUER = 'https://authio.api.hyperio.tech/realms/acme'
const AUTHIO_ORIGIN = new URL(ISSUER).origin

export function signInWithPopup(
  state: string,
  codeChallenge: string,
): Promise<Record<string, string>> {
  const authorize = new URL(ISSUER + '/protocol/openid-connect/auth')
  authorize.search = new URLSearchParams({
    client_id: 'acme-web',
    response_type: 'code',
    redirect_uri: 'http://localhost:3000/callback',
    scope: 'openid profile email',
    state,
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
    display: 'popup',
    response_mode: 'web_message',
  }).toString()

  const popup = window.open(authorize.toString(), 'authio-sign-in', 'popup,width=480,height=720')

  return new Promise((resolve, reject) => {
    if (!popup) {
      reject(new Error('popup_blocked'))
      return
    }

    function onMessage(event: MessageEvent) {
      if (event.origin !== AUTHIO_ORIGIN || event.source !== popup) return
      if (event.data?.type !== 'authorization_response') return

      window.removeEventListener('message', onMessage)
      resolve(event.data.response)
    }

    window.addEventListener('message', onMessage)
  })
}

Check that state matches the one you sent, then exchange code with your PKCE verifier exactly as after a redirect. Call the function straight from a click handler, or the browser blocks the popup.

On Next.js, authio-provider-nextauth 2.1.0 and later ship the popup flow ready-made in authio-provider-nextauth/popup, with the code exchange still done by Auth.js on your server. See Next.js.

Where accounts come from

The hosted login has no self-service sign-up form. An account reaches a realm in one of three ways:

Next steps