Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Fixed CI typecheck failures: - Added missing AgentLifecycleService dependency to AgentsController test mocks - Made validateToken method async to match service return type - Fixed formatting in federation.module.ts All affected tests pass. Typecheck now succeeds. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
144 lines
4.4 KiB
TypeScript
144 lines
4.4 KiB
TypeScript
/**
|
|
* Federation Auth Controller
|
|
*
|
|
* API endpoints for federated OIDC authentication.
|
|
* Issue #272: Rate limiting applied to prevent DoS attacks
|
|
*/
|
|
|
|
import { Controller, Post, Get, Delete, Body, Param, Req, UseGuards, Logger } from "@nestjs/common";
|
|
import { Throttle } from "@nestjs/throttler";
|
|
import { OIDCService } from "./oidc.service";
|
|
import { FederationAuditService } from "./audit.service";
|
|
import { AuthGuard } from "../auth/guards/auth.guard";
|
|
import type { AuthenticatedRequest } from "../common/types/user.types";
|
|
import type { FederatedIdentity, FederatedTokenValidation } from "./types/oidc.types";
|
|
import {
|
|
InitiateFederatedAuthDto,
|
|
LinkFederatedIdentityDto,
|
|
ValidateFederatedTokenDto,
|
|
} from "./dto/federated-auth.dto";
|
|
|
|
@Controller("api/v1/federation/auth")
|
|
export class FederationAuthController {
|
|
private readonly logger = new Logger(FederationAuthController.name);
|
|
|
|
constructor(
|
|
private readonly oidcService: OIDCService,
|
|
private readonly auditService: FederationAuditService
|
|
) {}
|
|
|
|
/**
|
|
* Initiate federated authentication flow
|
|
* Returns authorization URL to redirect user to
|
|
* Rate limit: "medium" tier (20 req/min) - authenticated endpoint
|
|
*/
|
|
@Post("initiate")
|
|
@UseGuards(AuthGuard)
|
|
@Throttle({ medium: { limit: 20, ttl: 60000 } })
|
|
initiateAuth(
|
|
@Req() req: AuthenticatedRequest,
|
|
@Body() dto: InitiateFederatedAuthDto
|
|
): { authUrl: string; state: string } {
|
|
if (!req.user) {
|
|
throw new Error("User not authenticated");
|
|
}
|
|
|
|
this.logger.log(`User ${req.user.id} initiating federated auth with ${dto.remoteInstanceId}`);
|
|
|
|
const authUrl = this.oidcService.generateAuthUrl(dto.remoteInstanceId, dto.redirectUrl);
|
|
|
|
// Audit log
|
|
this.auditService.logFederatedAuthInitiation(req.user.id, dto.remoteInstanceId);
|
|
|
|
return {
|
|
authUrl,
|
|
state: dto.remoteInstanceId,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Link federated identity to local user
|
|
* Rate limit: "medium" tier (20 req/min) - authenticated endpoint
|
|
*/
|
|
@Post("link")
|
|
@UseGuards(AuthGuard)
|
|
@Throttle({ medium: { limit: 20, ttl: 60000 } })
|
|
async linkIdentity(
|
|
@Req() req: AuthenticatedRequest,
|
|
@Body() dto: LinkFederatedIdentityDto
|
|
): Promise<FederatedIdentity> {
|
|
if (!req.user) {
|
|
throw new Error("User not authenticated");
|
|
}
|
|
|
|
this.logger.log(`User ${req.user.id} linking federated identity with ${dto.remoteInstanceId}`);
|
|
|
|
const identity = await this.oidcService.linkFederatedIdentity(
|
|
req.user.id,
|
|
dto.remoteUserId,
|
|
dto.remoteInstanceId,
|
|
dto.oidcSubject,
|
|
dto.email,
|
|
dto.metadata
|
|
);
|
|
|
|
// Audit log
|
|
this.auditService.logFederatedIdentityLinked(req.user.id, dto.remoteInstanceId);
|
|
|
|
return identity;
|
|
}
|
|
|
|
/**
|
|
* Get user's federated identities
|
|
* Rate limit: "long" tier (200 req/hour) - read-only endpoint
|
|
*/
|
|
@Get("identities")
|
|
@UseGuards(AuthGuard)
|
|
@Throttle({ long: { limit: 200, ttl: 3600000 } })
|
|
async getIdentities(@Req() req: AuthenticatedRequest): Promise<FederatedIdentity[]> {
|
|
if (!req.user) {
|
|
throw new Error("User not authenticated");
|
|
}
|
|
|
|
return this.oidcService.getUserFederatedIdentities(req.user.id);
|
|
}
|
|
|
|
/**
|
|
* Revoke a federated identity
|
|
* Rate limit: "medium" tier (20 req/min) - authenticated endpoint
|
|
*/
|
|
@Delete("identities/:instanceId")
|
|
@UseGuards(AuthGuard)
|
|
@Throttle({ medium: { limit: 20, ttl: 60000 } })
|
|
async revokeIdentity(
|
|
@Req() req: AuthenticatedRequest,
|
|
@Param("instanceId") instanceId: string
|
|
): Promise<{ success: boolean }> {
|
|
if (!req.user) {
|
|
throw new Error("User not authenticated");
|
|
}
|
|
|
|
this.logger.log(`User ${req.user.id} revoking federated identity with ${instanceId}`);
|
|
|
|
await this.oidcService.revokeFederatedIdentity(req.user.id, instanceId);
|
|
|
|
// Audit log
|
|
this.auditService.logFederatedIdentityRevoked(req.user.id, instanceId);
|
|
|
|
return { success: true };
|
|
}
|
|
|
|
/**
|
|
* Validate a federated token
|
|
* Public endpoint (no auth required) - used by federated instances
|
|
* Rate limit: "short" tier (3 req/sec) - CRITICAL DoS protection (Issue #272)
|
|
*/
|
|
@Post("validate")
|
|
@Throttle({ short: { limit: 3, ttl: 1000 } })
|
|
async validateToken(@Body() dto: ValidateFederatedTokenDto): Promise<FederatedTokenValidation> {
|
|
this.logger.debug(`Validating federated token from ${dto.instanceId}`);
|
|
|
|
return this.oidcService.validateToken(dto.token, dto.instanceId);
|
|
}
|
|
}
|