⚙ Explorador de Código

Exploración: Sistema de Autenticación

NexoLearn SaaS — Next.js 14 + Node.js/Express + PostgreSQL
Analizado el 18 jun 2026 📄 6 archivos clave 🔗 4 capas de arquitectura ⚠️ Objetivo: integrar MFA TOTP
📍

Puntos de Entrada

HTTP POST
POST /api/auth/[...nextauth]
Login con credentials (email + contraseña). Disparado por el formulario /login via NextAuth signIn().
OAuth Redirect
GET /api/auth/callback/google
SSO con Google/Microsoft. NextAuth gestiona el intercambio de código OAuth2 y crea la sesión.
Middleware Edge
src/middleware.ts
Intercepta TODAS las requests. Verifica JWT antes de permitir acceso a rutas protegidas /dashboard/**.
API Guard
withRBAC() higher-order fn
Wrapper sobre handlers de API. Comprueba roles del token decodificado contra la política del recurso.
React Hook
usePermissions()
Consultado por componentes UI para renderizado condicional de acciones según el rol del usuario.
Cron / Background
POST /api/auth/refresh
Llamado silenciosamente cada 14 min por el cliente para renovar el access token usando el refresh token.

Flujo de Ejecución — Login Credentials

PASO 1 — Capa de Presentación
Usuario envía formulario de login
LoginForm.tsx llama signIn('credentials', {email, password}). Validación Zod client-side antes del POST.
PASO 2 — NextAuth Route Handler
Handler NextAuth recibe la requestASYNC
[...nextauth]/route.ts ejecuta el callback authorize() del provider credentials.
PASO 3 — Capa de Servicio Auth
Verificación de credenciales en DB
Consulta users en PostgreSQL → compara hash bcrypt → si falla, retorna null (NextAuth devuelve 401).
PASO 4 — Generación de Tokens
Emisión de access + refresh JWTASYNC
jwt.ts: access token (15 min, HS256, payload: {userId, orgId, role, permissions[]}). Refresh token (7 días) guardado en tabla refresh_tokens.
PASO 5 — Callback JWT de NextAuth
Enriquecimiento del token de sesión
Callback jwt() añade accessToken, role y orgId al objeto session. El token JWT de NextAuth envuelve el access token propio.
PASO 6 — Middleware Edge
Verificación en cada request protegida
middleware.ts usa getToken() de NextAuth. Si el token expira → redirige a /login?callbackUrl=.... ⚠️ No verifica la firma del access token propio, solo el de NextAuth.
PASO 7 — RBAC en API handlers
Autorización por rolCRÍTICO
withRBAC(handler, ['org_admin', 'superadmin']) decodifica el access token propio → verifica rol. La lógica de permisos está duplicada: aquí y en usePermissions() del cliente.
PASO 8 — Refresco silencioso
Renovación automática de tokensBACKGROUND
AuthProvider.tsx monta un setInterval cada 840 s → llama /api/auth/refresh → endpoint valida refresh token en DB y emite nuevo par.
🏛

Capas de Arquitectura

🌐 Edge / Middleware Crítico para MFA
Intercepta rutas /dashboard/**, /api/v1/**
Comunica con NextAuth via getToken() — sin DB
No puede importar módulos Node.js (edge runtime)
Aquí debe añadirse el check mfa_verified del token
🔨 NextAuth Route Handler Modificar para MFA
Orquesta el flujo OAuth2 + credentials
Callbacks jwt() y session() enriquecen el token
Aquí añadir mfa_enabled y mfa_verified al token
Callback signIn() puede bloquear el flujo si MFA requerido
🛡️ Capa de Autorización (RBAC) Lógica duplicada
rbac.ts define matriz roles → permisos
withRBAC() en server, usePermissions() en client
Deberían compartir el mismo source of truth
MFA no afecta esta capa directamente
💾 Capa de Persistencia (PostgreSQL) Migración necesaria
Tabla users: credenciales + rol + orgId
Tabla refresh_tokens: token, userId, expiresAt
Añadir: mfa_secret, mfa_enabled, mfa_backup_codes
Tabla nueva: mfa_challenges para flujo de verificación
🧸

Patrones Identificados

HOF Higher-Order Function Guard
src/lib/auth/rbac.ts → withRBAC()
Wrapper funcional sobre API handlers. Consistente y reutilizable. Extender para añadir check de MFA en rutas sensibles.
HOOK Custom Auth Hook
src/hooks/usePermissions.ts
Exposición de permisos del token de sesión a componentes. El estado MFA también debería exponerse aquí.
PROVIDER Context Provider
src/components/AuthProvider.tsx
Envuelve la app con SessionProvider de NextAuth. Gestiona el refresh silencioso con setInterval. Punto natural para inicializar el estado MFA.
MIDDLEWARE Edge Guard
src/middleware.ts
Pattern "guard at the gate": verifica auth antes de llegar a cualquier página/handler. Convenio Next.js bien implementado.
ENUM Role Enum + Matrix
src/lib/auth/rbac.ts
Roles centralizados como enum TypeScript + objeto de permisos. Buena práctica. La duplicación con el cliente es el único anti-patrón.
JWT Token-in-Token
NextAuth JWT ↔ access token propio
El JWT de NextAuth envuelve un access token propio. Introduce complejidad: el middleware verifica uno, la API verifica el otro. A documentar bien.
📄

Archivos Clave

Archivo Rol Importancia
src/app/api/auth/[...nextauth]/route.ts Orquestador del flujo auth (credentials + OAuth) Crítico
src/middleware.ts Guard de edge runtime — protección de rutas Crítico
src/lib/auth/jwt.ts Utilidades: sign, verify, decode del access token propio Crítico
src/lib/auth/rbac.ts Definición de roles, permisos y HOF withRBAC() Alto
src/components/AuthProvider.tsx Provider React + lógica de refresh silencioso Alto
src/hooks/usePermissions.ts Exposición de permisos a componentes UI Medio
🔁

Dependencias

Externas

next-auth@5
Gestión de sesión + OAuth + callbacks JWT
jsonwebtoken
Firma y verificación del access token propio
bcryptjs
Hash y verificación de contraseñas
zod
Validación de esquemas — inputs de login
@prisma/client
ORM para acceso a PostgreSQL (users, refresh_tokens)

Internas

src/lib/db
Cliente Prisma singleton
src/lib/auth/jwt.ts
Utilidades JWT propias — usadas por NextAuth + API handlers
src/lib/auth/rbac.ts
Lógica de autorización — usada por withRBAC + usePermissions
src/types/auth.ts
Tipos TypeScript compartidos (User, Role, JWTPayload)
src/lib/email
Envío de OTP por email — si se implementa MFA por email

Recomendaciones para nuevo desarrollo (MFA TOTP)

Añadir MFA en el callback signIn() de NextAuth
Retornar false en signIn() si el usuario tiene mfa_enabled=true pero aún no ha verificado el TOTP. Redirigir a /auth/mfa-challenge con un token de estado temporal.
♻️
Reutilizar el patrón withRBAC() para el guard MFA
Crear withMFA(handler) siguiendo el mismo patrón HOF de withRBAC(). Verificar el campo mfa_verified del JWT antes de continuar al handler.
♻️
Extender jwt.ts para incluir mfa_verified en el payload
El access token ya contiene userId, orgId, role, permissions[]. Añadir mfa_verified: boolean y mfa_at: timestamp. Actualizar el tipo JWTPayload en src/types/auth.ts.
⚠️
Unificar la lógica duplicada de permisos antes de añadir MFA
La comprobación de roles en withRBAC() (server) y usePermissions() (client) está duplicada. Crear un módulo src/lib/auth/permissions.ts como fuente única e importarlo en ambos lados.
🚫
No verificar firma del access token propio en el middleware Edge
El runtime Edge no puede importar jsonwebtoken (Node.js). Confiar en el token de NextAuth para el middleware y usar el access token propio solo en los API handlers (Node.js runtime).
🚫
No almacenar el TOTP secret en el JWT ni en la sesión del cliente
El mfa_secret (semilla TOTP) debe quedar solo en la columna users.mfa_secret de PostgreSQL, cifrado en reposo. En el JWT solo va mfa_verified: true/false.

🔑 Plan de implementación MFA sugerido

1. Migración DB — Añadir mfa_secret TEXT, mfa_enabled BOOLEAN DEFAULT false, mfa_backup_codes TEXT[] a la tabla users. Crear tabla mfa_challenges(id, user_id, expires_at) para el estado temporal entre pasos 1 y 2 del login.

2. Setup flow — Nueva ruta /settings/security/mfa: generar secret con otplib, mostrar QR con qrcode, verificar primer código, persistir mfa_enabled=true.

3. Login flow — Modificar callback signIn() en [...nextauth]/route.ts: si mfa_enabled, redirigir a /auth/mfa-challenge?challenge_id=.... Crear handler POST /api/auth/mfa/verify que valide el TOTP, actualice el token JWT con mfa_verified: true y complete la sesión.

4. Guards — Añadir withMFA() a handlers de operaciones críticas (transferencias de crédito, cambio de plan). Actualizar middleware.ts para redirigir a challenge si el token tiene mfa_verified: false y la ruta lo requiere.