Skip to content

Commit 8c4e2f5

Browse files
authored
Merge pull request #258 from favourawaku/staging
Adds four auth orchestration improvements: explicit validation of auth
2 parents d74e3b7 + 4f4086a commit 8c4e2f5

12 files changed

Lines changed: 1252 additions & 11 deletions

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,9 @@ WEBHOOK_MAX_RETRIES=5
4747
WEBHOOK_RETRY_BACKOFF_MS=1000
4848
WEBHOOK_TIMEOUT_MS=10000
4949
WEBHOOK_MAX_CONSECUTIVE_FAILURES=10
50+
51+
# Auth Rate Limiting Configuration
52+
# Maximum number of authentication attempts per IP address per window
53+
AUTH_RATE_LIMIT_MAX=10
54+
# Time window in milliseconds for auth rate limiting (default: 60 seconds)
55+
AUTH_RATE_LIMIT_WINDOW_MS=60000

prisma/schema.prisma

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,35 @@ model WebhookDelivery {
547547
@@index([createdAt])
548548
}
549549

550+
/// Idempotency record for caching authentication and other operation responses
551+
model IdempotencyRecord {
552+
id String @id @default(uuid())
553+
554+
/// Unique idempotency key provided by client
555+
key String @unique
556+
557+
/// HTTP method of the original request
558+
method String
559+
560+
/// Endpoint path of the original request
561+
endpoint String
562+
563+
/// Cached response (serialized JSON)
564+
response Json
565+
566+
/// Response status code
567+
statusCode Int @default(200)
568+
569+
/// TTL - when this record expires
570+
expiresAt DateTime
571+
572+
/// Metadata
573+
createdAt DateTime @default(now())
574+
575+
@@index([expiresAt])
576+
@@index([key, endpoint])
577+
}
578+
550579
/// Transaction lifecycle states
551580
enum TransactionStatus {
552581
PENDING // Transaction created but not yet submitted to network

src/auth/auth-orchestrator.controller.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@ import {
66
HttpStatus,
77
Get,
88
Param,
9+
Headers,
10+
Res,
11+
UseGuards,
912
} from '@nestjs/common';
13+
import { Response } from 'express';
1014
import {
1115
AuthOrchestrator,
1216
AuthenticationRequest,
1317
AuthenticationResult,
18+
AuthenticationRequestWithIdempotency,
1419
} from './auth-orchestrator.service';
1520
import { Public } from './public.decorator';
1621

@@ -27,17 +32,37 @@ export class AuthOrchestratorController {
2732
* 3. Returns existing user + wallet if already exists
2833
*
2934
* All operations are idempotent.
30-
*
31-
* @Public - This endpoint must be public as it's used for initial authentication
32-
* before an API key is available.
35+
* Supports optional Idempotency-Key header for request deduplication.
36+
* Protected by per-IP rate limiting to prevent brute force attacks.
3337
*/
3438
@Post('authenticate')
35-
@Public()
39+
@UseGuards(AuthRateLimitGuard)
3640
@HttpCode(HttpStatus.OK)
3741
async authenticate(
3842
@Body() request: AuthenticationRequest,
39-
): Promise<AuthenticationResult> {
40-
return await this.authOrchestrator.handleAuthentication(request);
43+
@Headers('idempotency-key') idempotencyKey: string | undefined,
44+
@Res() response: Response,
45+
): Promise<void> {
46+
const requestWithIdempotency: AuthenticationRequestWithIdempotency = {
47+
...request,
48+
idempotencyKey,
49+
};
50+
51+
const result = await this.authOrchestrator.handleAuthentication(
52+
requestWithIdempotency,
53+
);
54+
55+
// Extract and remove metadata before sending response
56+
const idempotencyReplayed = (result as any)._idempotencyReplayed ?? false;
57+
const responseBody = { ...result };
58+
delete (responseBody as any)._idempotencyReplayed;
59+
60+
// Set idempotency-replayed header if idempotency key was provided
61+
if (idempotencyKey) {
62+
response.setHeader('Idempotency-Replayed', idempotencyReplayed ? 'true' : 'false');
63+
}
64+
65+
response.json(responseBody);
4166
}
4267

4368
/**

0 commit comments

Comments
 (0)