--- title: "Magic Links" description: "Passwordless authentication with email magic links." publishedAt: 2024-04-11 order: 4 status: "published" --- > **Note:** This is mock/placeholder content for demonstration purposes. Magic links provide passwordless authentication by sending a one-time link to the user's email. ## How It Works 1. User enters their email address 2. System sends an email with a unique link 3. User clicks the link in their email 4. User is automatically signed in ## Benefits - **No password to remember** - Better UX - **More secure** - No password to steal - **Lower friction** - Faster sign-up process - **Email verification** - Confirms email ownership ## Implementation ### Magic Link Form ```tsx 'use client'; import { useForm } from 'react-hook-form'; import { sendMagicLinkAction } from '../_lib/actions'; export function MagicLinkForm() { const { register, handleSubmit, formState: { isSubmitting } } = useForm(); const [sent, setSent] = useState(false); const onSubmit = async (data) => { const result = await sendMagicLinkAction(data); if (result.success) { setSent(true); } }; if (sent) { return (
We've sent you a magic link to sign in.
Click the link below to sign in:
This link expires in {{ .TokenExpiryHours }} hours.
``` ## Callback Handler Handle the magic link callback: ```typescript // app/auth/callback/route.ts import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'; import { cookies } from 'next/headers'; import { NextResponse } from 'next/server'; export async function GET(request: Request) { const requestUrl = new URL(request.url); const token_hash = requestUrl.searchParams.get('token_hash'); const type = requestUrl.searchParams.get('type'); if (token_hash && type === 'magiclink') { const cookieStore = cookies(); const supabase = createRouteHandlerClient({ cookies: () => cookieStore }); const { error } = await supabase.auth.verifyOtp({ token_hash, type: 'magiclink', }); if (!error) { return NextResponse.redirect(new URL('/home', request.url)); } } // Return error if verification failed return NextResponse.redirect( new URL('/auth/sign-in?error=invalid_link', request.url) ); } ``` ## Advanced Features ### Custom Redirect Specify where users go after clicking the link: ```typescript await client.auth.signInWithOtp({ email: data.email, options: { emailRedirectTo: `${origin}/onboarding`, }, }); ``` ### Disable Auto Sign-Up Require users to sign up first: ```typescript await client.auth.signInWithOtp({ email: data.email, options: { shouldCreateUser: false, // Don't create new users }, }); ``` ### Token Expiry Configure link expiration (default: 1 hour): ```sql -- In Supabase SQL Editor ALTER TABLE auth.users SET default_token_lifetime = '15 minutes'; ``` ## Rate Limiting Prevent abuse by rate limiting magic link requests: ```typescript import { ratelimit } from '~/lib/rate-limit'; export const sendMagicLinkAction = enhanceAction( async (data, user, request) => { // Rate limit by IP const ip = request.headers.get('x-forwarded-for') || 'unknown'; const { success } = await ratelimit.limit(ip); if (!success) { throw new Error('Too many requests. Please try again later.'); } const client = getSupabaseServerClient(); await client.auth.signInWithOtp({ email: data.email, }); return { success: true }; }, { schema: EmailSchema } ); ``` ## Security Considerations ### Link Expiration Magic links should expire quickly: - Default: 1 hour - Recommended: 15-30 minutes for production - Shorter for sensitive actions ### One-Time Use Links should be invalidated after use: ```typescript // Supabase handles this automatically // Each link can only be used once ``` ### Email Verification Ensure emails are verified: ```typescript const { data: { user } } = await client.auth.getUser(); if (!user.email_confirmed_at) { redirect('/verify-email'); } ``` ## User Experience ### Loading State Show feedback while sending: ```tsx export function MagicLinkForm() { const [status, setStatus] = useState<'idle' | 'sending' | 'sent'>('idle'); const onSubmit = async (data) => { setStatus('sending'); await sendMagicLinkAction(data); setStatus('sent'); }; return ( <> {status === 'idle' &&