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 callbackconst 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 detailstrySilentLogin 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
| Value | SSO Success | No IdP Session |
|---|---|---|
'return' | Auto-authenticate | Return to app with sso_error=login_required |
'login' | Auto-authenticate | Show 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:
- App redirects to IdP (prompt=none)
- IdP has no session → redirects back with error
- App redirects to IdP again → infinite loop
The SDK prevents this with sessionStorage:
// Set flag before SSO attemptsessionStorage.setItem('sso_attempted', 'true');
// Check flag on page loadif (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 flagSecurity: Open Redirect Prevention
trySilentLogin() validates the returnTo URL to prevent open redirect attacks:
// Safe — same originawait 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:
- Client ID — Registered in Authrim Admin panel
- Callback URL —
https://app1.example.com/callback.html - Allowed Origins —
https://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 logicasync 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 retrydocument.getElementById('logout').addEventListener('click', async () => { await auth.signOut(); sessionStorage.removeItem('sso_attempted'); showLoginButton();});
init();Next Steps
- OAuth: Popup Login — Popup-based authentication
- OAuth: Redirect & Silent Auth — Standard OAuth redirect flow
- Session Monitoring — Detect session changes in real time