Smart Handoff SSO
このコンテンツはまだ日本語訳がありません。
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
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
Setup
1. Install Package
npm install @authrim/sveltekit2. Configure Hooks
In src/hooks.server.ts:
import { sequence } from '@sveltejs/kit/hooks';import { createAuthHandle, createHandoffHandler} from '@authrim/sveltekit/server';
export const handle = sequence( // First: Initialize auth context createAuthHandle({ issuer: import.meta.env.VITE_AUTHRIM_ISSUER, clientId: import.meta.env.VITE_AUTHRIM_CLIENT_ID, callbackPaths: ['/callback'], }),
// Second: Handle handoff callbacks createHandoffHandler({ issuer: import.meta.env.VITE_AUTHRIM_ISSUER, clientId: import.meta.env.VITE_AUTHRIM_CLIENT_ID, errorRedirect: '/login?error=handoff_failed', }));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
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
interface HandoffVerifyOptions { /** * Authrim IdP URL (required) */ issuer: string;
/** * OAuth client ID (required) */ 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 };}Advanced Usage
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 type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => { const result = await verifyHandoffToken(event, { issuer: import.meta.env.VITE_AUTHRIM_ISSUER, clientId: import.meta.env.VITE_AUTHRIM_CLIENT_ID, });
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
createHandoffHandler({ issuer: env.AUTHRIM_ISSUER, clientId: env.AUTHRIM_CLIENT_ID, errorRedirect: (error) => { // Custom error handling if (error === 'token_expired') { return '/login?error=expired'; } return '/login?error=handoff_failed'; },})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, },})Client-Side Usage
After successful handoff, access session data in your components:
<script lang="ts"> import { getAuthContext } from '@authrim/sveltekit';
const auth = getAuthContext();
// Note: Handoff is handled automatically by createHandoffHandler() // in hooks.server.ts. This component only accesses the session.</script>
{#if $auth.stores.isAuthenticated} <div> <p>Welcome, {$auth.stores.user?.name}!</p> <button on:click={() => auth.signOut()}>Sign Out</button> </div>{:else} <a href="/login">Sign In</a>{/if}Security Features
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
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
State validation happens server-side:
- State is sent by Authorization Server
- Verified during handoff token exchange
- No client-side state management needed
Complete Example
1. Hooks Configuration
import { sequence } from '@sveltejs/kit/hooks';import { createAuthHandle, createHandoffHandler } from '@authrim/sveltekit/server';import { env } from '$env/dynamic/public';
const issuer = env.PUBLIC_AUTHRIM_ISSUER;const clientId = env.PUBLIC_AUTHRIM_CLIENT_ID;
if (!issuer || !clientId) { console.warn('[Authrim] Missing environment variables');}
export const handle = sequence( createAuthHandle({ issuer: issuer || '', clientId: clientId || '', callbackPaths: ['/callback'], }), createHandoffHandler({ issuer: issuer || '', clientId: clientId || '', errorRedirect: '/login?error=handoff_failed', }));2. App Type Definitions
import type { ServerAuthContext } from '@authrim/sveltekit/server';
declare global { namespace App { interface Locals { auth?: ServerAuthContext; } }}
export {};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
<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 on:click={handleSignOut}>Sign Out</button></div>Error Handling
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
<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
Local Development
# Set environment variablesexport VITE_AUTHRIM_ISSUER="https://auth.example.com"export VITE_AUTHRIM_CLIENT_ID="dev-client-id"
# 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
# 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
| Feature | @authrim/sveltekit | @authrim/web |
|---|---|---|
| Processing | Server-side | Client-side |
| Session storage | Cookie | localStorage |
| 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
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
Problem: Session cookie not saved
Solution: Check cookie configuration
createHandoffHandler({ // ... sessionOptions: { secure: false, // Set to false for local development (http) sameSite: 'lax', },})Infinite Redirects
Problem: Keeps redirecting
Solution: Check createAuthHandle callback paths
createAuthHandle({ // ... callbackPaths: ['/callback'], // Should match your callback route})Related
- Server-Side Authentication — Server-side auth patterns
- Authentication — Client-side auth flows
- Configuration — SDK configuration options