Enlaces mágicos
Autenticación sin contraseña con enlaces mágicos por correo electrónico.
Nota: Este es contenido de ejemplo/marcador de posición con fines de demostración.
Los enlaces mágicos ofrecen autenticación sin contraseña mediante el envío de un enlace de un solo uso al correo electrónico del usuario.
Cómo funciona
- El usuario introduce su dirección de correo electrónico
- El sistema envía un correo electrónico con un enlace único
- El usuario hace clic en el enlace de su correo electrónico
- El usuario inicia sesión automáticamente
Ventajas
- Sin contraseñas que recordar - Mejor experiencia de usuario
- Más seguro - No hay contraseña que robar
- Menos fricción - Proceso de registro más rápido
- Verificación del correo electrónico - Confirma la titularidad del correo electrónico
Implementación
Formulario de enlace mágico
'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(),
}),
}
);
Configuración
Activar en Supabase
- Ve a Authentication → Providers → Email
- Activa "Enable Email Provider"
- Activa "Enable Email Confirmations"
Configurar la plantilla de correo electrónico
Personaliza el correo electrónico del enlace mágico en el panel de Supabase:
- Ve a Authentication → Email Templates
- Selecciona "Magic Link"
- Personaliza la plantilla:
<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>
Controlador de callback
Gestiona el callback del enlace mágico:
// 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));
}
}
// Devuelve un error si la verificación ha fallado
return NextResponse.redirect(
new URL('/auth/sign-in?error=invalid_link', request.url)
);
}
Funcionalidades avanzadas
Redirección personalizada
Especifica adónde van los usuarios después de hacer clic en el enlace:
await client.auth.signInWithOtp({
email: data.email,
options: {
emailRedirectTo: `${origin}/onboarding`,
},
});
Desactivar el registro automático
Exige que los usuarios se registren primero:
await client.auth.signInWithOtp({
email: data.email,
options: {
shouldCreateUser: false, // No crear usuarios nuevos
},
});
Caducidad del token
Configura la caducidad del enlace (valor predeterminado: 1 hora):
-- En el SQL Editor de Supabase ALTER TABLE auth.users SET default_token_lifetime = '15 minutes';
Limitación de solicitudes
Evita los abusos limitando la frecuencia de las solicitudes de enlaces mágicos:
import { ratelimit } from '~/lib/rate-limit';
export const sendMagicLinkAction = enhanceAction(
async (data, user, request) => {
// Limitar por 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 }
);
Consideraciones de seguridad
Caducidad de los enlaces
Los enlaces mágicos deben caducar rápidamente:
- Valor predeterminado: 1 hora
- Recomendado: 15-30 minutos en producción
- Más corto para acciones sensibles
Un solo uso
Los enlaces deben invalidarse después de usarse:
// Supabase se encarga de esto automáticamente // Cada enlace solo se puede usar una vez
Verificación del correo electrónico
Asegúrate de que los correos electrónicos estén verificados:
const { data: { user } } = await client.auth.getUser();
if (!user.email_confirmed_at) {
redirect('/verify-email');
}
Experiencia de usuario
Estado de carga
Muestra información mientras se realiza el envío:
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 />}
</>
);
}
Reenviar el enlace
Permite que los usuarios soliciten un nuevo enlace:
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>
);
}
Entregabilidad del correo electrónico
SPF, DKIM, DMARC
Configura la autenticación del correo electrónico:
- Añade un registro SPF al DNS
- Activa la firma DKIM
- Configura una política DMARC
Dominio de correo electrónico personalizado
Usa tu propio dominio para mejorar la entregabilidad:
- Ve a Project Settings → Auth
- Configura un SMTP personalizado
- Verifica la titularidad del dominio
Supervisar los rebotes
Haz un seguimiento de los problemas de entrega de correo electrónico:
// Gestionar los rebotes de correo electrónico
export async function handleEmailBounce(email: string) {
await client.from('email_bounces').insert({
email,
bounced_at: new Date(),
});
// Notificar al usuario por otro canal
}
Pruebas
Desarrollo local
En desarrollo, los correos electrónicos se envían a InBucket:
http://localhost:54324
Consulta esta URL para ver los correos electrónicos con enlaces mágicos durante las pruebas.
Modo de prueba
Crea un enlace de prueba sin enviar ningún correo electrónico:
if (process.env.NODE_ENV === 'development') {
console.log('Magic link URL:', confirmationUrl);
}
Buenas prácticas
- Comunicación clara - Indica a los usuarios que revisen la carpeta de spam
- Caducidad corta - 15-30 minutos por seguridad
- Limitación de solicitudes - Evita los abusos
- Opción alternativa - Ofrece la autenticación con contraseña como respaldo
- Dominio personalizado - Mejor entregabilidad
- Supervisa la entrega - Haz un seguimiento de los rebotes y los fallos
- Opción de reenvío - Permite que los usuarios soliciten un nuevo enlace
- Compatible con dispositivos móviles - Asegúrate de que los enlaces funcionen en móviles