feat(#86): implement Authentik OIDC integration for federation
Implements federated authentication infrastructure using OIDC: - Add FederatedIdentity model to Prisma schema for identity mapping - Create OIDCService with identity linking and token validation - Add FederationAuthController with 5 endpoints: * POST /auth/initiate - Start federated auth flow * POST /auth/link - Link identity to remote instance * GET /auth/identities - List user's federated identities * DELETE /auth/identities/:id - Revoke identity * POST /auth/validate - Validate federated token - Create comprehensive type definitions for OIDC flows - Add audit logging for security events - Write 24 passing tests (14 service + 10 controller) - Achieve 79% coverage for OIDCService, 100% for controller Notes: - Token validation and auth URL generation are placeholder implementations - Full JWT validation will be added when federation OIDC is actively used - Identity mappings enforce workspace isolation - All endpoints require authentication except /validate Refs #86 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
131
apps/api/src/federation/federation-auth.controller.ts
Normal file
131
apps/api/src/federation/federation-auth.controller.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Federation Auth Controller
|
||||
*
|
||||
* API endpoints for federated OIDC authentication.
|
||||
*/
|
||||
|
||||
import { Controller, Post, Get, Delete, Body, Param, Req, UseGuards, Logger } from "@nestjs/common";
|
||||
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
|
||||
*/
|
||||
@Post("initiate")
|
||||
@UseGuards(AuthGuard)
|
||||
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
|
||||
*/
|
||||
@Post("link")
|
||||
@UseGuards(AuthGuard)
|
||||
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
|
||||
*/
|
||||
@Get("identities")
|
||||
@UseGuards(AuthGuard)
|
||||
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
|
||||
*/
|
||||
@Delete("identities/:instanceId")
|
||||
@UseGuards(AuthGuard)
|
||||
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
|
||||
*/
|
||||
@Post("validate")
|
||||
validateToken(@Body() dto: ValidateFederatedTokenDto): FederatedTokenValidation {
|
||||
this.logger.debug(`Validating federated token from ${dto.instanceId}`);
|
||||
|
||||
return this.oidcService.validateToken(dto.token, dto.instanceId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user