ID Partners Copilot Federation

Architecture

Copilot Studio agents as OpenID Federation entities

How the Entra bridge works and why it has this shape. It describes the code and the Azure deployment as verified end to end in August 2026.

The shape of the answer

The bank's policy enforcement points and PingAuthorize decide on a delegated token: sub is the customer, act.sub is the agent acting for them. For a Copilot Studio agent, act.sub has no Microsoft-signed source. So for each agent we onboard, we create a mirror Entra agent identity under our own blueprint. We can obtain tokens for that identity, because our gateway's managed identity holds the blueprint's only credential.

  1. The Copilot Studio agent's connection proves which agent it is with a per-agent connection key. This is the one hop that depends on a shared secret.
  2. The gateway obtains a real Entra token as that agent's mirror.
  3. The attestation service verifies that token against the tenant's keys and signs a client attestation for the agent.
  4. Each agent is an OpenID Federation leaf under the bridge. PingFederate, the Trust Anchor, learns the agent's attestation key from a trust chain sent with the request, so no per-agent key is ever configured in PingFederate.
  5. PingFederate performs an RFC 8693 token exchange and issues sub = the customer, act.sub = https://<bridge>/agents/<slug>.

Everything after step 1 is cryptographic. If Microsoft starts sending verifiable agent context to external resources, step 1 and the mirror are replaced and nothing below them changes.

Components and what each one holds

flowchart LR
  CS["Copilot Studio agent<br/>(custom connector)"]
  subgraph Azure["Azure Container Apps"]
    GW["gateway-agent<br/>external ingress"]
    BR["bridge<br/>external ingress"]
    AT["attestation-service<br/>internal ingress only"]
  end
  ENTRA[("Microsoft Entra ID")]
  KV[("Key Vault")]
  PF["PingFederate<br/>Trust Anchor"]
  TA["account / payments<br/>task agents"]
  PEP["PEP, AuthZEN,<br/>PingAuthorize, bank API"]

  CS -- "user token, X-Agent-Id, X-Agent-Key" --> GW
  GW -- "managed identity, T1, mirror token" --> ENTRA
  GW -- "user token, mirror token" --> BR
  BR -- "POST /attest (managed identity auth)" --> AT
  AT -. "verify mirror token (JWKS)" .-> ENTRA
  BR -- "sign federation statements" --> KV
  AT -- "sign attestation, PoP" --> KV
  BR -- "RFC 8693 + attestation + PoP + trust chain" --> PF
  BR -- "A2A, delegated token" --> TA
  TA --> PEP
      
ServiceHoldsNever holds
gateway-agentConnection-key fingerprints from the registry. A managed identity federated to our blueprint.Any signing key. It authenticates callers and asks Entra for mirror tokens. It signs nothing and verifies no token.
bridgeThe bridge's own federation key and each agent's fed_entity key. Signs Entity Configurations and subordinate statements.attester and instance keys. It cannot produce an attestation.
attestation-serviceEach agent's attester and instance keys.fed_entity keys. It is reachable only from inside the Container Apps environment, and only by the bridge's managed identity.
PingFederateOne OAuth client record per agent, the Entra subject-token processor, the token exchange mapping.Any agent private key. It verifies with public keys the federation chain vouches for.

Three keys per agent, in two custody scopes

KeySignsCustody
fed_entityThe agent's leaf Entity ConfigurationBridge (Key Vault, access granted per key)
attesterThe client attestationAttestation service
instanceThe per-exchange proof of possession. Its public half is the attestation's cnf.jwk.Attestation service

A compromised bridge can sign a statement about which attester key to trust, but cannot sign an attestation. A compromised attestation service can sign attestations, and its own evidence check is the only thing in its way. The compromise model states what each service can and cannot do alone.

One request, end to end

sequenceDiagram
  autonumber
  actor U as Customer
  participant CS as Copilot Studio agent
  participant GW as gateway-agent
  participant E as Entra ID
  participant BR as bridge
  participant AT as attestation-service
  participant PF as PingFederate
  participant TA as task agent, PEP, PingAuthorize, API

  U->>CS: "list my accounts"
  CS->>GW: POST /forward/tasks-nw/a2a<br/>X-User-Token: user token<br/>X-Agent-Id, X-Agent-Key
  GW->>GW: key id resolves to a registry entry, SHA-256(secret) matches,<br/>X-Agent-Id matches the same entry, capability allowed
  GW->>E: managed identity token, then T1 (blueprint, fmi_path = mirror)
  GW->>E: T1 as client_assertion, returns mirror token (aud = Bridge API)
  GW->>BR: POST /forward/tasks-nw/a2a<br/>Authorization: user token, X-Entra-Agent-Token: mirror token
  BR->>BR: build trust chain: leaf EC, subordinate statement, bridge EC
  BR->>AT: POST /attest with mirror token, trust chain, upstream context<br/>Authorization: bridge managed identity token
  AT->>AT: verify the caller, verify the mirror token (tenant JWKS),<br/>azp resolves to the registry, group gate, RAR ceiling
  AT-->>BR: attestation (attester key) + PoP (instance key)
  BR->>PF: RFC 8693 token exchange<br/>OAuth-Client-Attestation (trust_chain header), OAuth-Client-Attestation-PoP<br/>subject_token = user token, client_id = agent entity id
  PF->>PF: validate subject token (tenant JWKS),<br/>resolve attester key through the chain, verify attestation + PoP
  PF-->>BR: at+jwt: sub = customer, act.sub = agent entity id
  BR->>TA: A2A message/send, Bearer delegated token
  TA-->>BR: result (permit, deny or step-up)
  BR-->>GW: JSON-RPC result + transcript steps
  GW-->>CS: JSON-RPC result
      

Copilot Studio to gateway: which connection is calling

The path names the destination (POST /forward/<capability>/a2a). The headers name the caller.

HeaderWhat it is
X-User-TokenThe customer's Entra access token, passed by the tool from System.User.AccessToken.
X-Agent-IdThe Copilot Studio agent's Entra Agent ID object id. A claim. Nobody signed it.
X-Agent-Keykey_<id>.<secret>, issued at onboarding. The proof.

The gateway looks the key id up in the registry, compares SHA-256(secret) with the stored fingerprint, and requires X-Agent-Id to belong to the same entry. Unknown key id, wrong secret and mismatched claim all return the same 401 body. The agent must also be registered for the capability in the path, otherwise 403. The gateway does not validate the user token. That is PingFederate's job.

Gateway to Entra: the mirror token

The mirror identity is created once, at onboarding. At request time the gateway only obtains a token as it, using the Federated Identity Credential chain Entra documents for autonomous agents:

  1. A managed identity token for api://AzureADTokenExchange.
  2. T1: client_credentials at the tenant token endpoint with client_id = our blueprint, fmi_path = the mirror's client id, and the managed identity token as client_assertion.
  3. Mirror token: client_credentials with client_id = the mirror, T1 as client_assertion, scope = the Bridge API.

The mirror token carries azp = the mirror, xms_par_app_azp = our blueprint, xms_act_fct containing 11 (agent identity) and idtyp=app. The mirror has no credential of its own. The gateway forwards only the user token, the mirror token, traceparent and Content-Type. The agent id and connection key stop at the gateway.

Bridge to attestation service: evidence verified at source

The bridge does not verify the mirror token. It builds the agent's trust chain and sends the raw mirror token to POST /attest, authenticated with its own managed identity token. The attestation service:

  1. verifies the caller's managed identity token and that the caller is on its allow-list;
  2. verifies the mirror token against the tenant JWKS: signature, iss, aud, exp, idtyp=app, azp, xms_par_app_azp = our blueprint, xms_act_fct contains 11;
  3. resolves the verified azp to a registry entry. An unknown azp is a hard 403;
  4. requires the agent to be in the bridge access group and computes its authorisation ceiling from its groups;
  5. checks that the leaf of the trust chain names the same agent;
  6. signs the attestation and a fresh proof of possession.

The OpenID Federation records

flowchart TD
  TA["Trust Anchor<br/>PingFederate"]
  BR["Intermediate<br/>the bridge<br/>publishes /federation/fetch and /federation/list"]
  LF["Leaf<br/>the agent: https://bridge/agents/slug<br/>= the PingFederate client_id"]
  TA -- "vouches for the bridge with jwks only" --> BR
  BR -- "subordinate statement with metadata_policy" --> LF
      

The bridge signs three statements and sends them, in this order, in the attestation's trust_chain JWS header. PingFederate does not need to fetch anything during a request.

  1. Leaf Entity Configuration. iss = sub = https://<bridge>/agents/<slug>, signed by the agent's fed_entity key. Its metadata declares an OAuth client that uses only token exchange with attest_jwt_client_auth, the agent's attester public key, and an agent entity type carrying the platform, the Copilot Studio agent's object id, the mirror, capabilities and the Agent Card URL.
  2. Subordinate statement. iss = the bridge, sub = the agent, signed by the bridge's federation key, with a metadata_policy that pins grant_types and token_endpoint_auth_method. PingFederate enforces the composed policy, so a leaf cannot claim looser terms.
  3. Bridge Entity Configuration. Self-signed, with the Trust Anchor in authority_hints, advertising the fetch and list endpoints.

Absence from PingFederate's static attester file is what sends an agent to federation resolution. PingFederate first looks a client_id up in its static file. The federated agents are deliberately not there, so adding an agent changes the registry and not the authorisation server's key configuration.

The fed_entity key and the attester key do different jobs and are kept in different services on purpose. fed_entity signs the statement naming the attester key. The attester key signs the attestation itself.

The attestation and the token exchange

What the attestation service signs

Client attestation, typ: oauth-client-attestation+jwt, signed with the agent's attester key, with trust_chain in the header:

ClaimValue
iss, subThe agent's entity id
cnf.jwkThe agent's instance public key
workload.entraWhat the attestation service verified: tenant, mirror identity, blueprint, token id, and a SHA-256 of the raw mirror token. verification = entra_token.
workload.upstreamThe Copilot Studio context the gateway passed, labelled verification: connection.
authorization_detailsThe agent's group-derived ceiling, as Rich Authorization Requests

Its lifetime is the shortest of 15 minutes, the mirror token's expiry, and the chain's earliest expiry less five minutes.

Proof of possession, typ: oauth-client-attestation-pop+jwt, signed with the instance key, aud = PingFederate's issuer, a random jti, a 60 second lifetime. It is never cached.

The exchange

POST /as/token.oauth2
OAuth-Client-Attestation: <attestation JWT>
OAuth-Client-Attestation-PoP: <PoP JWT>
traceparent: <...>

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<customer's Entra access token>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&client_id=https://<bridge>/agents/<slug>

No actor_token, scope or audience is sent. No DPoP header is sent: PingFederate's attestation module treats a DPoP header alongside the attestation pair as invalid_client.

What PingFederate does

  • The per-agent client has no client secret and only the token exchange grant. The attestation and PoP are verified in the mapping's issuance criterion. A failure is attestation_validation_failed.
  • The subject token goes through a JWT processor trusting the tenant's v2 issuer and JWKS. This is where the customer is verified. Nothing in the agent path can substitute a different customer.
  • The issued token, typ at+jwt, carries sub = the mapped customer and act = {"sub": "<client_id>"}. It is always one level deep.

How the actor is set. RFC 8693 makes actor_token optional, and its subject-only example is impersonation. Here an explicit PingFederate attribute mapping adds the authenticated client's entity id as act.sub, so the token records delegation. The alternative is to also send the Entra mirror token as actor_token, so PingFederate verifies Microsoft's agent evidence directly. That option is open in the design review.

The bridge then calls the task agent over A2A with the delegated token as bearer. Each task agent performs its own exchange, and the PEP, AuthZEN and PingAuthorize path decides each operation.

Step-up: when the bank wants the human

A payment over threshold, or an account opening that needs a verified mobile driver licence, comes back from the task agent as in flight, not denied. The task agent has raised a CIBA push or a credential presentation request, and returns a pending state with a task id.

  • The bridge waits up to 30 seconds. If the customer approves in time, it sends the same operation again with the approval and returns the finished result.
  • Past 30 seconds it returns the pending state and the task id. Copilot Studio then calls the same connector action with JSON-RPC tasks/get to resume.
  • The design review changed the resume pattern. An agent flow must respond within 100 seconds, so a long poll loop cannot fit in one run. The step-up becomes turn-based: return the task id and resume on the customer's next turn, with idempotency keys on payment submission.