feat(#87): implement cross-instance identity linking for federation

Implements FED-004: Cross-Instance Identity Linking, building on the
foundation from FED-001, FED-002, and FED-003.

New Services:
- IdentityLinkingService: Handles identity verification and mapping
  with signature validation and OIDC token verification
- IdentityResolutionService: Resolves identities between local and
  remote instances with support for bulk operations

New API Endpoints (IdentityLinkingController):
- POST /api/v1/federation/identity/verify - Verify remote identity
- POST /api/v1/federation/identity/resolve - Resolve remote to local user
- POST /api/v1/federation/identity/bulk-resolve - Bulk resolution
- GET /api/v1/federation/identity/me - Get current user's identities
- POST /api/v1/federation/identity/link - Create identity mapping
- PATCH /api/v1/federation/identity/:id - Update mapping
- DELETE /api/v1/federation/identity/:id - Revoke mapping
- GET /api/v1/federation/identity/:id/validate - Validate mapping

Security Features:
- Signature verification using remote instance public keys
- OIDC token validation before creating mappings
- Timestamp validation to prevent replay attacks
- Workspace isolation via authentication guards
- Comprehensive audit logging for all identity operations

Enhancements:
- Added SignatureService.verifyMessage() for remote signature verification
- Added FederationService.getConnectionByRemoteInstanceId()
- Extended FederationAuditService with identity logging methods
- Created comprehensive DTOs with class-validator decorators

Testing:
- 38 new tests (19 service + 7 resolution + 12 controller)
- All 132 federation tests passing
- TypeScript compilation passing with no errors
- High test coverage achieved (>85% requirement exceeded)

Technical Details:
- Leverages existing FederatedIdentity model from FED-003
- Uses RSA SHA-256 signatures for cryptographic verification
- Supports one identity mapping per remote instance per user
- Resolution service optimized for read-heavy operations
- Built following TDD principles (Red-Green-Refactor)

Closes #87

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-03 12:55:37 -06:00
parent fc87494137
commit 70a6bc82e0
15 changed files with 2115 additions and 2 deletions

View File

@@ -0,0 +1,151 @@
/**
* 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 { 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.
*/
@Post("verify")
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);
}
}