Skip to Content
ConceptsHosted Login (Universal Login)

Hosted Login (Universal Login)

The Problem: Where Should the Login Form Live?

Every application that authenticates users must decide where the login form lives. There are three fundamental approaches, and the choice has profound security implications.

Embedded Login (the risky approach)

The application renders its own login form and collects credentials directly:

The credentials are entered on a different origin — one controlled entirely by the identity provider. Your application’s JavaScript cannot read the form fields, intercept keystrokes, or access the credentials in any way. The browser’s same-origin policy enforces this boundary automatically.

RiskEmbedded LoginHosted Login
XSS steals credentialsYes — your JS can read the password fieldNo — different origin, your JS cannot access it
Third-party script reads credentialsYes — any script in your bundle runs in the same originNo — third-party scripts on your origin cannot reach the auth page
Credential phishing via DOM manipulationYes — an attacker can modify your login formNo — the login page is on the IdP’s domain
Password manager autofill scopeYour domainThe IdP’s domain (consistent across all apps)
MFA enforcementYou must implement itThe IdP handles it transparently
Social login integrationYou must integrate each providerThe IdP presents all configured providers
Compliance audit scopeYour entire applicationOnly the IdP’s login pages

Hosted Login is the approach recommended by the OAuth 2.0 Security Best Current Practice (RFC 6819, draft-ietf-oauth-security-topics). Auth0 calls it “Universal Login,” Okta calls it “Okta-hosted Sign-In,” and Auris calls it “Hosted Login.” The security principle is the same: never let your application touch raw credentials.

How It Works: The Complete Flow

Auris Hosted Login implements the OAuth2 Authorization Code flow with PKCE (RFC 7636). Here is the complete sequence, step by step.

Sequence Diagram

PKCE S256: Why It Matters

PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks. The mechanism is simple but effective.

The Code Verifier and Code Challenge

Before initiating the flow, the client generates two values:

  1. Code Verifier: A cryptographically random string (43-128 characters)
  2. Code Challenge: BASE64URL(SHA256(code_verifier))

The client sends the code challenge (the hash) to the authorize endpoint. When exchanging the code for tokens, the client sends the raw code verifier. Auris recomputes SHA256(code_verifier) and verifies it matches the stored code challenge.

code_verifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" SHA-256 + Base64URL code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"

Why This Stops Interception

If an attacker intercepts the authorization code in the redirect URL (via a malicious app registered on the same URI scheme, a proxy, or browser history), they cannot exchange it. To exchange the code, they need the code_verifier — which was never transmitted through the browser URL. It was stored in the client’s memory and only sent directly to the token endpoint over HTTPS.

SHA-256 is a one-way function: given the code challenge, the attacker cannot reverse it to obtain the code verifier.

Auris enforces PKCE S256 on all authorization code flows. The plain challenge method is rejected. There is no configuration to disable PKCE — it is always required.

Session Management

Auris uses two server-side models to manage the hosted login flow:

OAuthSession

Created at step 3 when the user arrives at the authorize endpoint. Stored in the database and identified by an httpOnly cookie.

FieldDescription
sessionTokenUnique identifier (set as httpOnly, Secure, SameSite=Lax cookie)
clientIdThe application requesting authentication
redirectUriWhere to redirect after authentication
codeChallengePKCE code challenge (S256)
codeChallengeMethodAlways S256
stateCSRF state value from the client
scopeRequested OAuth scopes
TTL30 minutes (session expires if user does not complete login)

The OAuthSession tracks the user’s progress through the hosted login pages. It persists across the login page, the 2FA page (if triggered), and any social login redirects. If the user abandons the flow, the session expires and is cleaned up by a cron job.

AuthorizationCode

Created at step 9 after successful authentication.

FieldDescription
codeUnique authorization code
clientIdMust match the token request’s client_id
redirectUriMust exactly match the token request’s redirect_uri
codeChallengeStored for PKCE verification at token exchange
codeChallengeMethodS256
userIdThe authenticated user
TTL5 minutes
usedAtSet atomically on first use (single-use enforcement)

The authorization code is consumed via an atomic Prisma transaction: the code is marked as used and deleted in the same database operation. If two requests attempt to use the same code simultaneously, only one succeeds — the other receives an error.

// Atomic single-use enforcement (simplified) const code = await prisma.authorizationCode.update({ where: { code: authCode, usedAt: null, // Only succeeds if not yet used }, data: { usedAt: new Date(), }, }) // If the code was already used, Prisma throws (record not found with usedAt: null)

Tenant Branding

Auris Hosted Login pages automatically adapt to the tenant’s brand configuration. Tenants configure branding in the Auris Console under Settings > Branding.

SettingEffect on Hosted Login
brandingCompanyNameDisplayed as the page title and in the header
brandingLogoUrlReplaces the default Auris logo on the login page
brandingBackgroundColorSets the page background color (with gradient)
brandingFaviconUrlSets the browser tab favicon

The hosted login pages read branding from the HostedAuthContext, which is resolved from the OAuthSession’s clientId → Application → Tenant chain:

The branding is applied purely with CSS custom properties, so the hosted pages render correctly regardless of the tenant’s color scheme. The Console includes a live preview so administrators can see the effect before publishing.

The Security Pipeline

Every authentication attempt through Hosted Login passes through the full Auris security pipeline. No layer can be bypassed — they execute sequentially, and any layer can block the request.

Each layer is independently configurable per tenant. A small startup might only have Keycloak auth enabled, while an enterprise tenant might have all nine layers active.

All security layers wrap their logic in try/catch blocks to prevent a single layer’s failure from crashing the entire login flow. However, each layer’s default failure mode is to block the request (fail-secure). A crash in the CAPTCHA verification, for example, blocks the login rather than silently passing through.

Social Login on Hosted Pages

Auris supports 9 social login providers on the hosted login pages:

ProviderProtocolIcon
GoogleOIDCGoogle “G”
GitHubOAuth 2.0Octocat
MicrosoftOIDCMicrosoft logo
AppleOIDCApple logo
FacebookOAuth 2.0Facebook “f”
DiscordOAuth 2.0Discord logo
LinkedInOAuth 2.0LinkedIn “in”
Twitter/XOAuth 2.0X logo
SlackOAuth 2.0Slack hash

When a tenant enables social providers in the Console, the hosted login page automatically shows the corresponding buttons. No changes are needed in the client application — social login is handled entirely within the hosted page.

SSO Detection via Email Domain

For tenants with Enterprise SSO (SAML 2.0 / OIDC federation) configured, the hosted login page can detect SSO domains automatically. When the user enters their email address:

  1. The page calls GET /api/auth/sso/[email protected]
  2. If the domain company.com is verified and linked to an SSO connection, the page shows a “Continue with SSO” button
  3. Clicking it redirects through the SSO flow (Keycloak IdP brokering) instead of password authentication

This provides a seamless experience: users from SSO-enabled organizations are automatically directed to their corporate identity provider.

Comparison: Login Approaches

DimensionHosted Login (Auris)Embedded LoginCustom Login Page
Security boundaryBrowser same-origin policy separates credentials from app codeCredentials are accessible to all scripts in the app’s originDepends on implementation
XSS impactXSS in your app cannot steal credentialsXSS in your app can read the password field directlyXSS in your app may or may not reach credentials
CustomizationLogo, colors, favicon, background (tenant branding)Full control over every pixelFull control, but you maintain the code
MFA handlingTransparent — Auris handles TOTP, SMS, WebAuthn, adaptive MFAYou must implement MFA UI and verification logicYou must implement or delegate
Social loginAutomatic — enable in Console, buttons appearYou must integrate each OAuth providerYou must integrate each provider
SSO / SAMLAutomatic — IdP brokering via KeycloakNot available without server-side integrationComplex server-side implementation
MaintenanceZero — Auris updates the login pageYou must keep your auth UI up to dateYou must maintain the page
ComplianceCredentials never touch your origin (clean audit trail)Your application is in scope for credential handlingDepends on architecture
Implementation effortOne SDK call (loginWithRedirect())Build form, call API, handle errors, store tokens, implement MFABuild everything from scratch
Password manager UXConsistent across all apps using the same Auris tenantDifferent autofill target per applicationDifferent autofill target per app

Code Example: Using the Auris SDK

The recommended way to integrate Hosted Login is through the Auris SDK. The SDK handles PKCE generation, state management, redirect handling, and token storage automatically.

JavaScript SDK (@auris/js)

import { AurisClient } from '@auris/js' const auris = new AurisClient({ domain: 'your-tenant.auris.example.com', clientId: 'app_abc123', redirectUri: 'https://app.yourdomain.com/callback', autoRefresh: true, }) // Initiate login — redirects to Hosted Login page // Internally: generates code_verifier, computes S256 challenge, // generates state, stores both in sessionStorage, redirects await auris.loginWithRedirect() // On the callback page (/callback) — exchange code for tokens // Internally: validates state, extracts code from URL, // sends code + code_verifier to token endpoint, stores tokens const result = await auris.handleRedirectCallback() console.log(result.user) // { // id: 'usr_abc123', // email: '[email protected]', // firstName: 'Jane', // lastName: 'Doe', // roles: ['user'], // }

React SDK (@auris/react)

import { AurisProvider, useAuris, AuthGuard } from '@auris/react' // Wrap your app with AurisProvider function App() { return ( <AurisProvider domain="your-tenant.auris.example.com" clientId="app_abc123" redirectUri="https://app.yourdomain.com/callback" > <Router /> </AurisProvider> ) } // Use the hook in any component function LoginButton() { const { loginWithRedirect, isAuthenticated, user, logout } = useAuris() if (isAuthenticated) { return ( <div> <span>Welcome, {user.firstName}</span> <button onClick={() => logout()}>Logout</button> </div> ) } return <button onClick={() => loginWithRedirect()}>Login</button> } // Protect routes with AuthGuard function ProtectedPage() { return ( <AuthGuard> <Dashboard /> </AuthGuard> ) }

Next.js SDK (@auris/nextjs)

// middleware.ts — protect routes at the Edge import { aurisMiddleware } from '@auris/nextjs' export default aurisMiddleware({ protectedPaths: ['/dashboard', '/settings', '/admin'], publicPaths: ['/', '/about', '/pricing'], loginPath: '/auth/login', }) export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'], }
// Server Component — check auth on the server import { getSession } from '@auris/nextjs/server' export default async function DashboardPage() { const session = await getSession() if (!session) { redirect('/auth/login') } return <h1>Welcome, {session.user.firstName}</h1> }

Redirect URI Validation

Auris enforces strict redirect URI validation to prevent authorization code theft via open redirect attacks.

Rules

RuleDescription
Exact matchThe redirect_uri in the token request must exactly match the one sent to the authorize endpoint
Pre-registeredThe redirect_uri must be registered in the Application’s redirectUris array in the Auris Console
No wildcardsWildcard redirect URIs (e.g., https://*.example.com/callback) are not supported
No fragmentsURIs with #fragment components are rejected
HTTPS requiredHTTP redirect URIs are rejected in production (localhost is allowed for development)

If the redirect_uri does not match a registered value, Auris returns an error page directly (it does not redirect to the mismatched URI, as that would defeat the purpose).

Why Exact Match?

Partial matching (e.g., prefix matching like https://app.example.com/) creates a vulnerability: an attacker could register https://app.example.com/evil-page and have authorization codes redirected there. Exact match eliminates this class of attack entirely.

When registering redirect URIs in the Auris Console, include the full path including any trailing components. https://app.example.com/callback and https://app.example.com/callback/ are treated as different URIs. Register exactly the URI your application will use.

Cleanup and Expiration

Auris runs a cron job (/api/oauth/cron/cleanup/) to clean up expired sessions and codes:

ResourceTTLCleanup Behavior
OAuthSession30 minutesDeleted if not completed within TTL
AuthorizationCode5 minutesDeleted if not exchanged within TTL
Used AuthorizationCodesImmediateDeleted atomically on use

The cron runs periodically (configured in vercel.json) and ensures the database does not accumulate stale sessions from abandoned login flows.