# AGNTCY Federation — Specification **Workstream**: 4 of 6 **Phase**: 3 — Enterprise **Author**: Virtual Architect **Date**: 2026-03-29 --- ## Overview Enable cross-instance agent identity federation using signed JWT assertions. Operators register trusted remote AgentIdP instances as federation partners. When an agent presents a token issued by a trusted partner instance, the local AgentIdP can verify it by fetching and caching the partner's JWKS. This enables multi-organization agent identity interoperability aligned with AGNTCY standards. Federation is opt-in per organization. Only tokens from explicitly registered, trusted partners are accepted. --- ## API Endpoints ### POST /federation/trust Register a new federation trust partner. Requires `admin:orgs` scope. ```yaml POST /federation/trust Authorization: Bearer Content-Type: application/json Request Body: schema: type: object required: [name, issuer, jwksUri] properties: name: type: string minLength: 2 maxLength: 100 description: Human-readable name for this federation partner example: "Contoso AgentIdP" issuer: type: string format: uri description: OIDC issuer URL of the partner instance (must match iss claim in tokens) example: "https://agentidp.contoso.com" jwksUri: type: string format: uri description: URL of the partner's JWKS endpoint example: "https://agentidp.contoso.com/.well-known/jwks.json" allowedOrganizations: type: array items: type: string description: Optional list of organization IDs in the partner instance whose tokens are accepted. Empty means all partner orgs are trusted. example: ["org_contoso_engineering"] expiresAt: type: string format: date-time description: Optional expiry for this trust relationship. If omitted, trust does not expire automatically. Responses: 201 Created: schema: $ref: '#/components/schemas/FederationPartner' example: partnerId: "fed_01HXK7Z9P3FKWABCDEF33333" name: "Contoso AgentIdP" issuer: "https://agentidp.contoso.com" jwksUri: "https://agentidp.contoso.com/.well-known/jwks.json" status: "active" allowedOrganizations: [] trustedSince: "2026-03-29T12:00:00Z" expiresAt: null 400 Bad Request: schema: $ref: '#/components/schemas/ErrorResponse' examples: duplicate_issuer: code: "DUPLICATE_ISSUER" message: "A trust relationship with this issuer already exists" unreachable_jwks: code: "JWKS_UNREACHABLE" message: "Could not fetch JWKS from the provided jwksUri" 401 Unauthorized: schema: $ref: '#/components/schemas/ErrorResponse' 403 Forbidden: schema: $ref: '#/components/schemas/ErrorResponse' ``` --- ### GET /federation/partners List all registered federation partners for the caller's organization. Requires `admin:orgs` scope. ```yaml GET /federation/partners Authorization: Bearer Query Parameters: status: type: string enum: [active, suspended, expired] page: type: integer default: 1 limit: type: integer default: 20 maximum: 100 Responses: 200 OK: schema: type: object properties: data: type: array items: $ref: '#/components/schemas/FederationPartner' total: type: integer page: type: integer limit: type: integer example: data: - partnerId: "fed_01HXK7Z9P3FKWABCDEF33333" name: "Contoso AgentIdP" issuer: "https://agentidp.contoso.com" jwksUri: "https://agentidp.contoso.com/.well-known/jwks.json" status: "active" trustedSince: "2026-03-29T12:00:00Z" expiresAt: null total: 1 page: 1 limit: 20 401 Unauthorized: schema: $ref: '#/components/schemas/ErrorResponse' 403 Forbidden: schema: $ref: '#/components/schemas/ErrorResponse' ``` --- ### DELETE /federation/partners/:partnerId Remove a federation trust relationship. Requires `admin:orgs` scope. ```yaml DELETE /federation/partners/{partnerId} Authorization: Bearer Path Parameters: partnerId: type: string Responses: 204 No Content: {} 401 Unauthorized: schema: $ref: '#/components/schemas/ErrorResponse' 403 Forbidden: schema: $ref: '#/components/schemas/ErrorResponse' 404 Not Found: schema: $ref: '#/components/schemas/ErrorResponse' ``` --- ### POST /federation/verify Verify a token issued by a federated partner AgentIdP instance. The caller presents the token; this endpoint resolves the issuer, fetches (or cache-hits) the partner's JWKS, and verifies the signature and claims. ```yaml POST /federation/verify Authorization: Bearer Content-Type: application/json Request Body: schema: type: object required: [token] properties: token: type: string description: The JWT token issued by the remote AgentIdP instance to verify expectedIssuer: type: string format: uri description: Optional — if provided, verification fails if token issuer does not match expectedOrganizationId: type: string description: Optional — if provided, verification fails if token organization_id does not match Responses: 200 OK: schema: type: object properties: valid: type: boolean claims: type: object description: Decoded JWT claims from the verified token properties: sub: type: string iss: type: string iat: type: integer exp: type: integer agent_id: type: string agent_type: type: string organization_id: type: string capabilities: type: array items: type: string did: type: string partner: type: object description: The federation partner record that vouches for this token properties: partnerId: type: string name: type: string issuer: type: string example: valid: true claims: sub: "agt_contoso_abc123" iss: "https://agentidp.contoso.com" iat: 1743249600 exp: 1743253200 agent_id: "agt_contoso_abc123" agent_type: "classifier" organization_id: "org_contoso_engineering" capabilities: ["text-classification"] did: "did:web:agentidp.contoso.com:agents:agt_contoso_abc123" partner: partnerId: "fed_01HXK7Z9P3FKWABCDEF33333" name: "Contoso AgentIdP" issuer: "https://agentidp.contoso.com" 400 Bad Request: schema: $ref: '#/components/schemas/ErrorResponse' 401 Unauthorized (local token invalid): schema: $ref: '#/components/schemas/ErrorResponse' 422 Unprocessable Entity (token invalid or untrusted issuer): schema: type: object properties: valid: type: boolean example: false reason: type: string enum: - TOKEN_EXPIRED - INVALID_SIGNATURE - UNTRUSTED_ISSUER - JWKS_FETCH_FAILED - ORGANIZATION_NOT_ALLOWED message: type: string example: valid: false reason: "UNTRUSTED_ISSUER" message: "No trust relationship registered for issuer https://unknown.example.com" ``` --- ## Database Schema Changes ### New Table: federation_partners ```sql CREATE TABLE federation_partners ( partner_id VARCHAR(40) PRIMARY KEY, organization_id VARCHAR(40) NOT NULL REFERENCES organizations(organization_id), name VARCHAR(100) NOT NULL, issuer VARCHAR(255) NOT NULL, jwks_uri VARCHAR(255) NOT NULL, allowed_organizations JSONB NOT NULL DEFAULT '[]', status VARCHAR(20) NOT NULL DEFAULT 'active', trusted_since TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ, last_jwks_fetch TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT federation_partners_status_check CHECK (status IN ('active', 'suspended', 'expired')), UNIQUE (organization_id, issuer) ); CREATE INDEX idx_federation_partners_org_id ON federation_partners(organization_id); CREATE INDEX idx_federation_partners_issuer ON federation_partners(issuer); CREATE INDEX idx_federation_partners_status ON federation_partners(status); ``` ### Redis: JWKS Cache Partner JWKS documents are cached in Redis with a TTL: ``` Key: federation:jwks: Value: JSON string of the JWKS document TTL: 1 hour (configurable via FEDERATION_JWKS_CACHE_TTL_SECONDS) ``` --- ## Configuration | Environment Variable | Description | Default | |---------------------|-------------|---------| | `FEDERATION_ENABLED` | Enable federation endpoints | `true` | | `FEDERATION_JWKS_CACHE_TTL_SECONDS` | Redis TTL for cached partner JWKS | `3600` | | `FEDERATION_JWKS_FETCH_TIMEOUT_MS` | HTTP timeout for fetching partner JWKS | `5000` | | `FEDERATION_MAX_PARTNERS_PER_ORG` | Max federation partners per organization | `50` | --- ## Dependencies No new npm packages. Federation uses `jsonwebtoken` (already present) for JWT verification and the existing HTTP client for JWKS fetches. --- ## Security Considerations - Only tokens from explicitly registered, active federation partners are accepted in `POST /federation/verify` - JWKS are cached to prevent JWKS endpoint hammering; cache is invalidated when a partner is updated - Token signature verification uses the partner's JWKS; `alg: none` is always rejected - `allowedOrganizations` field enables fine-grained trust: a partner can be trusted but only for tokens from specific organizations within that partner - Expired federation partners (`expiresAt` in the past) are automatically treated as status `expired` — their tokens are rejected - `POST /federation/verify` does not grant any local permissions — it is a verification-only endpoint. Callers must make their own access control decisions based on the returned claims. - Clock skew tolerance: `exp` claim verification allows 30 seconds of clock skew (standard JWT practice) --- ## Acceptance Criteria - [ ] `POST /federation/trust` registers a partner and fetches JWKS; returns 400 if JWKS unreachable - [ ] `POST /federation/verify` returns `valid: true` for a correctly signed token from a trusted partner - [ ] `POST /federation/verify` returns `valid: false` with `reason: UNTRUSTED_ISSUER` for unknown issuers - [ ] `POST /federation/verify` returns `valid: false` with `reason: TOKEN_EXPIRED` for expired tokens - [ ] Expired trust relationships (past `expiresAt`) are rejected automatically - [ ] JWKS cache hit is used on second verification request for same issuer (Redis key present) - [ ] TypeScript strict, zero `any`, >80% test coverage on FederationService