From 4d9b75994f5242416f6542b4ed3d35e479190a66 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Mon, 16 Feb 2026 15:44:31 -0600 Subject: [PATCH] =?UTF-8?q?fix(#411):=20add=20runtime=20null=20checks=20in?= =?UTF-8?q?=20auth=20controller=20=E2=80=94=20defense-in-depth=20for=20Aut?= =?UTF-8?q?henticatedRequest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- apps/api/src/auth/auth.controller.spec.ts | 49 +++++++++++++++++++++-- apps/api/src/auth/auth.controller.ts | 10 ++++- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/apps/api/src/auth/auth.controller.spec.ts b/apps/api/src/auth/auth.controller.spec.ts index 80229a0..2bec348 100644 --- a/apps/api/src/auth/auth.controller.spec.ts +++ b/apps/api/src/auth/auth.controller.spec.ts @@ -21,7 +21,7 @@ vi.mock("better-auth/plugins", () => ({ })); import { Test, TestingModule } from "@nestjs/testing"; -import { HttpException, HttpStatus } from "@nestjs/common"; +import { HttpException, HttpStatus, UnauthorizedException } from "@nestjs/common"; import type { AuthUser, AuthSession } from "@mosaic/shared"; import type { Request as ExpressRequest, Response as ExpressResponse } from "express"; import { AuthController } from "./auth.controller"; @@ -287,9 +287,50 @@ describe("AuthController", () => { expect(result).toEqual(expected); }); - // Note: Tests for missing user/session were removed because - // AuthenticatedRequest guarantees both are present (enforced by AuthGuard). - // NestJS returns 401 before getSession is reached if the guard rejects. + it("should throw UnauthorizedException when req.user is undefined", () => { + const mockRequest = { + session: { + id: "session-123", + token: "session-token", + expiresAt: new Date(Date.now() + 86400000), + }, + }; + + expect(() => controller.getSession(mockRequest as never)).toThrow( + UnauthorizedException, + ); + expect(() => controller.getSession(mockRequest as never)).toThrow( + "Missing authentication context", + ); + }); + + it("should throw UnauthorizedException when req.session is undefined", () => { + const mockRequest = { + user: { + id: "user-123", + email: "test@example.com", + name: "Test User", + }, + }; + + expect(() => controller.getSession(mockRequest as never)).toThrow( + UnauthorizedException, + ); + expect(() => controller.getSession(mockRequest as never)).toThrow( + "Missing authentication context", + ); + }); + + it("should throw UnauthorizedException when both req.user and req.session are undefined", () => { + const mockRequest = {}; + + expect(() => controller.getSession(mockRequest as never)).toThrow( + UnauthorizedException, + ); + expect(() => controller.getSession(mockRequest as never)).toThrow( + "Missing authentication context", + ); + }); }); describe("getProfile", () => { diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts index 45e5728..9e171aa 100644 --- a/apps/api/src/auth/auth.controller.ts +++ b/apps/api/src/auth/auth.controller.ts @@ -10,6 +10,7 @@ import { Logger, HttpException, HttpStatus, + UnauthorizedException, } from "@nestjs/common"; import { Throttle } from "@nestjs/throttler"; import type { Request as ExpressRequest, Response as ExpressResponse } from "express"; @@ -33,8 +34,13 @@ export class AuthController { @Get("session") @UseGuards(AuthGuard) getSession(@Request() req: AuthenticatedRequest): AuthSession { - // AuthGuard guarantees user and session are present — NestJS returns 401 - // before this method is reached if the guard rejects. + // Defense-in-depth: AuthGuard should guarantee these, but if someone adds + // a route with AuthenticatedRequest and forgets @UseGuards(AuthGuard), + // TypeScript types won't help at runtime. + if (!req.user || !req.session) { + throw new UnauthorizedException("Missing authentication context"); + } + return { user: req.user, session: {