Files
stack/apps/api/src/federation/identity-linking.controller.ts
Jason Woltje 1390da2e74
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
fix(#290): Secure identity verification endpoint
Added @UseGuards(AuthGuard) and rate limiting (@Throttle) to
/api/v1/federation/identity/verify endpoint. Configured strict
rate limit (10 req/min) to prevent abuse of this previously
public endpoint. Added test to verify guards are applied.

Security improvement: Prevents unauthorized access and rate limit
abuse of identity verification endpoint.

Fixes #290

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 21:36:31 -06:00

156 lines
4.6 KiB
TypeScript

/**
* Identity Linking Controller
*
* API endpoints for cross-instance identity verification and management.
*/
import { Controller, Post, Get, Patch, Delete, Body, Param, UseGuards } from "@nestjs/common";
import { Throttle } from "@nestjs/throttler";
import { AuthGuard } from "../auth/guards/auth.guard";
import { IdentityLinkingService } from "./identity-linking.service";
import { IdentityResolutionService } from "./identity-resolution.service";
import { CurrentUser } from "../auth/decorators/current-user.decorator";
import type {
VerifyIdentityDto,
ResolveIdentityDto,
BulkResolveIdentityDto,
CreateIdentityMappingDto,
UpdateIdentityMappingDto,
} from "./dto/identity-linking.dto";
import type {
IdentityVerificationResponse,
IdentityResolutionResponse,
BulkIdentityResolutionResponse,
IdentityMappingValidation,
} from "./types/identity-linking.types";
import type { FederatedIdentity } from "./types/oidc.types";
/**
* User object from authentication
*/
interface AuthenticatedUser {
id: string;
email: string;
name: string;
}
@Controller("federation/identity")
export class IdentityLinkingController {
constructor(
private readonly identityLinkingService: IdentityLinkingService,
private readonly identityResolutionService: IdentityResolutionService
) {}
/**
* POST /api/v1/federation/identity/verify
*
* Verify a user's identity from a remote instance.
* Validates signature and OIDC token.
* Rate limit: "strict" tier (10 req/min) - public endpoint requiring authentication
*/
@Post("verify")
@UseGuards(AuthGuard)
@Throttle({ strict: { limit: 10, ttl: 60000 } })
async verifyIdentity(@Body() dto: VerifyIdentityDto): Promise<IdentityVerificationResponse> {
return this.identityLinkingService.verifyIdentity(dto);
}
/**
* POST /api/v1/federation/identity/resolve
*
* Resolve a remote user to a local user.
*/
@Post("resolve")
@UseGuards(AuthGuard)
async resolveIdentity(@Body() dto: ResolveIdentityDto): Promise<IdentityResolutionResponse> {
return this.identityResolutionService.resolveIdentity(dto.remoteInstanceId, dto.remoteUserId);
}
/**
* POST /api/v1/federation/identity/bulk-resolve
*
* Bulk resolve multiple remote users to local users.
*/
@Post("bulk-resolve")
@UseGuards(AuthGuard)
async bulkResolveIdentity(
@Body() dto: BulkResolveIdentityDto
): Promise<BulkIdentityResolutionResponse> {
return this.identityResolutionService.bulkResolveIdentities(
dto.remoteInstanceId,
dto.remoteUserIds
);
}
/**
* GET /api/v1/federation/identity/me
*
* Get the current user's federated identities.
*/
@Get("me")
@UseGuards(AuthGuard)
async getCurrentUserIdentities(
@CurrentUser() user: AuthenticatedUser
): Promise<FederatedIdentity[]> {
return this.identityLinkingService.listUserIdentities(user.id);
}
/**
* POST /api/v1/federation/identity/link
*
* Create a new identity mapping for the current user.
*/
@Post("link")
@UseGuards(AuthGuard)
async createIdentityMapping(
@CurrentUser() user: AuthenticatedUser,
@Body() dto: CreateIdentityMappingDto
): Promise<FederatedIdentity> {
return this.identityLinkingService.createIdentityMapping(user.id, dto);
}
/**
* PATCH /api/v1/federation/identity/:remoteInstanceId
*
* Update an existing identity mapping.
*/
@Patch(":remoteInstanceId")
@UseGuards(AuthGuard)
async updateIdentityMapping(
@CurrentUser() user: AuthenticatedUser,
@Param("remoteInstanceId") remoteInstanceId: string,
@Body() dto: UpdateIdentityMappingDto
): Promise<FederatedIdentity> {
return this.identityLinkingService.updateIdentityMapping(user.id, remoteInstanceId, dto);
}
/**
* DELETE /api/v1/federation/identity/:remoteInstanceId
*
* Revoke an identity mapping.
*/
@Delete(":remoteInstanceId")
@UseGuards(AuthGuard)
async revokeIdentityMapping(
@CurrentUser() user: AuthenticatedUser,
@Param("remoteInstanceId") remoteInstanceId: string
): Promise<{ success: boolean }> {
await this.identityLinkingService.revokeIdentityMapping(user.id, remoteInstanceId);
return { success: true };
}
/**
* GET /api/v1/federation/identity/:remoteInstanceId/validate
*
* Validate an identity mapping exists and is valid.
*/
@Get(":remoteInstanceId/validate")
@UseGuards(AuthGuard)
async validateIdentityMapping(
@CurrentUser() user: AuthenticatedUser,
@Param("remoteInstanceId") remoteInstanceId: string
): Promise<IdentityMappingValidation> {
return this.identityLinkingService.validateIdentityMapping(user.id, remoteInstanceId);
}
}