Magic Links

Passwortlose Authentifizierung mit Magic Links per E-Mail.

Hinweis: Dies sind Beispiel- bzw. Platzhalterinhalte zu Demonstrationszwecken.

Magic Links ermöglichen eine passwortlose Authentifizierung, indem sie einen Einmal-Link an die E-Mail-Adresse des Benutzers senden.

So funktioniert es

  1. Der Benutzer gibt seine E-Mail-Adresse ein
  2. Das System sendet eine E-Mail mit einem eindeutigen Link
  3. Der Benutzer klickt auf den Link in seiner E-Mail
  4. Der Benutzer wird automatisch angemeldet

Vorteile

  • Kein Passwort, das man sich merken muss – Bessere UX
  • Sicherer – Es gibt kein Passwort, das gestohlen werden kann
  • Weniger Hürden – Schnellerer Registrierungsprozess
  • E-Mail-Verifizierung – Bestätigt die Inhaberschaft der E-Mail-Adresse

Implementierung

'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 (
      <div className="text-center">
        <h2>Check your email</h2>
        <p>We've sent you a magic link to sign in.</p>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>Email address</label>
        <input
          type="email"
          {...register('email', { required: true })}
          placeholder="you@example.com"
        />
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Sending...' : 'Send magic link'}
      </button>
    </form>
  );
}

Server Action

'use server';

import { enhanceAction } from '@kit/next/actions';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { z } from 'zod';

export const sendMagicLinkAction = enhanceAction(
  async (data) => {
    const client = getSupabaseServerClient();
    const origin = process.env.NEXT_PUBLIC_SITE_URL!;

    const { error } = await client.auth.signInWithOtp({
      email: data.email,
      options: {
        emailRedirectTo: `${origin}/auth/callback`,
        shouldCreateUser: true,
      },
    });

    if (error) throw error;

    return {
      success: true,
      message: 'Check your email for the magic link',
    };
  },
  {
    schema: z.object({
      email: z.string().email(),
    }),
  }
);

Konfiguration

In Supabase aktivieren

  1. Gehen Sie zu Authentication → Providers → Email
  2. Aktivieren Sie „Enable Email Provider“
  3. Aktivieren Sie „Enable Email Confirmations“

E-Mail-Vorlage konfigurieren

Passen Sie die Magic-Link-E-Mail im Supabase-Dashboard an:

  1. Gehen Sie zu Authentication → Email Templates
  2. Wählen Sie „Magic Link“ aus
  3. Passen Sie die Vorlage an:
<h2>Sign in to {{ .SiteURL }}</h2>
<p>Click the link below to sign in:</p>
<p><a href="{{ .ConfirmationURL }}">Sign in</a></p>
<p>This link expires in {{ .TokenExpiryHours }} hours.</p>

Callback-Handler

Verarbeiten Sie den Magic-Link-Callback:

// 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));
    }
  }

  // Fehler zurückgeben, wenn die Verifizierung fehlgeschlagen ist
  return NextResponse.redirect(
    new URL('/auth/sign-in?error=invalid_link', request.url)
  );
}

Erweiterte Funktionen

Benutzerdefinierte Weiterleitung

Legen Sie fest, wohin Benutzer nach dem Klick auf den Link gelangen:

await client.auth.signInWithOtp({
  email: data.email,
  options: {
    emailRedirectTo: `${origin}/onboarding`,
  },
});

Automatische Registrierung deaktivieren

Verlangen Sie, dass sich Benutzer zuerst registrieren:

await client.auth.signInWithOtp({
  email: data.email,
  options: {
    shouldCreateUser: false, // Keine neuen Benutzer anlegen
  },
});

Ablauf des Tokens

Konfigurieren Sie die Gültigkeitsdauer des Links (Standard: 1 Stunde):

-- Im Supabase SQL Editor
ALTER TABLE auth.users
SET default_token_lifetime = '15 minutes';

Ratenbegrenzung

Verhindern Sie Missbrauch, indem Sie Magic-Link-Anfragen begrenzen:

import { ratelimit } from '~/lib/rate-limit';

export const sendMagicLinkAction = enhanceAction(
  async (data, user, request) => {
    // Ratenbegrenzung nach IP-Adresse
    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 }
);

Sicherheitsaspekte

Magic Links sollten schnell ablaufen:

  • Standard: 1 Stunde
  • Empfohlen: 15–30 Minuten für die Produktion
  • Kürzer bei sensiblen Aktionen

Einmalige Verwendung

Links sollten nach der Verwendung ungültig werden:

// Supabase übernimmt dies automatisch
// Jeder Link kann nur einmal verwendet werden

E-Mail-Verifizierung

Stellen Sie sicher, dass E-Mail-Adressen verifiziert sind:

const { data: { user } } = await client.auth.getUser();

if (!user.email_confirmed_at) {
  redirect('/verify-email');
}

Benutzererfahrung

Ladezustand

Zeigen Sie während des Sendens eine Rückmeldung an:

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' && <EmailForm onSubmit={onSubmit} />}
      {status === 'sending' && <SendingMessage />}
      {status === 'sent' && <CheckEmailMessage />}
    </>
  );
}

Ermöglichen Sie Benutzern, einen neuen Link anzufordern:

export function ResendMagicLink({ email }: { email: string }) {
  const [canResend, setCanResend] = useState(false);
  const [countdown, setCountdown] = useState(60);

  useEffect(() => {
    if (countdown > 0) {
      const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
      return () => clearTimeout(timer);
    } else {
      setCanResend(true);
    }
  }, [countdown]);

  const handleResend = async () => {
    await sendMagicLinkAction({ email });
    setCountdown(60);
    setCanResend(false);
  };

  return (
    <button onClick={handleResend} disabled={!canResend}>
      {canResend ? 'Resend link' : `Resend in ${countdown}s`}
    </button>
  );
}

E-Mail-Zustellbarkeit

SPF, DKIM, DMARC

Konfigurieren Sie die E-Mail-Authentifizierung:

  1. Fügen Sie einen SPF-Eintrag zum DNS hinzu
  2. Aktivieren Sie die DKIM-Signierung
  3. Richten Sie eine DMARC-Richtlinie ein

Eigene E-Mail-Domain

Verwenden Sie Ihre eigene Domain für eine bessere Zustellbarkeit:

  1. Gehen Sie zu Project Settings → Auth
  2. Konfigurieren Sie einen eigenen SMTP-Server
  3. Bestätigen Sie die Inhaberschaft der Domain

Bounces überwachen

Verfolgen Sie Probleme bei der E-Mail-Zustellung:

// E-Mail-Bounces verarbeiten
export async function handleEmailBounce(email: string) {
  await client.from('email_bounces').insert({
    email,
    bounced_at: new Date(),
  });

  // Benutzer über einen anderen Kanal benachrichtigen
}

Testen

Lokale Entwicklung

In der Entwicklung werden E-Mails an InBucket gesendet:

http://localhost:54324

Rufen Sie diese URL auf, um sich während der Tests die Magic-Link-E-Mails anzusehen.

Testmodus

Erstellen Sie einen Testlink, ohne eine E-Mail zu senden:

if (process.env.NODE_ENV === 'development') {
  console.log('Magic link URL:', confirmationUrl);
}

Best Practices

  1. Klare Kommunikation – Weisen Sie Benutzer darauf hin, ihren Spam-Ordner zu prüfen
  2. Kurze Gültigkeitsdauer – 15–30 Minuten aus Sicherheitsgründen
  3. Ratenbegrenzung – Missbrauch verhindern
  4. Alternative Option – Passwort-Authentifizierung als Ausweichlösung anbieten
  5. Eigene Domain – Bessere Zustellbarkeit
  6. Zustellung überwachen – Bounces und Fehlschläge nachverfolgen
  7. Option zum erneuten Senden – Benutzern ermöglichen, einen neuen Link anzufordern
  8. Mobilfreundlich – Sicherstellen, dass Links auf Mobilgeräten funktionieren