クロスドメインSSO
概要
クロスドメインSSOにより、ユーザーは一度サインインするだけで、異なるドメインのアプリケーションでも自動的に認証されます。
従来のiframeベースのサイレント認証は、モダンブラウザでは信頼性が低くなっています:
- Safari ITP — サードパーティCookieとiframeストレージアクセスをブロック
- Chrome サードパーティCookie廃止 — iframe内のサードパーティCookieを制限
Authrimの trySilentLogin() はこの問題をトップレベルナビゲーションで解決します — ページ全体レベルで prompt=none でIdPにリダイレクトするため、サードパーティCookie制限の影響を受けません。
動作原理
sequenceDiagram
participant App as app.example.com
participant IdP as auth.example.com
App->>App: ユーザーがアプリにアクセス
App->>IdP: リダイレクト(prompt=none, state={returnTo})
alt IdPにセッションあり
IdP->>App: 認可コード付きリダイレクト
App->>App: callback.htmlがコード交換を処理
App->>App: returnTo URLにリダイレクト
Note over App: ユーザーはサインイン済み
else セッションなし
IdP->>App: error=login_required付きリダイレクト
App->>App: callback.htmlがsso_error付きでリダイレクト
Note over App: ログインボタンを表示
end
セットアップ
1. OAuthを有効にして初期化
const auth = await createAuthrim({ issuer: 'https://auth.example.com', clientId: 'my-app', enableOAuth: true, silentLoginRedirectUri: 'https://app.example.com/callback.html',});2. ページ読み込み時にサイレントログインを試行
async function initApp() { // 既に認証済みか確認 const { data } = await auth.session.get(); if (data) { showDashboard(data.user); return; }
// URLにSSOエラーがあるか確認(コールバックから返された) const params = new URLSearchParams(window.location.search); const ssoError = params.get('sso_error');
if (ssoError) { // SSO失敗 — URLをクリーンにしてログインボタンを表示 window.history.replaceState({}, '', window.location.pathname); showLoginButton(); return; }
// SSO試行(初回アクセスのみ — 無限ループ防止) const ssoAttempted = sessionStorage.getItem('sso_attempted'); if (!ssoAttempted) { sessionStorage.setItem('sso_attempted', 'true');
await auth.oauth.trySilentLogin({ onLoginRequired: 'return', returnTo: window.location.href, }); // この関数はリダイレクトして戻りません }
// SSO試行済み — ログインボタンを表示 showLoginButton();}3. コールバック処理
コールバックページ(callback.html)で:
const auth = await createAuthrim({ issuer: 'https://auth.example.com', clientId: 'my-app', enableOAuth: true,});
// サイレントログインコールバックを処理const result = await auth.oauth.handleSilentCallback();
// handleSilentCallback() はすべてのリダイレクトロジックを内部で処理:// - 成功: コード交換、returnToにリダイレクト// - login_required: sso_errorパラメータ付きでreturnToにリダイレクト// - エラー: エラー詳細付きでリダイレクトtrySilentLoginオプション
interface TrySilentLoginOptions { /** * IdPにセッションがない場合の動作: * - 'return': アプリに戻る(ログインボタンを表示) * - 'login': IdPのログイン画面を表示 * * デフォルト: 'return' */ onLoginRequired?: 'return' | 'login';
/** SSO後に戻るURL(デフォルト: 現在のURL) */ returnTo?: string;
/** 追加のOAuthスコープ */ scope?: string;}onLoginRequiredの動作
| 値 | SSO成功時 | IdPセッションなし時 |
|---|---|---|
'return' | 自動認証 | sso_error=login_required でアプリに戻る |
'login' | 自動認証 | IdPのログイン画面を表示 |
ほとんどの場合は 'return'(デフォルト)を使用してください — SSOをサイレントに確認し、セッションがなければアプリのログインUIを表示します。IdPのログイン画面をユーザーに強制的に表示したい場合は 'login' を使用します。
handleSilentCallbackの戻り値
type SilentLoginResult = | { status: 'success' } | { status: 'login_required' } | { status: 'error'; error: string; errorDescription?: string };実際には handleSilentCallback() がリダイレクトを内部で処理するため、戻り値を検査する必要はほとんどありません。コールバックページは単にこれを呼び出すだけで、ユーザーはリダイレクトされます。
ループ防止
SSOリダイレクトループは以下の場合に発生します:
- アプリがIdPにリダイレクト(prompt=none)
- IdPにセッションなし → エラーで戻る
- アプリが再びIdPにリダイレクト → 無限ループ
SDKは sessionStorage でこれを防止します:
// SSO試行前にフラグを設定sessionStorage.setItem('sso_attempted', 'true');
// ページ読み込み時にフラグを確認if (sessionStorage.getItem('sso_attempted')) { // SSO再試行しない showLoginButton();}
// ログイン成功時にフラグをクリア(再ログイン後のリトライを許可)sessionStorage.removeItem('sso_attempted');
// 明示的ログアウト時にフラグをクリア(次回訪問時のSSOを許可)auth.signOut(); // 内部でフラグをクリアセキュリティ:オープンリダイレクト防止
trySilentLogin() は returnTo URLを検証してオープンリダイレクト攻撃を防止します:
// 安全 — 同一オリジンawait auth.oauth.trySilentLogin({ returnTo: 'https://app.example.com/dashboard',});
// 拒否 — 異なるオリジン(Errorがスロー)await auth.oauth.trySilentLogin({ returnTo: 'https://evil.com/steal',});// Error: "returnTo must be same origin"マルチアプリSSOアーキテクチャ
Authrim IdPを共有する複数アプリの組織向け:
auth.example.com (Authrim IdP)├── app1.example.com (アプリ1 — enableOAuth: true)├── app2.example.com (アプリ2 — enableOAuth: true)└── admin.example.com (管理 — enableOAuth: true)各アプリが独立して trySilentLogin() を呼び出します。ユーザーがいずれかのアプリにログインするとIdPセッションが確立されます。その後の他のアプリへのアクセスではSSOでセッションが検出され、自動的に認証されます。
アプリごとの設定
各アプリに必要なもの:
- クライアントID — Authrim管理パネルで登録
- コールバックURL —
https://app1.example.com/callback.html - 許可されたオリジン —
https://app1.example.com
完全な例
import { createAuthrim } from '@authrim/web';
const auth = await createAuthrim({ issuer: 'https://auth.example.com', clientId: 'my-app', enableOAuth: true,});
// メインページロジックasync function init() { const { data } = await auth.session.get();
if (data) { // 認証成功時にSSOフラグをクリア sessionStorage.removeItem('sso_attempted'); showDashboard(data.user); return; }
// SSOエラーを確認 const params = new URLSearchParams(window.location.search); if (params.get('sso_error')) { window.history.replaceState({}, '', window.location.pathname); showLoginButton(); return; }
// SSOを一度試行 if (!sessionStorage.getItem('sso_attempted')) { sessionStorage.setItem('sso_attempted', 'true'); await auth.oauth.trySilentLogin({ onLoginRequired: 'return' }); return; // ここには到達しない }
showLoginButton();}
// ログアウト — SSOフラグをクリアしてリトライを許可document.getElementById('logout').addEventListener('click', async () => { await auth.signOut(); sessionStorage.removeItem('sso_attempted'); showLoginButton();});
init();次のステップ
- OAuth: ポップアップログイン — ポップアップベースの認証
- OAuth: リダイレクト&サイレント認証 — 標準OAuthリダイレクトフロー
- セッション監視 — リアルタイムのセッション変更検知