Skip to content

fix(auth): el login no responde 404, y el pacto se regenera contra el sobre real - #16

Merged
beyondnetPeru merged 1 commit into
mainfrom
fix/pacto-login-y-404-de-enumeracion
Aug 10, 2026
Merged

fix(auth): el login no responde 404, y el pacto se regenera contra el sobre real#16
beyondnetPeru merged 1 commit into
mainfrom
fix/pacto-login-y-404-de-enumeracion

Conversation

@beyondnetPeru

Copy link
Copy Markdown
Contributor

Cierra el único check rojo que quedaba en mainUms.ContractTest 37/38, rojo desde la resincronización (c011e42a). Eran dos fallos y solo uno era de contrato.

1. El 404 era un oráculo de enumeración

MapAuthError devolvía:

Código Significado Antes Ahora
AUTH_002 El tenant no existe 404 400
AUTH_004 El usuario no existe 404 401
AUTH_006 Credenciales inválidas 401 401

Los mensajes de AUTH_004 y AUTH_006 son idénticos palabra por palabra — «No pudimos iniciar sesión. Verifique sus credenciales.» Alguien los escribió así a propósito, para que quien intenta entrar no distinga «esa cuenta no existe» de «existe y te equivocaste». El estado HTTP deshacía ese cuidado: bastaba mirar si era 404 o 401 para saberlo, sin leer el cuerpo.

Una lista de correos contra POST /api/v1/auth/login separa las cuentas reales de las inventadas a razón de una petición por candidato. No da acceso, pero sí la mitad cara del trabajo: la que no se puede adivinar.

No es una idea nueva en esta plataforma. G-053 ya llegó a la misma conclusión para /api/v1/client/authenticate y colapsó allí AUTH_002 y AUTH_003 a 401 con mensaje genérico. El endpoint de portal nunca recibió el mismo tratamiento.

Por qué AUTH_002 va a 400 y no a 401

Porque aquí el cuerpo ya revela el tenant, y a propósito: el mensaje es «Verifique el código del tenant», distinto del de credenciales y elegido así para quien teclea mal el código de su organización. Colapsar el estado no ocultaría nada que el mensaje siga diciendo. En /client/authenticate —sin humano al otro lado— sí se colapsan los dos.

Esa asimetría es una decisión de producto anterior a este PR y aquí no se cambia. Queda señalada en ADR-0165 §2.3 para que no se lea como olvido: si se decidiera que el portal tampoco debe revelarlo, es un cambio de mensaje y de estado, y merece su propia conversación con producto.

Registrado como ADR-0165 (bilingüe) porque 401 donde el instinto REST pide 404 es exactamente lo que alguien «arregla» de vuelta si el motivo no está escrito.

2. El pacto verificaba un cuerpo que la API dejó de enviar

Declaraba { status, title }ProblemDetails, la forma por defecto de ASP.NET— y esta API responde LoginErrorResponse: { code, message, supportReferenceId }. El supportReferenceId es deliberado y es lo único que permite cruzar la queja de una persona con la traza del servidor, así que el contrato se corrige hacia la API, no al revés.

De paso, un segundo defecto más callado en el mismo fichero: la aserción de Content-Type usaba Match.Type("application/problem+json"). Un matcher de tipo sobre una cadena casa con cualquier cadena, así que esa línea daba OK contra un content type que la API ya no envía. La comprobación existía y no comprobaba nada — la misma forma que las claves de GraphQL de TD-004. Ahora se afirma por valor exacto.

Alcance

El front no cambia: auth.service.ts elige el mensaje por el code del cuerpo, nunca por el estado. La única rama que mira el estado es el respaldo para cuando no hay cuerpo, y sigue tratando 401 como «verifique sus credenciales».

El SDK no se ve afectado: habla con /api/v1/client/authenticate, otro endpoint, cuyo mapeo no se toca.

Registrado como TD-009 (resuelto) en technical-debt.md.

Verificación

  • Ums.ContractTest38/38 (37/38 antes)
  • Ums.Domain.Test — 930/930
  • Ums.Application.Test — 934/934
  • Ums.Presentation.IntegrationTest — 314/316 (2 saltados)
  • dotnet build Ums.sln — 0 errores

Queda pendiente

Reportar aguas arriba: unimar-ums tiene el mismo MapAuthError.

🤖 Generated with Claude Code

… sobre real

Cierra el único check rojo que quedaba en main (`Ums.ContractTest` 37/38, rojo desde
la resincronización). Eran dos fallos y solo uno era de contrato.

## El 404 era un oráculo de enumeración

`MapAuthError` devolvía 404 para AUTH_002 (tenant inexistente) y AUTH_004 (usuario
inexistente), y 401 para AUTH_006 (credenciales inválidas). Los mensajes de AUTH_004 y
AUTH_006 son idénticos palabra por palabra —alguien los escribió así a propósito, para
que no se distinga «esa cuenta no existe» de «te equivocaste»— y el estado deshacía ese
cuidado: bastaba mirar 404 contra 401 para saberlo, sin leer el cuerpo. Una lista de
correos contra POST /api/v1/auth/login separa las cuentas reales de las inventadas a
petición por candidato.

Es la misma conclusión que G-053 ya había aplicado en `/client/authenticate`, el
endpoint anónimo de máquina, donde AUTH_002 y AUTH_003 se colapsaron a 401 con mensaje
genérico. El endpoint de portal nunca recibió ese tratamiento.

AUTH_004 pasa a 401, indistinguible de AUTH_006. AUTH_002 pasa a 400 —y no a 401—
porque aquí el CUERPO ya revela el tenant a propósito («Verifique el código del
tenant»): colapsar el estado no ocultaría nada que el mensaje siga diciendo. Esa
asimetría es una decisión de producto anterior a este cambio; queda señalada en el ADR
para que no se lea como olvido.

Registrado como ADR-0165, porque 401 donde el instinto REST pide 404 es justo lo que
alguien «arregla» de vuelta si el motivo no está escrito.

## El pacto verificaba un cuerpo que la API dejó de enviar

Declaraba `{ status, title }` —ProblemDetails, la forma por defecto de ASP.NET— y esta
API responde `LoginErrorResponse`: `{ code, message, supportReferenceId }`. El
`supportReferenceId` es deliberado y es lo único que permite cruzar la queja de una
persona con la traza del servidor, así que el contrato se corrige hacia la API.

De paso, la aserción de `Content-Type` usaba `Match.Type("application/problem+json")`:
un matcher de tipo sobre una cadena casa con CUALQUIER cadena, así que esa línea daba
OK contra un content type que la API ya no envía. La comprobación existía y no
comprobaba nada — la misma forma que las claves de GraphQL de TD-004. Ahora se afirma
por valor exacto.

El front no cambia: `auth.service.ts` elige el mensaje por el `code` del cuerpo, nunca
por el estado.

Verificado: contrato 38/38, Domain 930/930, Application 934/934, Integración 314/316
(2 saltados), `dotnet build` 0 errores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@beyondnetPeru
beyondnetPeru merged commit 8a4d38e into main Aug 10, 2026
24 checks passed
@beyondnetPeru
beyondnetPeru deleted the fix/pacto-login-y-404-de-enumeracion branch August 10, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant