openapi: "3.0.3" info: title: SentryAgent.ai — Identity Federation version: 1.0.0 description: | Cross-IdP identity federation endpoints for the SentryAgent.ai AgentIdP platform. The federation subsystem enables this IdP to trust and verify JWT tokens issued by external identity providers (partner IdPs). This allows agents from federated organizations to authenticate without re-registering. **Partner management** (`admin:orgs` scope required): - `POST /federation/trust` — Register a new trusted partner IdP - `GET /federation/partners` — List all registered partners - `GET /federation/partners/:id` — Get a specific partner - `PATCH /federation/partners/:id` — Update a partner - `DELETE /federation/partners/:id` — Remove a partner **Token verification** (any authenticated agent): - `POST /federation/verify` — Verify a federated JWT from a trusted partner servers: - url: http://localhost:3000/api/v1 description: Local development server - url: https://api.sentryagent.ai/v1 description: Production server tags: - name: Federation Partners description: Trusted partner IdP management (admin:orgs scope) - name: Federation Verification description: Cross-IdP token verification (any authenticated agent) components: securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT description: | JWT access token obtained via `POST /token`. Include as `Authorization: Bearer `. schemas: FederationPartnerStatus: type: string enum: - active - suspended - expired description: | Lifecycle status of a federation partner. - `active` — partner is trusted; tokens accepted. - `suspended` — temporarily disabled; tokens rejected. - `expired` — `expires_at` has passed; tokens rejected. example: active FederationPartner: type: object description: A registered trusted federation partner IdP. required: - id - name - issuer - jwks_uri - allowed_organizations - status - created_at - updated_at properties: id: type: string format: uuid description: Immutable system-assigned UUID for this partner. readOnly: true example: "fed-abcd-1234-5678-ef01" name: type: string description: Human-readable partner name. example: "Acme Partner IdP" issuer: type: string format: uri description: Issuer URL — must match the `iss` claim in federated tokens. example: "https://idp.acme-partner.example" jwks_uri: type: string format: uri description: URL of the partner's JWKS endpoint for token signature verification. example: "https://idp.acme-partner.example/.well-known/jwks.json" allowed_organizations: type: array items: type: string description: | Allowlist of organization_id values accepted from this partner. An empty array means all organizations from this partner are accepted. example: ["org-acme-001", "org-acme-002"] status: $ref: '#/components/schemas/FederationPartnerStatus' created_at: type: string format: date-time readOnly: true example: "2026-03-01T08:00:00.000Z" updated_at: type: string format: date-time readOnly: true example: "2026-03-28T11:30:00.000Z" expires_at: type: string format: date-time nullable: true description: Optional expiry timestamp. Null means the partner never expires. example: "2027-01-01T00:00:00.000Z" CreatePartnerRequest: type: object description: Request body for registering a new trusted federation partner. required: - name - issuer - jwks_uri properties: name: type: string description: Human-readable partner name. minLength: 1 maxLength: 256 example: "Acme Partner IdP" issuer: type: string format: uri description: Issuer URL of the external IdP. Must match `iss` in federated tokens. example: "https://idp.acme-partner.example" jwks_uri: type: string format: uri description: | URL of the partner's JWKS endpoint. Must be reachable at registration time — the endpoint is probed before the partner is persisted. example: "https://idp.acme-partner.example/.well-known/jwks.json" allowed_organizations: type: array items: type: string description: Optional allowlist of organization_id values. Defaults to empty (all allowed). example: ["org-acme-001"] expires_at: type: string format: date-time description: Optional ISO 8601 date-time after which the partner will be automatically expired. example: "2027-01-01T00:00:00.000Z" UpdatePartnerRequest: type: object description: | Request body for partially updating a federation partner. All fields are optional; only provided fields are updated. minProperties: 1 properties: name: type: string minLength: 1 maxLength: 256 example: "Acme Partner IdP (Updated)" jwks_uri: type: string format: uri description: Updated JWKS endpoint URL. The JWKS cache will be invalidated on update. example: "https://idp.acme-partner.example/.well-known/jwks.json" allowed_organizations: type: array items: type: string example: ["org-acme-001", "org-acme-003"] status: $ref: '#/components/schemas/FederationPartnerStatus' expires_at: type: string format: date-time nullable: true description: Updated expiry. Pass null to remove the expiry. example: "2028-01-01T00:00:00.000Z" FederationVerifyRequest: type: object description: Request body for verifying a federated token from a trusted partner. required: - token properties: token: type: string description: The JWT token string issued by the partner IdP. example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ2VudC0wMDEiLCJpc3MiOiJodHRwczovL2lkcC5hY21lLXBhcnRuZXIuZXhhbXBsZSIsImV4cCI6MTc0MzE1NDgwMH0.signature" expected_issuer: type: string format: uri description: | Optional issuer hint. When provided, the token's `iss` claim must match exactly. Useful to prevent issuer confusion when multiple partners share JWKS. example: "https://idp.acme-partner.example" FederationVerifyResult: type: object description: Result of a successful federated token verification. required: - valid - issuer - subject - claims properties: valid: type: boolean description: Whether the token is valid (true when verification passed). example: true issuer: type: string format: uri description: The `iss` claim from the verified token. example: "https://idp.acme-partner.example" subject: type: string description: The `sub` claim from the verified token. example: "agent-001-external" organization_id: type: string description: The `organization_id` claim from the token, if present. example: "org-acme-001" claims: type: object description: All claims from the verified token payload. additionalProperties: true example: iss: "https://idp.acme-partner.example" sub: "agent-001-external" aud: "https://api.sentryagent.ai" iat: 1743151200 exp: 1743154800 organization_id: "org-acme-001" ErrorResponse: type: object description: Standard error response envelope. required: - code - message properties: code: type: string example: "FEDERATION_PARTNER_NOT_FOUND" message: type: string example: "Federation partner with the specified ID was not found." details: type: object additionalProperties: true responses: Unauthorized: description: Missing or invalid Bearer token. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "UNAUTHORIZED" message: "A valid Bearer token is required to access this resource." Forbidden: description: Valid token but insufficient permissions. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "FORBIDDEN" message: "You do not have permission to perform this action." NotFound: description: Federation partner not found. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "FEDERATION_PARTNER_NOT_FOUND" message: "Federation partner with the specified ID was not found." InternalServerError: description: Unexpected server error. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "INTERNAL_SERVER_ERROR" message: "An unexpected error occurred. Please try again later." security: - BearerAuth: [] paths: /federation/trust: post: operationId: registerFederationPartner tags: - Federation Partners summary: Register a trusted federation partner description: | Registers a new external IdP as a trusted federation partner. The partner's JWKS endpoint (`jwks_uri`) is probed at registration time to confirm it is reachable. If the endpoint is unreachable, the request is rejected with `422 Unprocessable Entity`. Requires `admin:orgs` scope. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreatePartnerRequest' example: name: "Acme Partner IdP" issuer: "https://idp.acme-partner.example" jwks_uri: "https://idp.acme-partner.example/.well-known/jwks.json" allowed_organizations: - "org-acme-001" expires_at: "2027-01-01T00:00:00.000Z" responses: '201': description: Federation partner registered successfully. content: application/json: schema: $ref: '#/components/schemas/FederationPartner' example: id: "fed-abcd-1234-5678-ef01" name: "Acme Partner IdP" issuer: "https://idp.acme-partner.example" jwks_uri: "https://idp.acme-partner.example/.well-known/jwks.json" allowed_organizations: - "org-acme-001" status: "active" created_at: "2026-04-07T09:00:00.000Z" updated_at: "2026-04-07T09:00:00.000Z" expires_at: "2027-01-01T00:00:00.000Z" '400': description: Validation error in request body. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "VALIDATION_ERROR" message: "Request validation failed." details: field: "jwks_uri" reason: "Must be a valid HTTPS URL." '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '409': description: A partner with the same issuer is already registered. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "FEDERATION_PARTNER_CONFLICT" message: "A federation partner with this issuer is already registered." '422': description: Partner JWKS endpoint is unreachable. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "JWKS_UNREACHABLE" message: "The partner JWKS endpoint is unreachable. Verify the URL and try again." '500': $ref: '#/components/responses/InternalServerError' /federation/partners: get: operationId: listFederationPartners tags: - Federation Partners summary: List all federation partners description: | Returns all registered federation partners. Partners past their `expires_at` are returned with status `expired`. Requires `admin:orgs` scope. responses: '200': description: List of federation partners returned successfully. content: application/json: schema: type: array items: $ref: '#/components/schemas/FederationPartner' example: - id: "fed-abcd-1234-5678-ef01" name: "Acme Partner IdP" issuer: "https://idp.acme-partner.example" jwks_uri: "https://idp.acme-partner.example/.well-known/jwks.json" allowed_organizations: ["org-acme-001"] status: "active" created_at: "2026-03-01T08:00:00.000Z" updated_at: "2026-03-28T11:30:00.000Z" expires_at: "2027-01-01T00:00:00.000Z" '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '500': $ref: '#/components/responses/InternalServerError' /federation/partners/{id}: parameters: - name: id in: path required: true description: UUID of the federation partner. schema: type: string format: uuid example: "fed-abcd-1234-5678-ef01" get: operationId: getFederationPartner tags: - Federation Partners summary: Get a federation partner by ID description: | Returns a single federation partner record by its UUID. Requires `admin:orgs` scope. responses: '200': description: Federation partner returned successfully. content: application/json: schema: $ref: '#/components/schemas/FederationPartner' example: id: "fed-abcd-1234-5678-ef01" name: "Acme Partner IdP" issuer: "https://idp.acme-partner.example" jwks_uri: "https://idp.acme-partner.example/.well-known/jwks.json" allowed_organizations: ["org-acme-001"] status: "active" created_at: "2026-03-01T08:00:00.000Z" updated_at: "2026-03-28T11:30:00.000Z" expires_at: null '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' patch: operationId: updateFederationPartner tags: - Federation Partners summary: Update a federation partner description: | Partially updates a federation partner's configuration. Only provided fields are updated. Changing `jwks_uri` invalidates the cached JWKS for this partner. Requires `admin:orgs` scope. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdatePartnerRequest' example: status: "suspended" allowed_organizations: - "org-acme-001" - "org-acme-003" responses: '200': description: Federation partner updated successfully. content: application/json: schema: $ref: '#/components/schemas/FederationPartner' example: id: "fed-abcd-1234-5678-ef01" name: "Acme Partner IdP" issuer: "https://idp.acme-partner.example" jwks_uri: "https://idp.acme-partner.example/.well-known/jwks.json" allowed_organizations: ["org-acme-001", "org-acme-003"] status: "suspended" created_at: "2026-03-01T08:00:00.000Z" updated_at: "2026-04-07T09:00:00.000Z" expires_at: null '400': description: Validation error in request body. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "VALIDATION_ERROR" message: "Request validation failed." '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' delete: operationId: deleteFederationPartner tags: - Federation Partners summary: Remove a federation partner description: | Permanently removes a federation partner. After removal, tokens issued by the partner's IdP will no longer be accepted. Requires `admin:orgs` scope. responses: '204': description: Federation partner removed successfully. No response body. '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /federation/verify: post: operationId: verifyFederatedToken tags: - Federation Verification summary: Verify a federated JWT token description: | Verifies a JWT token issued by a registered federation partner. The token's `iss` claim is matched against registered active partners. The signature is verified against the partner's JWKS. The partner's `allowed_organizations` filter is applied if non-empty. Returns verification result with claims on success. Returns `422` with verification failure details on invalid tokens. Requires any valid Bearer token (`agents:read` scope is sufficient). requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/FederationVerifyRequest' example: token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ2VudC0wMDEiLCJpc3MiOiJodHRwczovL2lkcC5hY21lLXBhcnRuZXIuZXhhbXBsZSJ9.signature" expected_issuer: "https://idp.acme-partner.example" responses: '200': description: Token verification result returned. content: application/json: schema: $ref: '#/components/schemas/FederationVerifyResult' examples: valid: summary: Token verified successfully value: valid: true issuer: "https://idp.acme-partner.example" subject: "agent-001-external" organization_id: "org-acme-001" claims: iss: "https://idp.acme-partner.example" sub: "agent-001-external" aud: "https://api.sentryagent.ai" iat: 1743151200 exp: 1743154800 invalid: summary: Token invalid or expired value: valid: false issuer: "" subject: "" claims: {} '400': description: Missing or malformed token in request body. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "VALIDATION_ERROR" message: "The 'token' field is required." '401': $ref: '#/components/responses/Unauthorized' '422': description: | Token is well-formed but verification failed (unknown issuer, expired, invalid signature, or organization not in allowlist). content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: unknownIssuer: summary: Unknown issuer value: code: "UNKNOWN_FEDERATION_ISSUER" message: "No active federation partner found for this token issuer." partnerSuspended: summary: Partner is suspended value: code: "FEDERATION_PARTNER_SUSPENDED" message: "The federation partner for this token issuer is suspended." orgNotAllowed: summary: Organization not in allowlist value: code: "FEDERATION_ORG_NOT_ALLOWED" message: "The token's organization_id is not in the partner's allowed_organizations list." '500': $ref: '#/components/responses/InternalServerError'