Reference
Terms, decisions, risks and standards
The words agent, mirror, entity and instance mean different things in this design. The code cites its decisions by id. This page defines both.
Identities in this design
| Term | Meaning here | Identifier | Established by |
|---|---|---|---|
| Copilot Studio agent | The Microsoft-managed agent a maker configures | Entra Agent ID object id, sent as X-Agent-Id | Connection-key check at the gateway |
| Mirror agent identity | Our agent identity under our blueprint, one per Copilot Studio agent | App id, seen as azp in its tokens | Entra token verified by the attestation service |
| Federation entity | The stable external OAuth client for that configured agent | https://<bridge>/agents/<slug> | OpenID Federation statements signed by the bridge |
| PoP key | One per-agent Key Vault key, used to sign a fresh proof of possession on each exchange | Public key in the attestation's cnf | Signature by that Key Vault key |
| Attestation instance id | A random UUID the attestation service generates for each attestation | workload.instance_id | Our attestation service only. It is not a Microsoft identifier. |
| Copilot Studio run | One execution of the agent | Conversation id, self-asserted | Not verified |
Decision register
The code cites decisions by id. This register was rebuilt from the code, the comments and the commit history in October 2026. Each row says what the code does.
| Id | Decision | What the code does |
|---|---|---|
| D1 | Per-agent connection key | The caller sends X-Agent-Id (the claim) and X-Agent-Key (the proof). The gateway authenticates the Copilot Studio connection with this pair. |
| D2 | Gateway header allow-list | Only the user token, the mirror token, traceparent and Content-Type go to the bridge. An inbound mirror token header is dropped and its presence logged. |
| D5 | Mock and real modes never mix | Mock-tenant tokens are labelled mock, never entra_token. Real-mode verifiers refuse mock material by name before checking the signature. |
| D8 | Full check list on the raw mirror token | Signature against the tenant JWKS, issuer, audience, expiry, idtyp=app, azp present, parent blueprint = ours, agent identity facet present. |
| D9 | Authentication failures are indistinguishable | Unknown key id, unknown agent id, wrong secret and a mismatch all return the same 401 body. Enumeration learns nothing. |
| D10 | Registry changes only through review | Every service loads one versioned registry. It changes by commit, never at runtime. Uniqueness of slug, agent id, mirror and key id is enforced at load. |
| D11 | Missing key material is never papered over | A missing key disables that role for that agent. The services never fall back to an ephemeral key. |
| D12 | Unknown agent: deny in real mode | A verified azp that is not registered gets 403 unregistered_agent. |
| D13 | Audit link to the evidence | The attestation carries the mirror token's id and a SHA-256 of the raw token. |
| D14 | Fresh PoP per exchange, one challenge retry | A new PoP for every exchange, never cached. If PingFederate asks for a challenge, the bridge fetches one and retries once. |
| D15 | One key per role | Three EC P-256 pairs per agent, never shared across roles or agents. |
| D21 | Metadata policy on the bridge's subordinate statement | The bridge's statement about each agent pins the grant type to token exchange and the client authentication method to attestation. PingFederate enforces it, failing closed. |
| D23 | Federation list endpoint open in the demo | The list endpoint is unauthenticated. The production profile authenticates it. |
| D25 | Restrict the Entra token mapping to registered clients | Applied in two phases: create the clients, then restrict the access token manager to them. |
| D26 | Attested ceiling not enforced at the exchange | The bridge does not send authorization_details by default, so the issued token's is empty. A switch turns it on. |
| D27 | Key custody precedence | Key Vault first, then an environment variable, then a local PEM. Local PEMs for development, Key Vault from staging up. |
| D28 | No plaintext secrets in the registry | The registry holds a key id and a SHA-256 of the connection secret, plus an optional next fingerprint for rotation. A unit test lints the registry. |
| D29 | Evidence verified at source | The attestation service verifies the raw mirror token itself. The bridge never verifies it and never summarises it. |
| D30 | One door per capability | The path names the destination, the headers name the caller. One shared connector, many doors. |
| D32 | Attestation service split from the bridge | Signing moved out of the bridge so a bridge compromise alone cannot produce an attestation. Each service loads only its key roles. |
| D33 | Caching | The gateway caches the mirror token until five minutes before expiry. The attestation is cached on agent, mirror-token hash and chain hash. Leaf and subordinate statements are never cached. |
| D34 | Observability | Failed authentication is rate-limited with an alert log. W3C traceparent is propagated from the gateway to the task agent. Refusals carry claim names and safe values, never the token. |
Gates
| Id | What |
|---|---|
| G1 | PingFederate's attestation module runs in attestation + PoP mode. The bridge sends the attestation pair and no DPoP header, because sending both is invalid_client. |
| G2 | PingFederate, the Trust Anchor, vouches for the bridge with jwks only. This is why the metadata policy is on the bridge's statement. |
| M2 | A switch makes the bridge send the attested ceiling as authorization_details, so PingFederate enforces requested ⊆ attested. Off by default. |
Named risks
| Id | Risk |
|---|---|
| R-ASSERT | Microsoft cannot assert which Copilot Studio agent is calling. The agent to mirror binding proves only that the caller holds that agent's connection secret, so it is labelled connection, below entra_token. |
| R-PAIR | No check that this user and this agent belong together. Any registered agent holding a valid subject token can exchange it. |
| R-BEARER | Issued access tokens are bearer. Client authentication is proof of possession. The token is not DPoP-bound. |
| R-NAME | naming_constraints is not implemented in PingFederate's validator, so the bridge's /agents/* namespace is enforced by the bridge. |
| R-ENUM | The federation list endpoint is open in the demo. |
Standards the design uses
OAuth 2.0 Token Exchange
RFC 8693. The customer's token is the subject. The issued token records delegation in act.
OpenID Federation 1.0
Entity Configurations, subordinate statements, metadata policy and a trust chain from the agent to the Trust Anchor.
OAuth attestation-based client authentication
A client attestation and a per-request proof of possession, sent in the OAuth-Client-Attestation headers.
Rich Authorization Requests
RFC 9396. The agent's authority ceiling is expressed as authorization_details.
AuthZEN Authorization API
The gateway enforcement point asks PingAuthorize for a decision on each operation.
CIBA and A2A
Client-Initiated Backchannel Authentication for the step-up push. The Agent2Agent protocol between the bridge and the task agents.