DEV-88 BE — Middleware de autenticación JWT — feat(auth): add a global JWT guard with explicit public opt-out - #45
Open
LucianABC wants to merge 1 commit into
Open
DEV-88 BE — Middleware de autenticación JWT — feat(auth): add a global JWT guard with explicit public opt-out#45LucianABC wants to merge 1 commit into
LucianABC wants to merge 1 commit into
Conversation
Validates the Bearer token and leaves the actor in req.user, with @currentuser() to read it and @public() to opt a route out. The guard is registered globally rather than applied per route. Opting in means a forgotten guard leaves an endpoint open and nothing says so; opting out means a forgotten @public() returns 401 on the first request. The way this fails by accident is now closed rather than open. Every current route is marked public, so nothing changes for callers yet. Games, playbooks and the healthcheck are permanently public — they carry no owner. Register and login are public by necessity, since they are how a token is obtained. Characters is the one temporary case, and it is flagged as such: protecting it today would 401 the frontend without fixing anything, because the queries still return every user's characters to whoever holds a token. That is DEV-59 and DEV-64. The principal comes from the token alone, with no database lookup, so every request does not pay for a case that cannot happen yet: there is no way to delete a user. When there is, a deleted user's token staying valid until expiry needs revisiting. Because no real route is protected yet, the global registration is covered by an integration spec with probe controllers — otherwise a broken APP_GUARD would go unnoticed until the first protected route. Verified against the running build by temporarily unprotecting /games: missing, forged, expired and alg:none tokens all get 401; a real one gets 200. Refs: DEV-88
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
JwtAuthGuard: validaAuthorization: Bearer <token>y deja el actor enreq.user.@CurrentUser()para leerlo en un handler,@Public()para abrir una ruta.La decisión de diseño
El guard se registra globalmente (
APP_GUARD) en vez de aplicarse ruta por ruta.Con opt-in, olvidarse del guard deja un endpoint abierto y nada lo delata. Con opt-out, olvidarse de
@Public()devuelve 401 en el primer request. El modo de fallar por descuido pasa a ser "de más" en vez de "de menos" — que es lo que querés en la capa de auth.Los
@Public()que agrego, y por qué:/(health)/games,/playbooks/auth/register,/auth/login/charactersEl de characters es el único que hay que sacar. Protegerlo hoy daría 401 al front sin arreglar nada de fondo: las queries siguen devolviendo los personajes de todos a cualquiera que tenga token, y
create()todavía tomaownerIddel body. El arreglo real es scopear por el actor — DEV-59 (listado) y DEV-64 (detalle/edición/borrado). Está marcado con un comentario grande en el controller.Otra decisión: el guard no consulta la base
El principal sale del token y nada más. Agregar un lookup por request encarecería todos los endpoints para cubrir un caso que hoy no existe: no hay baja de usuarios. La consecuencia a tener presente es que, si un usuario se borrara, su token seguiría válido hasta expirar — cuando exista esa baja hay que revisar esto.
/auth/me(DEV-86), que sí necesita el usuario completo, lo busca porsub.Verificación
Hay un problema propio de este ticket: como todavía no existe ninguna ruta realmente protegida, el guard global no se puede verificar de punta a punta. Un
APP_GUARDmal registrado pasaría desapercibido hasta DEV-86. Dos cosas para cubrirlo:jwt-auth.guard.integration.spec.ts) — levantaAuthModulecon una ruta protegida y una@Public(), y comprueba que el guard intercepta de verdad. Queda como regresión en CI.@Public()de/games(después restaurado):401Token de autenticación requerido200con los 4 games401401alg: none)401Test plan
jwt-auth.guard.spec.ts— 14 casos: token válido/falsificado/vencido/alg:none/sinsub, y parseo del header (sin esquema,Basic, minúscula, partes de más, vacío)jwt-auth.guard.integration.spec.ts— el guard global intercepta;/auth/loginsigue accesible (afirmando sobre el mensaje, porque un 401 del guard y uno de credenciales no se distinguen por status)public.decorator.spec.ts— el decorador escribe la metadata que el guard lee, en handler y en clasecurrent-user.decorator.spec.ts— devuelve el actor; tira 401 en vez deundefined(que terminaría comoownerId: undefineden una query, sin scopear nada)npm run lint,build,test:cov(153 tests) ytest:e2e(11) verdes/,/games,/playbooks,/charactersy/auth/loginsiguen respondiendo 200 sin tokenRefs: DEV-88