fix(#338): Add rate limiting and logging to auth catch-all route

- Apply restrictive rate limits (10 req/min) to prevent brute-force attacks
- Log requests with path and client IP for monitoring and debugging
- Extract client IP handling for proxy setups (X-Forwarded-For)
- Add comprehensive tests for rate limiting and logging behavior

Refs #338
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-05 16:49:06 -06:00
parent 06de72a355
commit 970cc9f606
2 changed files with 246 additions and 1 deletions

View File

@@ -1,4 +1,5 @@
import { Controller, All, Req, Get, UseGuards, Request } from "@nestjs/common";
import { Controller, All, Req, Get, UseGuards, Request, Logger } from "@nestjs/common";
import { Throttle } from "@nestjs/throttler";
import type { AuthUser, AuthSession } from "@mosaic/shared";
import { AuthService } from "./auth.service";
import { AuthGuard } from "./guards/auth.guard";
@@ -16,6 +17,8 @@ interface RequestWithSession {
@Controller("auth")
export class AuthController {
private readonly logger = new Logger(AuthController.name);
constructor(private readonly authService: AuthService) {}
/**
@@ -76,10 +79,46 @@ export class AuthController {
/**
* Handle all other auth routes (sign-in, sign-up, sign-out, etc.)
* Delegates to BetterAuth
*
* Rate limit: "strict" tier (10 req/min) - More restrictive than normal routes
* to prevent brute-force attacks on auth endpoints
*
* Security note: This catch-all route bypasses standard guards that other routes have.
* Rate limiting and logging are applied to mitigate abuse (SEC-API-10).
*/
@All("*")
@Throttle({ strict: { limit: 10, ttl: 60000 } })
async handleAuth(@Req() req: Request): Promise<unknown> {
// Extract client IP for logging
const clientIp = this.getClientIp(req);
const requestPath = (req as unknown as { url?: string }).url ?? "unknown";
const method = (req as unknown as { method?: string }).method ?? "UNKNOWN";
// Log auth catch-all hits for monitoring and debugging
this.logger.debug(`Auth catch-all: ${method} ${requestPath} from ${clientIp}`);
const auth = this.authService.getAuth();
return auth.handler(req);
}
/**
* Extract client IP from request, handling proxies
*/
private getClientIp(req: Request): string {
const reqWithHeaders = req as unknown as {
headers?: Record<string, string | string[] | undefined>;
ip?: string;
socket?: { remoteAddress?: string };
};
// Check X-Forwarded-For header (for reverse proxy setups)
const forwardedFor = reqWithHeaders.headers?.["x-forwarded-for"];
if (forwardedFor) {
const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor;
return ips?.split(",")[0]?.trim() ?? "unknown";
}
// Fall back to direct IP
return reqWithHeaders.ip ?? reqWithHeaders.socket?.remoteAddress ?? "unknown";
}
}