Skip to content

Cross-Domain SSO

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.

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

In your callback page (callback.html):

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

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.

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 stores silent-login state under authrim:silent_login:{state} and removes it during callback handling. Your app can also keep a simple sso_attempted flag to avoid retrying SSO on every page load:

// 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)
await auth.signOut();
sessionStorage.removeItem('sso_attempted');

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"

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.

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
import { createAuthrim } from '@authrim/web';
const auth = await createAuthrim({
issuer: 'https://auth.example.com',
clientId: 'my-app',
enableOAuth: true,
silentLoginRedirectUri: 'https://app.example.com/callback.html',
});
// 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();