Skip to content

Cross-Domain SSO

Overview

Cross-Domain SSO allows users to sign in once and be automatically authenticated across all your applications, even on different domains.

Traditional iframe-based silent auth does not work reliably in modern browsers due to:

  • Safari ITP — Blocks third-party cookies and iframe storage access
  • Chrome Third-Party Cookie Phase-out — Restricting third-party cookies in iframes

Authrim’s trySilentLogin() solves this using top-level navigation — redirecting to the IdP with prompt=none at the full page level, which is not affected by third-party cookie restrictions.

How It Works

sequenceDiagram
    participant App as app.example.com
    participant IdP as auth.example.com

    App->>App: User visits app
    App->>IdP: Redirect (prompt=none, state={returnTo})
    alt IdP has session
        IdP->>App: Redirect with auth code
        App->>App: callback.html handles code exchange
        App->>App: Redirect to returnTo URL
        Note over App: User is now signed in
    else No session
        IdP->>App: Redirect with error=login_required
        App->>App: callback.html redirects with sso_error
        Note over App: Show login button
    end

Setup

1. Initialize with OAuth

const auth = await createAuthrim({
issuer: 'https://auth.example.com',
clientId: 'my-app',
enableOAuth: true,
silentLoginRedirectUri: 'https://app.example.com/callback.html',
});

2. Try Silent Login on Page Load

async function initApp() {
// Check if already authenticated
const { data } = await auth.session.get();
if (data) {
showDashboard(data.user);
return;
}
// Check for SSO error in URL (returned from callback)
const params = new URLSearchParams(window.location.search);
const ssoError = params.get('sso_error');
if (ssoError) {
// SSO failed — clean URL and show login button
window.history.replaceState({}, '', window.location.pathname);
showLoginButton();
return;
}
// Try SSO (first visit only — prevent infinite loop)
const ssoAttempted = sessionStorage.getItem('sso_attempted');
if (!ssoAttempted) {
sessionStorage.setItem('sso_attempted', 'true');
await auth.oauth.trySilentLogin({
onLoginRequired: 'return',
returnTo: window.location.href,
});
// This function redirects and never returns
}
// SSO already attempted — show login button
showLoginButton();
}

3. Handle Callback

In your callback page (callback.html):

const auth = await createAuthrim({
issuer: 'https://auth.example.com',
clientId: 'my-app',
enableOAuth: true,
});
// Handle silent login callback
const result = await auth.oauth.handleSilentCallback();
// handleSilentCallback() handles all redirect logic internally:
// - Success: exchanges code, redirects to returnTo
// - login_required: redirects to returnTo with sso_error param
// - Error: redirects with error details

trySilentLogin Options

interface TrySilentLoginOptions {
/**
* What to do when IdP has no session:
* - 'return': Go back to the app (show login button)
* - 'login': Show the IdP login screen
*
* Default: 'return'
*/
onLoginRequired?: 'return' | 'login';
/** URL to return to after SSO (default: current URL) */
returnTo?: string;
/** Additional OAuth scopes */
scope?: string;
}

onLoginRequired Behavior

ValueSSO SuccessNo IdP Session
'return'Auto-authenticateReturn to app with sso_error=login_required
'login'Auto-authenticateShow IdP login screen

Use 'return' (default) for most cases — it checks for SSO silently and shows your app’s login UI if no session exists. Use 'login' when you want to force the user through the IdP login screen.

handleSilentCallback Return Value

type SilentLoginResult =
| { status: 'success' }
| { status: 'login_required' }
| { status: 'error'; error: string; errorDescription?: string };

In practice, handleSilentCallback() handles redirects internally, so you rarely need to inspect the return value. The callback page typically just calls it and the user is redirected.

Loop Prevention

SSO redirect loops occur when:

  1. App redirects to IdP (prompt=none)
  2. IdP has no session → redirects back with error
  3. App redirects to IdP again → infinite loop

The SDK prevents this with sessionStorage:

// Set flag before SSO attempt
sessionStorage.setItem('sso_attempted', 'true');
// Check flag on page load
if (sessionStorage.getItem('sso_attempted')) {
// Don't attempt SSO again
showLoginButton();
}
// Clear flag on successful login (allow retry after re-login)
sessionStorage.removeItem('sso_attempted');
// Clear flag on explicit logout (allow SSO on next visit)
auth.signOut(); // Internally clears the flag

Security: Open Redirect Prevention

trySilentLogin() validates the returnTo URL to prevent open redirect attacks:

// Safe — same origin
await auth.oauth.trySilentLogin({
returnTo: 'https://app.example.com/dashboard',
});
// Rejected — different origin (throws Error)
await auth.oauth.trySilentLogin({
returnTo: 'https://evil.com/steal',
});
// Error: "returnTo must be same origin"

Multi-App SSO Architecture

For organizations with multiple apps sharing an Authrim IdP:

auth.example.com (Authrim IdP)
├── app1.example.com (App 1 — enableOAuth: true)
├── app2.example.com (App 2 — enableOAuth: true)
└── admin.example.com (Admin — enableOAuth: true)

Each app independently calls trySilentLogin(). When a user logs in to any app, the IdP session is established. Subsequent visits to other apps detect the session via SSO and authenticate automatically.

Configuration Per App

Each app needs:

  1. Client ID — Registered in Authrim Admin panel
  2. Callback URLhttps://app1.example.com/callback.html
  3. Allowed Originshttps://app1.example.com

Complete Example

import { createAuthrim } from '@authrim/web';
const auth = await createAuthrim({
issuer: 'https://auth.example.com',
clientId: 'my-app',
enableOAuth: true,
});
// Main page logic
async function init() {
const { data } = await auth.session.get();
if (data) {
// Clear SSO flag on successful auth
sessionStorage.removeItem('sso_attempted');
showDashboard(data.user);
return;
}
// Check for SSO error
const params = new URLSearchParams(window.location.search);
if (params.get('sso_error')) {
window.history.replaceState({}, '', window.location.pathname);
showLoginButton();
return;
}
// Try SSO once
if (!sessionStorage.getItem('sso_attempted')) {
sessionStorage.setItem('sso_attempted', 'true');
await auth.oauth.trySilentLogin({ onLoginRequired: 'return' });
return; // Never reached
}
showLoginButton();
}
// Logout — clear SSO flag to allow retry
document.getElementById('logout').addEventListener('click', async () => {
await auth.signOut();
sessionStorage.removeItem('sso_attempted');
showLoginButton();
});
init();

Next Steps