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.
| Risk | Embedded Login | Hosted Login |
|---|---|---|
| XSS steals credentials | Yes — your JS can read the password field | No — different origin, your JS cannot access it |
| Third-party script reads credentials | Yes — any script in your bundle runs in the same origin | No — third-party scripts on your origin cannot reach the auth page |
| Credential phishing via DOM manipulation | Yes — an attacker can modify your login form | No — the login page is on the IdP’s domain |
| Password manager autofill scope | Your domain | The IdP’s domain (consistent across all apps) |
| MFA enforcement | You must implement it | The IdP handles it transparently |
| Social login integration | You must integrate each provider | The IdP presents all configured providers |
| Compliance audit scope | Your entire application | Only 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:
- Code Verifier: A cryptographically random string (43-128 characters)
- 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.
| Field | Description |
|---|---|
sessionToken | Unique identifier (set as httpOnly, Secure, SameSite=Lax cookie) |
clientId | The application requesting authentication |
redirectUri | Where to redirect after authentication |
codeChallenge | PKCE code challenge (S256) |
codeChallengeMethod | Always S256 |
state | CSRF state value from the client |
scope | Requested OAuth scopes |
TTL | 30 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.
| Field | Description |
|---|---|
code | Unique authorization code |
clientId | Must match the token request’s client_id |
redirectUri | Must exactly match the token request’s redirect_uri |
codeChallenge | Stored for PKCE verification at token exchange |
codeChallengeMethod | S256 |
userId | The authenticated user |
TTL | 5 minutes |
usedAt | Set 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.
| Setting | Effect on Hosted Login |
|---|---|
brandingCompanyName | Displayed as the page title and in the header |
brandingLogoUrl | Replaces the default Auris logo on the login page |
brandingBackgroundColor | Sets the page background color (with gradient) |
brandingFaviconUrl | Sets 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:
| Provider | Protocol | Icon |
|---|---|---|
| OIDC | Google “G” | |
| GitHub | OAuth 2.0 | Octocat |
| Microsoft | OIDC | Microsoft logo |
| Apple | OIDC | Apple logo |
| OAuth 2.0 | Facebook “f” | |
| Discord | OAuth 2.0 | Discord logo |
| OAuth 2.0 | LinkedIn “in” | |
| Twitter/X | OAuth 2.0 | X logo |
| Slack | OAuth 2.0 | Slack 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:
- The page calls
GET /api/auth/sso/[email protected] - If the domain
company.comis verified and linked to an SSO connection, the page shows a “Continue with SSO” button - 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
| Dimension | Hosted Login (Auris) | Embedded Login | Custom Login Page |
|---|---|---|---|
| Security boundary | Browser same-origin policy separates credentials from app code | Credentials are accessible to all scripts in the app’s origin | Depends on implementation |
| XSS impact | XSS in your app cannot steal credentials | XSS in your app can read the password field directly | XSS in your app may or may not reach credentials |
| Customization | Logo, colors, favicon, background (tenant branding) | Full control over every pixel | Full control, but you maintain the code |
| MFA handling | Transparent — Auris handles TOTP, SMS, WebAuthn, adaptive MFA | You must implement MFA UI and verification logic | You must implement or delegate |
| Social login | Automatic — enable in Console, buttons appear | You must integrate each OAuth provider | You must integrate each provider |
| SSO / SAML | Automatic — IdP brokering via Keycloak | Not available without server-side integration | Complex server-side implementation |
| Maintenance | Zero — Auris updates the login page | You must keep your auth UI up to date | You must maintain the page |
| Compliance | Credentials never touch your origin (clean audit trail) | Your application is in scope for credential handling | Depends on architecture |
| Implementation effort | One SDK call (loginWithRedirect()) | Build form, call API, handle errors, store tokens, implement MFA | Build everything from scratch |
| Password manager UX | Consistent across all apps using the same Auris tenant | Different autofill target per application | Different 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
| Rule | Description |
|---|---|
| Exact match | The redirect_uri in the token request must exactly match the one sent to the authorize endpoint |
| Pre-registered | The redirect_uri must be registered in the Application’s redirectUris array in the Auris Console |
| No wildcards | Wildcard redirect URIs (e.g., https://*.example.com/callback) are not supported |
| No fragments | URIs with #fragment components are rejected |
| HTTPS required | HTTP 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:
| Resource | TTL | Cleanup Behavior |
|---|---|---|
| OAuthSession | 30 minutes | Deleted if not completed within TTL |
| AuthorizationCode | 5 minutes | Deleted if not exchanged within TTL |
| Used AuthorizationCodes | Immediate | Deleted atomically on use |
The cron runs periodically (configured in vercel.json) and ensures the database does not accumulate stale sessions from abandoned login flows.
Related Concepts
- PKCE Flow — Detailed walkthrough of the PKCE mechanism used by Hosted Login
- OAuth 2.0 & OIDC — The authorization framework Hosted Login implements
- Sessions & Token Rotation — How tokens are managed after Hosted Login completes
- Adaptive MFA & Risk Scoring — Risk-based step-up MFA during the login flow
- Actions & Sandboxed Execution — Custom logic hooks that run during Hosted Login
- Tokens Explained — The access, refresh, and ID tokens returned after login