Registro e Inicio de Sesión con Email en Supabase
El método más común de autenticación es email y contraseña. Supabase lo maneja con dos métodos: signUp para registro y signInWithPassword para login.
Registro con signUp
signUp crea un nuevo usuario en tu proyecto de Supabase:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
const { data, error } = await supabase.auth.signUp({
email: 'usuario@ejemplo.com',
password: 'min-6-caracteres'
})
if (error) {
console.error('Error en registro:', error.message)
} else {
console.log('Usuario creado:', data.user)
console.log('Sesión:', data.session)
}Qué devuelve signUp
El objeto data tiene dos propiedades:
data.user-- El objeto del usuario creado, con suid,email,created_at, etc.data.session-- La sesión (access_token + refresh_token). Si la confirmación de email está habilitada,sessionseránullhasta que el usuario confirme su email.
Confirmacion de email
Por defecto, Supabase requiere que el usuario confirme su email antes de poder iniciar sesión. Puedes desactivar esto en el Dashboard: Authentication > Providers > Email > Confirm email. Para producción, dejalo activado.
Agregar metadata al registro
Puedes enviar datos adicionales al momento del registro usando la propiedad options.data:
const { data, error } = await supabase.auth.signUp({
email: 'usuario@ejemplo.com',
password: 'contrasena-segura',
options: {
data: {
nombre: 'Juan Perez',
avatar_url: 'https://ejemplo.com/avatar.jpg',
rol_app: 'editor'
}
}
})
// Despues puedes acceder a estos datos en:
// data.user.user_metadata.nombre
// data.user.user_metadata.avatar_urluser_metadata es pública
La metadata que envias en options.data se guarda en user_metadata y el usuario puede modificarla desde el cliente. No la uses para permisos criticos. Para roles de acceso, usa una tabla separada con RLS.
Flujo de confirmación de email
Cuando la confirmación de email está habilitada, este es el flujo completo:
1. Usuario llama a signUp con email y password
|
v
2. Supabase crea el usuario con email_confirmed_at = null
|
v
3. Supabase envia un email con un link de confirmación
|
v
4. El usuario hace clic en el link
|
v
5. Supabase redirige a tu Redirect URL con tokens en la URL
|
v
6. Tu app procesa los tokens y el usuario queda autenticadoConfigura la URL de redirección en el Dashboard de Supabase: Authentication > URL Configuration > Site URL.
Personalizar el email de confirmación
En el Dashboard: Authentication > Email Templates. Puedes editar el HTML del email de confirmación. La variable {{ .ConfirmationURL }} contiene el link que el usuario debe hacer clic.
Login con signInWithPassword
Una vez que el usuario tiene cuenta (y confirmo su email si es necesario), puede iniciar sesión:
const { data, error } = await supabase.auth.signInWithPassword({
email: 'usuario@ejemplo.com',
password: 'su-contrasena'
})
if (error) {
console.error('Error en login:', error.message)
} else {
console.log('Login exitoso')
console.log('Usuario:', data.user)
console.log('Sesión:', data.session)
}A diferencia de signUp, signInWithPassword siempre devuelve una sesión completa si las credenciales son correctas.
Cerrar sesión con signOut
const { error } = await supabase.auth.signOut()
if (error) {
console.error('Error al cerrar sesión:', error.message)
} else {
console.log('Sesión cerrada')
}signOut elimina la sesión del almacenamiento local y revoca el refresh_token en el servidor.
Obtener el usuario actual
Hay dos formas de obtener el usuario actual, y es importante saber cuándo usar cada una:
getSession (lectura local)
const { data: { session } } = await supabase.auth.getSession()
if (session) {
console.log('Usuario:', session.user.email)
console.log('Token expira en:', new Date(session.expires_at! * 1000))
} else {
console.log('No hay sesión activa')
}getSession lee la sesión almacenada localmente. Es rápido pero no valida el token contra el servidor.
getUser (validación en servidor)
const { data: { user }, error } = await supabase.auth.getUser()
if (user) {
console.log('Usuario verificado:', user.email)
console.log('ID:', user.id)
console.log('Metadata:', user.user_metadata)
} else {
console.log('No autenticado o token invalido')
}Cuando usar cada uno
Usa getSession para mostrar la UI rápidamente (nombre del usuario, avatar, etc.). Usa getUser antes de operaciones importantes donde necesites certeza de que la sesión es válida (cambiar contraseña, eliminar cuenta, etc.).
Manejo de errores
Los errores de auth tienen una estructura consistente. Estos son los más comunes:
const { data, error } = await supabase.auth.signInWithPassword({
email: 'usuario@ejemplo.com',
password: 'contrasena-incorrecta'
})
if (error) {
switch (error.message) {
case 'Invalid login credentials':
// Email o contraseña incorrectos
console.log('Credenciales inválidas')
break
case 'Email not confirmed':
// El usuario no ha confirmado su email
console.log('Confirma tu email antes de iniciar sesión')
break
case 'User already registered':
// El email ya esta registrado (en signUp)
console.log('Este email ya tiene una cuenta')
break
default:
console.log('Error:', error.message)
}
}No expongas detalles del error
En producción, no muestres el mensaje de error de Supabase directamente al usuario. Un atacante podría usar mensajes como "User already registered" para enumerar cuentas. Usa mensajes genéricos como "Credenciales inválidas" o "No se pudo completar el registro".
Formulario completo de registro y login
Este es un componente funcional que puedes usar como base en tu proyecto con NextJS:
'use client'
import { useState } from 'react'
import { createClient } from '@/lib/supabase/client'
type AuthMode = 'login' | 'register'
export function AuthForm() {
const [mode, setMode] = useState<AuthMode>('login')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState('')
const supabase = createClient()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setMessage('')
if (mode === 'register') {
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`
}
})
if (error) {
setMessage(error.message)
} else {
setMessage('Revisa tu email para confirmar tu cuenta.')
}
} else {
const { error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) {
setMessage('Email o contraseña incorrectos.')
} else {
// Redirigir al dashboard o página principal
window.location.href = '/dashboard'
}
}
setLoading(false)
}
return (
<div className="max-w-md mx-auto">
<h1 className="text-2xl font-bold mb-6">
{mode === 'login' ? 'Iniciar sesión' : 'Crear cuenta'}
</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium mb-1">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-3 py-2 border rounded-md"
placeholder="tu@email.com"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium mb-1">
Contrasena
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={6}
className="w-full px-3 py-2 border rounded-md"
placeholder="Minimo 6 caracteres"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-2 px-4 bg-green-600 text-white rounded-md
hover:bg-green-700 disabled:opacity-50"
>
{loading
? 'Procesando...'
: mode === 'login'
? 'Iniciar sesión'
: 'Crear cuenta'}
</button>
</form>
{message && (
<p className="mt-4 text-sm text-center">{message}</p>
)}
<button
onClick={() => setMode(mode === 'login' ? 'register' : 'login')}
className="mt-4 text-sm text-gray-400 hover:text-white w-full text-center"
>
{mode === 'login'
? '¿No tienes cuenta? Regístrate'
: '¿Ya tienes cuenta? Inicia sesión'}
</button>
</div>
)
}Ruta de callback para confirmación de email
Cuando el usuario hace clic en el link de confirmación, Supabase redirige a tu app con un code en la URL. Necesitas una ruta que procese ese código:
// app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
const next = searchParams.get('next') ?? '/dashboard'
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
return NextResponse.redirect(`${origin}${next}`)
}
}
// Si algo fallo, redirigir al login con error
return NextResponse.redirect(`${origin}/login?error=auth`)
}exchangeCodeForSession
exchangeCodeForSession toma el código temporal de la URL y lo intercambia por una sesión completa (access_token + refresh_token). Este es el paso final del flujo PKCE (Proof Key for Code Exchange -- un protocolo de seguridad que evita que alguien intercepte el código de autenticación).
Resetear contraseña
Para implementar el flujo de "olvide mi contraseña":
// Paso 1: Enviar email de recuperacion
const { error } = await supabase.auth.resetPasswordForEmail(
'usuario@ejemplo.com',
{
redirectTo: `${window.location.origin}/auth/update-password`
}
)
// Paso 2: En la página /auth/update-password, el usuario ingresa su nueva contraseña
const { error } = await supabase.auth.updateUser({
password: 'nueva-contrasena-segura'
})El email de recuperacion incluye un link que redirige al usuario a la URL que especificaste en redirectTo, con los tokens necesarios en la URL.
Configuración de seguridad recomendada
En el Dashboard de Supabase (Authentication > Providers > Email):
- Confirm email: Activado en producción
- Secure email change: Activado (requiere confirmación del email anterior y el nuevo)
- Minimum password length: 8 caracteres mínimo (6 es el default, pero 8 es más seguro)
Rate limiting
Supabase aplica rate limiting (límite de peticiones por tiempo) automáticamente en los endpoints de auth. Por defecto, un email no puede intentar login más de 30 veces por hora. Puedes ajustar estos límites en el Dashboard.