mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 07:33:39 +02:00
feat(auth): add audit logging, account lockout, and API key rate limiting
1. SecurityEventsService: Centralized audit logging for all auth events (login, register, logout, password changes, API key operations, SSO token exchange, etc.). Fire-and-forget pattern ensures auth flows are never blocked by logging failures. 2. AccountLockoutService: Locks accounts after 5 failed login attempts within 15 minutes. 30-minute lockout duration. Fails open on DB errors. Clears attempts on successful login. Email-not-verified does not count as a failed attempt. 3. API Key validation endpoint secured with rate limiting (10 req/min per IP via ThrottlerGuard) and audit logging. Key prefixes logged for forensics, never full keys. New schema: auth.login_attempts table for tracking failed logins. 174 tests passing across all auth and security modules. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
effa57fd61
commit
f7df8e97aa
14 changed files with 700 additions and 68 deletions
|
|
@ -5,19 +5,26 @@ import {
|
|||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Req,
|
||||
UseGuards,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { ApiKeysService } from './api-keys.service';
|
||||
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser } from '../common/decorators/current-user.decorator';
|
||||
import type { CurrentUserData } from '../common/decorators/current-user.decorator';
|
||||
import { CreateApiKeyDto, ValidateApiKeyDto } from './dto';
|
||||
import { SecurityEventsService, SecurityEventType } from '../security';
|
||||
|
||||
@Controller('api-keys')
|
||||
export class ApiKeysController {
|
||||
constructor(private readonly apiKeysService: ApiKeysService) {}
|
||||
constructor(
|
||||
private readonly apiKeysService: ApiKeysService,
|
||||
private readonly securityEvents: SecurityEventsService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* List all API keys for the authenticated user
|
||||
|
|
@ -34,8 +41,20 @@ export class ApiKeysController {
|
|||
*/
|
||||
@Post()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async createKey(@CurrentUser() user: CurrentUserData, @Body() dto: CreateApiKeyDto) {
|
||||
return this.apiKeysService.createApiKey(user.userId, dto);
|
||||
async createKey(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Body() dto: CreateApiKeyDto,
|
||||
@Req() req: Request
|
||||
) {
|
||||
const result = await this.apiKeysService.createApiKey(user.userId, dto);
|
||||
|
||||
this.securityEvents.logEventWithRequest(req, {
|
||||
userId: user.userId,
|
||||
eventType: SecurityEventType.API_KEY_CREATED,
|
||||
metadata: { keyId: result.id, name: dto.name, scopes: dto.scopes },
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -44,16 +63,48 @@ export class ApiKeysController {
|
|||
@Delete(':id')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async revokeKey(@CurrentUser() user: CurrentUserData, @Param('id') id: string) {
|
||||
async revokeKey(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('id') id: string,
|
||||
@Req() req: Request
|
||||
) {
|
||||
await this.apiKeysService.revokeApiKey(user.userId, id);
|
||||
|
||||
this.securityEvents.logEventWithRequest(req, {
|
||||
userId: user.userId,
|
||||
eventType: SecurityEventType.API_KEY_REVOKED,
|
||||
metadata: { keyId: id },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an API key (for STT/TTS services)
|
||||
* This endpoint does NOT require JWT authentication
|
||||
* Validate an API key (for internal services like STT/TTS)
|
||||
*
|
||||
* This endpoint does NOT require JWT authentication since it's called
|
||||
* by services that only have an API key, not a JWT.
|
||||
*
|
||||
* Rate limited to 10 requests/minute per IP to prevent brute force.
|
||||
*/
|
||||
@Post('validate')
|
||||
async validateKey(@Body() dto: ValidateApiKeyDto) {
|
||||
return this.apiKeysService.validateApiKey(dto.apiKey, dto.scope);
|
||||
@UseGuards(ThrottlerGuard)
|
||||
@Throttle({ default: { ttl: 60000, limit: 10 } })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async validateKey(@Body() dto: ValidateApiKeyDto, @Req() req: Request) {
|
||||
const result = await this.apiKeysService.validateApiKey(dto.apiKey, dto.scope);
|
||||
|
||||
const eventType = result.valid
|
||||
? SecurityEventType.API_KEY_VALIDATED
|
||||
: SecurityEventType.API_KEY_VALIDATION_FAILED;
|
||||
|
||||
this.securityEvents.logEventWithRequest(req, {
|
||||
userId: result.valid ? result.userId : undefined,
|
||||
eventType,
|
||||
metadata: {
|
||||
scope: dto.scope,
|
||||
keyPrefix: dto.apiKey?.substring(0, 16) + '...',
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue