Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class LoginUseCase {

async execute(input: LoginDto, clientMeta: ClientMeta = {}) {
const normalizedEmail = this.normalizeEmail(input.email);
console.log(`[AUTH_DEBUG] Received login request for email: ${normalizedEmail}`);
authDebugLog('[AUTH-BACK] login use-case start', {
email: normalizedEmail,
passwordLength: input.password ? input.password.length : 0,
Expand All @@ -45,7 +46,8 @@ export class LoginUseCase {
if (!user) {
authDebugLog('[AUTH-BACK] user lookup', { email: normalizedEmail, found: false });
// Prevent timing attacks by hashing a static string
await bcryptjs.compare(input.password, '$2a$12$dummyhashdummyhashdummyhashdummyhashdummyhashdummyha');
console.log('[AUTH_DEBUG] User not found, executing dummy bcrypt compare');
await bcryptjs.compare(input.password, '$2a$10$XUaE2o.8.vR.1W1oW8qF3ucH/qH6kXq5lA.pXQvP3x.o.lqZ6g3G6');
throw new UnauthorizedError('Invalid email or password');
}

Expand All @@ -60,6 +62,7 @@ export class LoginUseCase {
});

isPasswordValid = await bcryptjs.compare(input.password, user.password);
console.log(`[AUTH_DEBUG] Password comparison result for user ${user.id}: ${isPasswordValid}`);
if (!isPasswordValid) {
authDebugLog('[AUTH-BACK] password check', { userId: user.id, ok: false });
throw new UnauthorizedError('Invalid email or password');
Expand Down Expand Up @@ -107,6 +110,7 @@ export class LoginUseCase {
role: user.role,
});

console.log(`[AUTH_DEBUG] Generated token payload for user ${user.id}`);
authDebugLog('[AUTH-BACK] tokens issued', {
userId: user.id,
hasAccessToken: Boolean(tokens.accessToken),
Expand Down
8 changes: 6 additions & 2 deletions back/src/modules/auth/infrastructure/http/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,16 @@ export class AuthController {
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7d
});
console.log(`[AUTH_DEBUG] Successfully set refreshToken cookie for request ${requestId}`);
authDebugLog('[AUTH-COOKIE] refresh cookie set', {
requestId,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAgeSeconds: 7 * 24 * 60 * 60,
});
return { user: result.user, accessToken: result.accessToken };
}

console.log(`[AUTH_DEBUG] Returning JSON response with tokens and user object`);
authDebugLog('[AUTH-BACK] login response', {
requestId,
hasAccessToken: Boolean(result.accessToken),
Expand All @@ -92,6 +93,8 @@ export class AuthController {
role: result.user?.role,
});

// We always return the refreshToken in the payload even if cookies are set
// so that the Next.js BFF does not break due to missing tokens in the payload.
return result;
}

Expand Down Expand Up @@ -135,15 +138,16 @@ export class AuthController {
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
console.log(`[AUTH_DEBUG] Successfully set refreshToken cookie for request ${requestId}`);
authDebugLog('[AUTH-COOKIE] refresh cookie set', {
requestId,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAgeSeconds: 7 * 24 * 60 * 60,
});
return { user: result.user, accessToken: result.accessToken };
}

console.log(`[AUTH_DEBUG] Returning JSON response with tokens and user object on refresh`);
authDebugLog('[AUTH-BACK] refresh response', {
requestId,
hasAccessToken: Boolean(result.accessToken),
Expand Down
35 changes: 35 additions & 0 deletions docs/issues/01-critical-login-bff-token-mismatch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Título:
[AUTH] El login falla porque Next.js (BFF) espera un refreshToken en el JSON que NestJS no devuelve

# Descripción:
- **Contexto del problema:** En el flujo de autenticación actua Next.js Route Handler (`/api/auth/login`) como BFF (Backend for Frontend). Este recibe la respuesta de NestJS (`/auth/login`) y extrae los tokens `accessToken` y `refreshToken` para guardarlos en cookies httpOnly.
- **Qué está fallando:** Si en el backend de NestJS la variable de entorno `AUTH_COOKIES` está activada (`true`), NestJS intenta colocar el `refreshToken` directamente en una cookie (`response.cookie(...)`) y omite el `refreshToken` en el cuerpo de la respuesta JSON (retorna solo `{ user, accessToken }`). Cuando el BFF de Next.js recibe esta respuesta, falla en la validación `!payload.refreshToken` y devuelve un error 502 al frontend.
- **Evidencia (logs o comportamiento):** Al intentar loguearse, el frontend muestra un error "Error al iniciar sesión". En los logs del BFF (Next.js) aparece `[auth.login] invalid payload` indicando que faltan tokens.
- **Impacto:** Crítico. Rompe el inicio de sesión por completo en cualquier entorno donde `AUTH_COOKIES='true'` esté activo en el backend.

# Pasos para reproducir:
1. Configurar `AUTH_COOKIES=true` en el backend (NestJS).
2. Intentar loguearse desde el frontend (Next.js AuthPage).
3. Verificar la respuesta de la red: Next.js retorna 502 Bad Gateway.

# Comportamiento esperado:
El BFF de Next.js debería poder procesar el login. O bien, NestJS debe siempre retornar ambos tokens en el JSON independientemente de las cookies que intente setear, O la arquitectura debe delegar el manejo de cookies exclusivamente al BFF.

# Comportamiento actual:
NestJS no devuelve `refreshToken` en el JSON, lo que rompe la validación estricta del BFF de Next.js.

# Solución propuesta:
Eliminar la condicional en `auth.controller.ts` (NestJS) que omite `refreshToken` del objeto retornado. Siempre devolver `{ user, accessToken, refreshToken }` en el JSON para que cualquier cliente API/BFF pueda usarlo. Opcionalmente, quitar la lógica de cookies del backend ya que tenemos un BFF (Next.js).

# Checklist:
- [ ] Fix implementado
- [ ] Test agregado
- [ ] Validado manualmente

# Labels sugeridos:
- auth
- bug
- backend

# Prioridad:
- Critical
34 changes: 34 additions & 0 deletions docs/issues/02-security-bcrypt-dummy-hash-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Título:
[AUTH] Login falla silenciosamente para usuarios inexistentes (Dummy Hash Bcrypt error)

# Descripción:
- **Contexto del problema:** En el caso de uso `login.use-case.ts` (backend), cuando un usuario no existe, la aplicación intenta realizar una comparación dummy con `bcryptjs.compare` usando un hash hardcodeado para prevenir ataques de *timing*.
- **Qué está fallando:** El hash dummy utilizado `$2a$12$dummyhashdummyhashdummyhashdummyhashdummyhashdummyha` es inválido (provoca un error de formato o versión de salt en bcryptjs). Esto causa que bcryptjs lance una excepción no controlada (`Error: Invalid salt version`), lo que devuelve un error 500 al cliente en lugar del esperado error 401 (Unauthorized), rompiendo el flujo limpio.
- **Evidencia (logs o comportamiento):** Al ingresar un email de un usuario que no existe en la base de datos, el servidor NestJS arroja un 500 Internal Server Error en la consola por la excepción de bcryptjs.
- **Impacto:** Alto. Expone errores internos del servidor, corrompe el flujo esperado del endpoint y permite *username enumeration* (enumeración de usuarios) debido a que los tiempos y el código HTTP de respuesta difieren entre usuarios existentes y no existentes.

# Pasos para reproducir:
1. Intentar iniciar sesión con un correo electrónico que no esté registrado en la base de datos (por ejemplo, `no-existe@example.com`).
2. Observar la respuesta HTTP del servidor. En lugar de un 401, el servidor devuelve un 500 con un log de error en la consola por parte de bcrypt.

# Comportamiento esperado:
El servidor debe retornar silenciosamente un error 401 Unauthorized sin arrojar excepciones internas de bcryptjs.

# Comportamiento actual:
El servidor arroja un 500 por un hash mal formado usado en la prevención de timing attacks.

# Solución propuesta:
Generar un hash estático válido con bcrypt (ej. `$2a$10$XUaE2o.8.vR.1W1oW8qF3ucH/qH6kXq5lA.pXQvP3x.o.lqZ6g3G6`) y utilizarlo como cadena dummy en `login.use-case.ts`.

# Checklist:
- [ ] Fix implementado
- [ ] Test agregado
- [ ] Validado manualmente

# Labels sugeridos:
- auth
- security
- backend

# Prioridad:
- High
34 changes: 34 additions & 0 deletions docs/issues/03-architecture-auth-cookies-coupling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Título:
[AUTH] Acoplamiento de arquitectura: Next.js y NestJS colisionan al intentar setear Cookies de Autenticación

# Descripción:
- **Contexto del problema:** Tenemos una arquitectura Frontend (Next.js) -> BFF (Next.js Route Handlers) -> Backend API (NestJS). La responsabilidad de mantener la sesión web recae en el BFF mediante cookies httpOnly.
- **Qué está fallando:** El backend de NestJS todavía contiene lógica fuertemente acoplada a la web para setear y leer cookies (`req.cookies.refreshToken`, `res.cookie('refreshToken')`), controlada mediante la variable `AUTH_COOKIES`. Esto es problemático en arquitecturas BFF / multi-tenant / CORS, ya que NestJS intenta setear cookies en dominios cruzados que a menudo fallan. Al tener el BFF en Next.js gestionando esto, la lógica de cookies del backend es redundante, conflictiva y rompe respuestas (ver Issue 01).
- **Evidencia (logs o comportamiento):** Ambos sistemas tienen código para emitir la cabecera `Set-Cookie` para la misma información.
- **Impacto:** Medio. Confusión en el flujo, errores de CORS al setear cookies directamente desde el backend API a un dominio frontend, y acoplamiento innecesario del backend API a clientes web específicos.

# Pasos para reproducir:
1. Revisar `auth.controller.ts` en el backend: hay lógica que usa la dependencia `cookie-parser` y modifica `res` directamente.
2. Revisar `route.ts` en `/api/auth/login` (Next.js): hay lógica que usa la API de cookies de Next.js.

# Comportamiento esperado:
Un Backend API puro e independiente de la presentación que solo envíe cabeceras `Authorization` o cuerpos de respuesta JSON. El cliente BFF debe ser el único responsable de transformar estos tokens en mecanismos de estado web (Cookies).

# Comportamiento actual:
Responsabilidades divididas y duplicadas entre NestJS y el BFF (Next.js) respecto al manejo de Cookies.

# Solución propuesta:
Refactorizar el backend para eliminar el soporte y la lógica de cookies (`res.cookie` y dependencias directas en controladores como `cookie-parser`). Dejar que las cookies sean problema exclusivo del BFF en Next.js. El controlador de NestJS solo debe recibir encabezados estandar y cuerpos JSON, y devolver JSON puro.

# Checklist:
- [ ] Fix implementado
- [ ] Test agregado
- [ ] Validado manualmente

# Labels sugeridos:
- auth
- architecture
- backend

# Prioridad:
- Medium
3 changes: 3 additions & 0 deletions front/src/app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export async function POST(request: NextRequest) {
const payload = data.data ?? data;

if (!res.ok) {
console.log('[AUTH_DEBUG] BFF login failed with upstream error', { status: res.status });
console.log('[auth.login] upstream error', {
status: res.status,
message: data.message,
Expand All @@ -56,6 +57,7 @@ export async function POST(request: NextRequest) {
}

if (!payload.accessToken || !payload.refreshToken || !payload.user) {
console.log('[AUTH_DEBUG] BFF login failed: Missing accessToken or refreshToken in JSON payload');
console.log('[auth.login] invalid payload', {
status: res.status,
hasUser: Boolean(payload.user),
Expand All @@ -67,6 +69,7 @@ export async function POST(request: NextRequest) {

const response = NextResponse.json({ user: payload.user });
applyAuthCookies(response, { accessToken: payload.accessToken, refreshToken: payload.refreshToken });
console.log('[AUTH_DEBUG] Successfully set httpOnly cookies for Next.js session');
authRouteLog('api login cookies set', {
hasAccessToken: Boolean(payload.accessToken),
hasRefreshToken: Boolean(payload.refreshToken),
Expand Down
12 changes: 10 additions & 2 deletions front/src/app/auth/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { Button, Form, Input, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@heroui/react';
import { Check, Copy } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { Suspense, useEffect, useRef, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { useAuth } from '@/contexts/AuthContext';
Expand All @@ -29,7 +29,7 @@ const SEED_TEST_USERS: { label: string; email: string; role: string }[] = [
{ label: 'CUSTOMER', email: 'carlos@nexstore.com', role: 'CUSTOMER' },
];

export default function AuthPage() {
function AuthPageContent() {
const { login, register, resendVerification, user } = useAuth();
const router = useRouter();
const searchParams = useSearchParams();
Expand Down Expand Up @@ -282,3 +282,11 @@ export default function AuthPage() {
</>
);
}

export default function AuthPage() {
return (
<Suspense fallback={<div className="flex justify-center items-center h-screen">Cargando...</div>}>
<AuthPageContent />
</Suspense>
);
}
3 changes: 1 addition & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading