Files
stack/apps/api/src/federation/federation-auth.controller.ts
Jason Woltje 72c64d2eeb
All checks were successful
ci/woodpecker/push/orchestrator Pipeline was successful
ci/woodpecker/push/api Pipeline was successful
ci/woodpecker/push/web Pipeline was successful
fix(api): add global /api prefix to resolve frontend route mismatch (#507)
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-02-26 01:13:48 +00:00

144 lines
4.3 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("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);
}
}