Smart Handoff SSO
このコンテンツはまだ日本語訳がありません。
Overview
Section titled “Overview”Smart Handoff SSO enables secure cross-domain Single Sign-On by transferring authentication state from an Authorization Server (AS) to a Relying Party (RP) through a secure handoff token.
This approach is ideal when:
- You want seamless SSO without user interaction
- You need to bridge authentication between different applications
- You want to avoid iframe-based flows entirely
How It Works
Section titled “How It Works”sequenceDiagram
participant User
participant AS as Authorization Server
participant RP as Relying Party App
User->>AS: Already authenticated
User->>AS: Click "Open App" button
AS->>AS: Generate handoff_token
AS->>RP: Redirect with handoff_token + state
RP->>AS: POST /handoff/verify
AS->>RP: Return access_token + session + user
RP->>RP: Save access token using configured token storage
RP->>User: Redirect to app (authenticated)
Security Features:
- Short-lived tokens: Handoff tokens expire quickly (typically 60 seconds)
- One-time use: Tokens can only be used once
- State CSRF protection: State parameter validated against handoff-specific namespace
- Referrer protection: Token immediately removed from URL
1. Initialize SDK
Section titled “1. Initialize SDK”import { createAuthrim } from '@authrim/web';
const auth = await createAuthrim({ issuer: 'https://auth.example.com', clientId: 'my-rp-client-id', storage: { storage: 'memory', // default; use 'sessionStorage' only if reload persistence is required },});2. Handle Handoff Callback
Section titled “2. Handle Handoff Callback”In your callback page (callback.html):
<!DOCTYPE html><html><head> <title>Processing Authentication</title></head><body> <div id="loading">Processing authentication...</div> <div id="error" style="display:none"></div>
<script src="https://unpkg.com/@authrim/web@latest/dist/authrim-web.umd.global.js"></script> <script> async function handleHandoffCallback() { const params = new URLSearchParams(window.location.search); const handoffToken = params.get('handoff_token'); const state = params.get('state');
if (!handoffToken || !state) { return false; // Not a handoff callback }
console.log('[Authrim] Handoff callback detected');
// Remove token from URL immediately (Referrer protection) history.replaceState(null, '', window.location.pathname);
try { const auth = await AuthrimWeb.createAuthrim({ issuer: 'https://auth.example.com', clientId: 'my-rp-client-id', });
// Verify token, save access token, cleanup handoff storage await auth.handoff.verifyAndSave(handoffToken, state);
console.log('[Authrim] Handoff login successful');
// Redirect to app window.location.href = '/'; return true;
} catch (error) { console.error('[Authrim] Handoff error:', error);
if (error.code === 'state_mismatch') { showError('CSRF attack detected. Please try again.'); } else { showError('Authentication failed. Please try again.'); }
return true; } }
// Initialize (async () => { const handled = await handleHandoffCallback(); if (!handled) { // Not a handoff callback, handle other flows // ... } })(); </script></body></html>What verifyAndSave() Does
Section titled “What verifyAndSave() Does”The auth.handoff.verifyAndSave() method automatically:
- Verifies handoff token with the Authorization Server
- Validates state parameter against handoff-specific namespace (CSRF protection)
- Saves access token using the configured session storage policy (memory by default,
sessionStoragewhen explicitly configured) - Cleans up sessionStorage (only handoff-specific keys)
- Emits
auth:loginevent for reactive state updates
Advanced Usage
Section titled “Advanced Usage”Low-Level Token Verification
Section titled “Low-Level Token Verification”If you need more control, use auth.handoff.verify():
const tokenData = await auth.handoff.verify( handoffToken, state, clientId, { include: 'session,user' });
// Token data contains:// - access_token// - expires_in// - session: { id, userId, createdAt, expiresAt }// - user: { id, email, name, emailVerified }
// Prefer verifyAndSave() when you want the SDK to store the access token.Get Storage Key
Section titled “Get Storage Key”For debugging or advanced use cases:
const storageKey = auth.session.getStorageKey();console.log('Storage key:', storageKey);
// Only sessionStorage configurations persist a token under this key.// Memory-only clients keep token material in memory.Error Handling
Section titled “Error Handling”The SDK provides unified error handling:
try { await auth.handoff.verifyAndSave(handoffToken, state);} catch (error) { if (error.code === 'invalid_token') { // Token invalid or expired console.error('Token verification failed:', error.message); } else if (error.code === 'state_mismatch') { // CSRF attack detected console.error('State mismatch - CSRF protection:', error.message); } else { // Other errors console.error('Handoff error:', error); }}Security Considerations
Section titled “Security Considerations”State Parameter
Section titled “State Parameter”The SDK uses a handoff-specific namespace for state storage:
- Handoff state:
authrim:handoff:state - Social login state:
authrim:direct:social:state - OAuth state:
authrim:oauth:state
This prevents namespace collisions and ensures each flow has isolated CSRF protection.
Storage Key Compatibility
Section titled “Storage Key Compatibility”The handoff implementation delegates access-token storage to SessionAuthImpl:
// Available for advanced diagnostics.const storageKey = auth.session.getStorageKey();This ensures handoff-authenticated sessions work seamlessly with auth.session.get(). In the default memory profile, tokens are not written to Web Storage.
PKCE Not Required
Section titled “PKCE Not Required”Unlike OAuth flows, Smart Handoff does not require PKCE:
- The Authorization Server handles the OAuth flow internally
- The handoff token exchange is a separate, secure channel
- State parameter provides CSRF protection
Complete Example
Section titled “Complete Example”<!DOCTYPE html><html><head> <title>My App - Callback</title></head><body> <div id="app"> <div id="loading">Processing authentication...</div> <div id="success" style="display:none">Success! Redirecting...</div> <div id="error" style="display:none"></div> </div>
<script src="https://unpkg.com/@authrim/web@latest/dist/authrim-web.umd.global.js"></script> <script> function showState(state) { document.getElementById('loading').style.display = state === 'loading' ? 'block' : 'none'; document.getElementById('success').style.display = state === 'success' ? 'block' : 'none'; document.getElementById('error').style.display = state === 'error' ? 'block' : 'none'; }
function showError(message) { document.getElementById('error').textContent = message; showState('error'); }
async function handleHandoffCallback() { const params = new URLSearchParams(window.location.search); const handoffToken = params.get('handoff_token'); const state = params.get('state');
if (!handoffToken || !state) { return false; }
console.log('[Authrim] Handoff callback detected'); history.replaceState(null, '', window.location.pathname);
try { const auth = await AuthrimWeb.createAuthrim({ issuer: 'https://auth.example.com', clientId: 'my-rp-client-id', });
// Verify and save in one call await auth.handoff.verifyAndSave(handoffToken, state);
console.log('[Authrim] Handoff login successful'); showState('success');
setTimeout(() => { window.location.href = '/'; }, 1000);
return true;
} catch (error) { console.error('[Authrim] Handoff error:', error);
if (error.code === 'state_mismatch') { showError('CSRF attack detected. Please try again.'); } else { showError('Authentication failed. Please try again.'); }
return true; } }
// Main initialization (async () => { const handled = await handleHandoffCallback();
if (!handled) { // Handle other callback types (social login, OAuth, etc.) const auth = await AuthrimWeb.createAuthrim({ issuer: 'https://auth.example.com', clientId: 'my-rp-client-id', });
// Check for social login callback if (auth.social.hasCallbackParams()) { const result = await auth.social.handleCallback(); if (result.error) { showError(result.error.message); } else { showState('success'); setTimeout(() => window.location.href = '/', 1000); } } } })(); </script></body></html>Diagnostic Logging
Section titled “Diagnostic Logging”Enable diagnostic logging to debug handoff flows:
const auth = await createAuthrim({ issuer: 'https://auth.example.com', clientId: 'my-rp-client-id', diagnosticLogging: { enabled: true, persistToStorage: true, },});
// Logs will include:// - "Handoff token verification started"// - "Handoff token verified successfully"// Diagnostic decisions include smart-handoff verification success/failure.Comparison: Smart Handoff vs Silent Login
Section titled “Comparison: Smart Handoff vs Silent Login”| Feature | Smart Handoff SSO | Silent Login SSO |
|---|---|---|
| Flow | AS → RP handoff token | RP → AS prompt=none |
| User interaction | None (seamless) | None (seamless) |
| Token lifetime | Short (60s) | Standard OAuth |
| PKCE required | No | Yes |
| Use case | AS-initiated SSO | RP-initiated SSO |
| Cookie dependency | None | Top-level navigation; no hidden third-party iframe |
| Browser support | All browsers | Limited (Safari ITP) |
Related
Section titled “Related”- Cross-Domain SSO — Silent Login SSO using prompt=none
- Session Management — Managing user sessions
- Error Handling — Unified error handling