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.
In SvelteKit, handoff flows are handled entirely server-side using hooks, providing:
- Zero client-side JavaScript for authentication
- Automatic session management via cookies
- 303 See Other redirects (POST → GET)
- Type-safe server-side APIs
How It Works
Section titled “How It Works”sequenceDiagram
participant User
participant AS as Authorization Server
participant Hook as SvelteKit Hook
participant App as SvelteKit App
User->>AS: Already authenticated
User->>AS: Click "Open App" button
AS->>AS: Generate handoff_token
AS->>Hook: Redirect with handoff_token + state
Hook->>AS: POST /auth/external/handoff/verify
AS->>Hook: Return access_token + session + user
Hook->>Hook: Set session cookie
Hook->>Hook: Clean URL (remove token)
Hook->>App: Redirect (303 See Other)
App->>User: Show authenticated app
Key Features:
- Server-side only: No token exposure to client
- Cookie-based sessions: HttpOnly, Secure, SameSite
- Automatic cleanup: Token removed from URL
- 303 redirects: Proper POST → GET redirect handling
1. Install Package
Section titled “1. Install Package”npm install @authrim/sveltekit2. Configure Hooks
Section titled “2. Configure Hooks”In src/hooks.server.ts:
import { sequence } from '@sveltejs/kit/hooks';import { createAuthHandle, createHandoffHandler} from '@authrim/sveltekit/server';import { env } from '$env/dynamic/private';
export const handle = sequence( // First: Initialize auth context createAuthHandle({ sessionSecret: env.AUTHRIM_SESSION_SECRET, callbackPaths: ['/callback'], }),
// Second: Handle handoff callbacks createHandoffHandler({ issuer: env.AUTHRIM_ISSUER, clientId: env.AUTHRIM_CLIENT_ID, errorRedirect: '/login?error=handoff_failed', sessionOptions: { sessionSecret: env.AUTHRIM_SESSION_SECRET, }, }));3. That’s It!
Section titled “3. That’s It!”The handoff handler automatically:
- Detects handoff_token in URL parameters
- Verifies token with Authorization Server
- Saves session to cookie
- Removes token from URL
- Redirects to clean URL with 303 See Other
What createHandoffHandler() Does
Section titled “What createHandoffHandler() Does”The handler provides automatic processing:
// Pseudo-code of what happens internallyexport function createHandoffHandler(options) { return async ({ event, resolve }) => { // Check for handoff token if (event.url.searchParams.has('handoff_token')) { const handoffToken = event.url.searchParams.get('handoff_token'); const state = event.url.searchParams.get('state');
// Verify with Authorization Server const result = await verifyHandoffToken(event, options);
if (!result.success) { // Error: redirect to login throw redirect(303, options.errorRedirect || '/login'); }
// Success: remove token from URL and redirect const cleanUrl = new URL(event.url); cleanUrl.searchParams.delete('handoff_token'); cleanUrl.searchParams.delete('state'); throw redirect(303, cleanUrl.toString()); }
return resolve(event); };}Configuration Options
Section titled “Configuration Options”interface HandoffVerifyOptions { /** * Authrim IdP URL. */ issuer: string;
/** * OAuth client ID. */ clientId: string;
/** * Handoff verify endpoint * @default '/auth/external/handoff/verify' */ verifyEndpoint?: string;
/** * Error redirect path * @default '/login' */ errorRedirect?: string;
/** * Session manager options */ sessionOptions?: { cookieName?: string; // default: 'authrim_session' sameSite?: 'strict' | 'lax' | 'none'; // default: 'lax' secure?: boolean; // default: true in production path?: string; // default: '/' maxAge?: number; // default: 7 days httpOnly?: boolean; // default: true sessionSecret?: string | Uint8Array | CryptoKey; // required };}Advanced Usage
Section titled “Advanced Usage”Manual Token Verification
Section titled “Manual Token Verification”If you need more control, use verifyHandoffToken() in a load function:
import { verifyHandoffToken } from '@authrim/sveltekit/server';import { redirect } from '@sveltejs/kit';import { env } from '$env/dynamic/private';import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => { const result = await verifyHandoffToken(event, { issuer: env.AUTHRIM_ISSUER, clientId: env.AUTHRIM_CLIENT_ID, sessionOptions: { sessionSecret: env.AUTHRIM_SESSION_SECRET, }, });
if (!result.success) { throw redirect(302, '/login?error=' + result.error); }
// Session is now saved in cookie // Redirect to app throw redirect(302, '/dashboard');};Custom Error Handling
Section titled “Custom Error Handling”createHandoffHandler({ issuer: env.AUTHRIM_ISSUER, clientId: env.AUTHRIM_CLIENT_ID, errorRedirect: '/login?error=handoff_failed', sessionOptions: { sessionSecret: env.AUTHRIM_SESSION_SECRET, },})Custom Session Storage
Section titled “Custom Session Storage”createHandoffHandler({ issuer: env.AUTHRIM_ISSUER, clientId: env.AUTHRIM_CLIENT_ID, sessionOptions: { cookieName: 'my_app_session', sameSite: 'strict', secure: true, path: '/', maxAge: 14 * 24 * 60 * 60, // 14 days httpOnly: true, sessionSecret: env.AUTHRIM_SESSION_SECRET, },})Client-Side Usage
Section titled “Client-Side Usage”After successful handoff, access session data in your components:
<script lang="ts"> import { getAuthContext } from '@authrim/sveltekit';
const auth = getAuthContext(); const { isAuthenticated, user } = auth.stores;
// Note: Handoff is handled automatically by createHandoffHandler() // in hooks.server.ts. This component only accesses the session.</script>
{#if $isAuthenticated} <div> <p>Welcome, {$user?.name ?? $user?.email}!</p> <button onclick={() => auth.signOut()}>Sign Out</button> </div>{:else} <a href="/login">Sign In</a>{/if}Security Features
Section titled “Security Features”303 See Other Redirect
Section titled “303 See Other Redirect”The handler uses 303 See Other instead of 302 Found:
// After verification, redirect with 303throw redirect(303, cleanUrl.toString());Why 303?
- Converts POST to GET (proper semantic)
- Prevents form resubmission on refresh
- Recommended by HTTP spec for post-authentication redirects
Cookie Security
Section titled “Cookie Security”Session cookies are configured with security best practices:
{ httpOnly: true, // Prevents XSS secure: true, // HTTPS only (in production) sameSite: 'lax', // CSRF protection path: '/', // Application-wide maxAge: 604800, // 7 days}State CSRF Protection
Section titled “State CSRF Protection”State validation happens server-side:
- State is sent by Authorization Server
- Verified during handoff token exchange
- No client-side state management needed
Complete Example
Section titled “Complete Example”1. Hooks Configuration
Section titled “1. Hooks Configuration”import { sequence } from '@sveltejs/kit/hooks';import { createAuthHandle, createHandoffHandler } from '@authrim/sveltekit/server';import { env } from '$env/dynamic/private';
const issuer = env.AUTHRIM_ISSUER;const clientId = env.AUTHRIM_CLIENT_ID;const sessionSecret = env.AUTHRIM_SESSION_SECRET;
if (!issuer || !clientId || !sessionSecret) { console.warn('[Authrim] Missing environment variables');}
export const handle = sequence( createAuthHandle({ sessionSecret: sessionSecret || '', callbackPaths: ['/callback'], }), createHandoffHandler({ issuer: issuer || '', clientId: clientId || '', errorRedirect: '/login?error=handoff_failed', sessionOptions: { sessionSecret: sessionSecret || '', }, }));2. App Type Definitions
Section titled “2. App Type Definitions”import type { ServerAuthContext } from '@authrim/sveltekit/server';
declare global { namespace App { interface Locals { auth?: ServerAuthContext; } }}
export {};3. Protected Route
Section titled “3. Protected Route”import { requireAuth } from '@authrim/sveltekit/server';import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => { // Automatically redirects to /login if not authenticated const { auth } = await requireAuth()(event);
return { user: auth.user, session: auth.session, };};4. Dashboard Component
Section titled “4. Dashboard Component”<script lang="ts"> import { getAuthContext } from '@authrim/sveltekit'; import type { PageData } from './$types';
export let data: PageData;
const auth = getAuthContext();
async function handleSignOut() { await auth.signOut(); // Automatically redirects to login }</script>
<div class="dashboard"> <h1>Dashboard</h1> <p>Welcome, {data.user.name}!</p> <p>Email: {data.user.email}</p> <button onclick={handleSignOut}>Sign Out</button></div>Error Handling
Section titled “Error Handling”Server-Side Errors
Section titled “Server-Side Errors”// In +page.server.tsexport const load: PageServerLoad = async (event) => { const result = await verifyHandoffToken(event);
if (!result.success) { // Log error for monitoring console.error('[Handoff] Verification failed:', result.error);
// Redirect to login with error message throw redirect(303, `/login?error=${encodeURIComponent(result.error)}`); }
return { session: result.session, user: result.user };};Client-Side Error Display
Section titled “Client-Side Error Display”<script lang="ts"> import { page } from '$app/stores';
$: error = $page.url.searchParams.get('error');
const errorMessages: Record<string, string> = { 'handoff_failed': 'Authentication failed. Please try again.', 'token_expired': 'Session expired. Please sign in again.', 'invalid_state': 'Security check failed. Please try again.', };</script>
{#if error} <div class="error"> {errorMessages[error] || 'An error occurred during authentication.'} </div>{/if}
<a href="/auth/login">Sign In</a>Testing
Section titled “Testing”Local Development
Section titled “Local Development”# Set environment variablesexport AUTHRIM_ISSUER="https://auth.example.com"export AUTHRIM_CLIENT_ID="dev-client-id"export AUTHRIM_SESSION_SECRET="replace-with-at-least-32-bytes"
# Start dev servernpm run dev
# Test handoff flow# 1. Get handoff token from Authorization Server# 2. Visit: http://localhost:5173/callback?handoff_token=xxx&state=yyy# 3. Should automatically redirect to / with sessionTesting with cURL
Section titled “Testing with cURL”# Verify handoff token manuallycurl -X POST https://auth.example.com/auth/external/handoff/verify \ -H "Content-Type: application/json" \ -d '{ "handoff_token": "your-handoff-token", "state": "your-state", "client_id": "your-client-id" }'Comparison: SvelteKit vs Vanilla JS
Section titled “Comparison: SvelteKit vs Vanilla JS”| Feature | @authrim/sveltekit | @authrim/web |
|---|---|---|
| Processing | Server-side | Client-side |
| Session storage | Encrypted HttpOnly cookie | Browser storage/token profile |
| Type safety | Full TypeScript | Full TypeScript |
| SSR support | ✅ Yes | ❌ No |
| Setup complexity | Low (hooks only) | Medium (callback page) |
| Security | Higher (HttpOnly cookies) | Good (localStorage) |
| Browser compatibility | All browsers | All browsers |
Troubleshooting
Section titled “Troubleshooting”Handoff Not Detected
Section titled “Handoff Not Detected”Problem: Handler doesn’t trigger
Solution: Check URL parameters
// Add debug logging in hooks.server.tsexport const handle = sequence( createAuthHandle({ ... }), createHandoffHandler({ ... }), async ({ event, resolve }) => { console.log('URL params:', Object.fromEntries(event.url.searchParams)); return resolve(event); });Cookie Not Set
Section titled “Cookie Not Set”Problem: Session cookie not saved
Solution: Check cookie configuration
createHandoffHandler({ // ... sessionOptions: { secure: false, // Set to false for local development (http) sameSite: 'lax', sessionSecret: env.AUTHRIM_SESSION_SECRET, },})Infinite Redirects
Section titled “Infinite Redirects”Problem: Keeps redirecting
Solution: Check createAuthHandle callback paths
createAuthHandle({ // ... callbackPaths: ['/callback'], // Should match your callback route})Related
Section titled “Related”- Server-Side Authentication — Server-side auth patterns
- Authentication — Client-side auth flows
- Configuration — SDK configuration options