feat: add detailed request logging and error tracing to API key and JWT guards
Build and Push Multi-Platform Images / build-and-push (push) Successful in 29s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 29s
This commit is contained in:
@@ -1,19 +1,25 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
|
||||||
import { ApiKeysService } from './api-keys.service';
|
import { ApiKeysService } from './api-keys.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApiKeyGuard implements CanActivate {
|
export class ApiKeyGuard implements CanActivate {
|
||||||
|
private readonly logger = new Logger(ApiKeyGuard.name);
|
||||||
|
|
||||||
constructor(private readonly apiKeysService: ApiKeysService) {}
|
constructor(private readonly apiKeysService: ApiKeysService) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const method: string = request.method;
|
||||||
|
const url: string = request.url;
|
||||||
|
|
||||||
// Check header (X-API-Key)
|
// Check header (X-API-Key)
|
||||||
let apiKey = request.headers['x-api-key'] || request.headers['X-API-Key'];
|
let apiKey = request.headers['x-api-key'] || request.headers['X-API-Key'];
|
||||||
|
let source = 'X-API-Key header';
|
||||||
|
|
||||||
// Fallback to query parameter (apiKey)
|
// Fallback to query parameter (apiKey)
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
apiKey = request.query['apiKey'];
|
apiKey = request.query['apiKey'];
|
||||||
|
if (apiKey) source = 'apiKey query param';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to Authorization: Bearer (used by SSE clients that can't set X-API-Key)
|
// Fallback to Authorization: Bearer (used by SSE clients that can't set X-API-Key)
|
||||||
@@ -21,24 +27,28 @@ export class ApiKeyGuard implements CanActivate {
|
|||||||
const auth: string | undefined = request.headers['authorization'];
|
const auth: string | undefined = request.headers['authorization'];
|
||||||
if (auth?.startsWith('Bearer ')) {
|
if (auth?.startsWith('Bearer ')) {
|
||||||
apiKey = auth.slice(7);
|
apiKey = auth.slice(7);
|
||||||
|
source = 'Authorization: Bearer';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`[${method} ${url}] key source: ${apiKey ? source : 'NONE'} | ` +
|
||||||
|
`headers: ${JSON.stringify(Object.keys(request.headers))} | ` +
|
||||||
|
`key prefix: ${apiKey ? String(apiKey).slice(0, 8) + '…' : 'n/a'}`,
|
||||||
|
);
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
|
this.logger.warn(`[${method} ${url}] rejected – no API key found`);
|
||||||
throw new UnauthorizedException('API Key missing');
|
throw new UnauthorizedException('API Key missing');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyEntry = await this.apiKeysService.validateKey(apiKey as string);
|
const keyEntry = await this.apiKeysService.validateKey(apiKey as string);
|
||||||
|
this.logger.debug(`[${method} ${url}] accepted – key "${keyEntry.name}" (id=${keyEntry.id})`);
|
||||||
// Attach metadata to request if needed later
|
request.apiKeyMetadata = { id: keyEntry.id, name: keyEntry.name };
|
||||||
request.apiKeyMetadata = {
|
|
||||||
id: keyEntry.id,
|
|
||||||
name: keyEntry.name,
|
|
||||||
};
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
this.logger.warn(`[${method} ${url}] rejected – validation failed: ${err.message}`);
|
||||||
throw new UnauthorizedException(err.message || 'Invalid API Key');
|
throw new UnauthorizedException(err.message || 'Invalid API Key');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,31 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common';
|
||||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||||
import { ApiKeyGuard } from './api-key.guard';
|
import { ApiKeyGuard } from './api-key.guard';
|
||||||
import { lastValueFrom, isObservable } from 'rxjs';
|
import { lastValueFrom, isObservable } from 'rxjs';
|
||||||
|
|
||||||
/**
|
|
||||||
* Combined guard that accepts either a valid JWT Bearer token
|
|
||||||
* or a valid API key (X-API-Key header / apiKey query param).
|
|
||||||
* Tries JWT first, falls back to API key.
|
|
||||||
*/
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtOrApiKeyGuard implements CanActivate {
|
export class JwtOrApiKeyGuard implements CanActivate {
|
||||||
|
private readonly logger = new Logger(JwtOrApiKeyGuard.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly jwtGuard: JwtAuthGuard,
|
private readonly jwtGuard: JwtAuthGuard,
|
||||||
private readonly apiKeyGuard: ApiKeyGuard,
|
private readonly apiKeyGuard: ApiKeyGuard,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const req = context.switchToHttp().getRequest();
|
||||||
|
const tag = `[${req.method} ${req.url}]`;
|
||||||
|
|
||||||
// Try JWT first
|
// Try JWT first
|
||||||
try {
|
try {
|
||||||
const result = this.jwtGuard.canActivate(context);
|
const result = this.jwtGuard.canActivate(context);
|
||||||
const jwtOk = isObservable(result) ? await lastValueFrom(result) : await result;
|
const jwtOk = isObservable(result) ? await lastValueFrom(result) : await result;
|
||||||
if (jwtOk) return true;
|
if (jwtOk) {
|
||||||
} catch {
|
this.logger.debug(`${tag} authenticated via JWT`);
|
||||||
// JWT failed, try API key
|
return true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.debug(`${tag} JWT failed (${err.message}), trying API key…`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to API key
|
// Fall back to API key
|
||||||
|
|||||||
Reference in New Issue
Block a user