Next.js
Authentication en Next.js
Explica cómo transformar credenciales en sesiones verificables con cookies seguras, passwords, OAuth, rotación, revocación, MFA y recuperación de cuenta.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Next.js
Explica cómo transformar credenciales en sesiones verificables con cookies seguras, passwords, OAuth, rotación, revocación, MFA y recuperación de cuenta.
Authentication demuestra quién controla una identidad en este momento. En Next.js suele terminar en una sesión almacenada o referenciada por una cookie segura. El framework aporta Server Components, Actions, Route Handlers, cookies y Proxy, pero no diseña por sí solo credenciales, hashing, OAuth, revocación ni recuperación de cuenta.
credential proof
→ verify identity
→ establish session
→ browser stores session reference
→ each request verifies session
→ current user becomes principalAuthentication responde quién eres. Authorization responde qué puedes hacer. Un usuario autenticado todavía puede no tener acceso a un recurso.
La documentación oficial recomienda usar una librería mantenida cuando sea posible. Un proveedor o biblioteca resuelve detalles como:
Construir desde cero es razonable solo con requisitos claros y experiencia de seguridad.
El servidor compara una contraseña con un hash especializado. No almacena texto plano ni cifra contraseñas para poder recuperarlas.
Usa algoritmos diseñados para passwords, como Argon2id o bcrypt con parámetros actuales. El coste debe hacer lento el ataque offline sin volver inviable el login legítimo.
El usuario prueba identidad mediante un proveedor. El flujo necesita:
state contra CSRF.No confíes únicamente en el email devuelto si el proveedor no lo marca verificado o si permite cambios ambiguos.
Un token de un solo uso llega por correo. Debe:
Reducen phishing y reutilización de passwords, pero requieren soporte de plataforma, recovery y UX específica.
form
→ normalize email
→ validate password policy
→ check/create under unique constraint
→ hash password
→ create verification challenge
→ send email asynchronouslyNo confirmes “ese correo ya existe” si la enumeración de cuentas es un riesgo. Puedes mostrar un mensaje neutral y mantener el mismo patrón de tiempo razonable.
La base de datos necesita unique constraint sobre email normalizado; una consulta previa sola no evita carreras.
Action conceptual:
"use server";
export async function loginAction(
_previous: LoginState,
formData: FormData,
): Promise<LoginState> {
const parsed = loginSchema.safeParse({
email: formData.get("email"),
password: formData.get("password"),
});
if (!parsed.success) {
return { status: "invalid" };
}
const result = await authenticateCredentials(parsed.data);
if (!result.ok) {
await recordFailedAttempt(parsed.data.email);
return { status: "invalidCredentials" };
}
await createSession(result.user.id);
redirect(safeReturnPath(formData.get("next")));
}Prioriza:
No truncar silenciosamente. No registrar passwords. No enviarlas a analytics.
Tabla conceptual:
Session
- id_hash
- user_id
- created_at
- expires_at
- last_seen_at
- revoked_at
- device metadata limitadaCookie contiene un token aleatorio; la DB conserva hash del token para que una filtración de la tabla no entregue sesiones directamente utilizables.
import "server-only";
import { cache } from "react";
export const verifySession = cache(async () => {
const store = await cookies();
const token = store.get("session")?.value;
if (!token) return null;
const session = await sessionRepository.findValidByToken(token);
if (!session) return null;
return {
userId: session.userId,
sessionId: session.id,
};
});React.cache evita repetir la lectura dentro de un request. No mantiene sesiones entre requests.
Un token firmado puede incluir user ID y expiración.
Para permisos sensibles, consulta estado actual aunque el token incluya claims.
const store = await cookies();
store.set("session", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
expires: expiresAt,
});En producción, no derives seguridad solo de NODE_ENV; configura explícitamente los entornos y usa HTTPS.
export function proxy(request: NextRequest) {
if (!request.cookies.has("session")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}Esto evita trabajo y mejora navegación. No verifica que la cookie sea válida, no revocada ni que el usuario tenga permiso.
La mayoría de comprobaciones deben ocurrir cerca de los datos.
export default async function DashboardPage() {
const session = await verifySession();
if (!session) redirect("/login");
const dashboard = await getDashboardForUser(session.userId);
return <Dashboard data={dashboard} />;
}Sirve para seleccionar UI y cargar datos autorizados. No pases la sesión completa a cliente.
Cada entrada vuelve a verificar:
"use server";
export async function deleteAccount() {
const session = await requireSession();
await requireRecentAuthentication(session);
await accountService.delete(session.userId);
}Una Action puede invocarse aunque el botón esté oculto. Para operaciones críticas exige recent authentication o MFA.
Flujo simplificado:
start login
→ generate state + PKCE verifier
→ store challenge securely
→ redirect provider
→ callback with code/state
→ verify state
→ exchange code
→ validate ID token
→ link identity
→ create sessionNo aceptes un next arbitrario ni vincules automáticamente una identidad externa a una cuenta existente solo por coincidencia de email sin política segura.
Opciones:
Guarda secretos cifrados, recovery codes hasheados y registra cambios. MFA no reemplaza sesiones seguras.
Es frecuentemente la ruta más débil.
Token:
No uses preguntas de seguridad.
Separa:
identity registered
≠ email ownership verifiedLimita acciones hasta verificar cuando el dominio lo requiere. Un cambio de email necesita verificar el nuevo y avisar al anterior.
Defensa por capas:
No bloquees una cuenta indefinidamente con intentos que cualquiera puede provocar.
Genera una sesión nueva después de login y elevación de privilegios. No aceptes un ID de sesión elegido por cliente.
Atar rígidamente a IP puede expulsar usuarios móviles y no es defensa suficiente.
Revoca servidor y elimina cookie. Ofrece “cerrar todas las sesiones” y lista de dispositivos cuando el riesgo lo justifica.
Un cambio de password debe invalidar sesiones antiguas o incrementar una sessionVersion que se verifica.
No caches HTML personalizado públicamente. La lectura de cookie vuelve dinámica la región.
Puedes cachear datos públicos y después componer una región privada bajo Suspense.
No incluyas session/user en una key pública incompleta.
Mensajes neutrales:
“Correo o contraseña incorrectos”No distinguir cuenta inexistente de password incorrecto. Registra internamente outcome con datos minimizados.
No devuelvas tokens en query strings; terminan en historial, logs y referrers.
Usa librerías/proveedores mantenidos.
Permisos quedan obsoletos.
Solo es filtro optimista.
XSS puede leerlo; evalúa arquitectura y amenazas.
Mensajes y tiempos distintos revelan existencia.
Open redirect.
No revoca sesión.
Toma de cuenta.
Authorization y protección real usa la identidad autenticada para decidir acceso por acción, recurso, tenant y estado.