<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anton Staykov</title>
    <description>The latest articles on DEV Community by Anton Staykov (@astaykov).</description>
    <link>https://dev.to/astaykov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3827606%2F3cee6030-a0e4-4b4f-9194-400f671a4957.jpg</url>
      <title>DEV Community: Anton Staykov</title>
      <link>https://dev.to/astaykov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/astaykov"/>
    <language>en</language>
    <item>
      <title>Encryption in transit is not end-to-end encryption: block unsanctioned GenAI access</title>
      <dc:creator>Anton Staykov</dc:creator>
      <pubDate>Tue, 22 Sep 2026 10:33:00 +0000</pubDate>
      <link>https://dev.to/astaykov/encryption-in-transit-is-not-end-to-end-encryption-block-unsanctioned-genai-access-5a3j</link>
      <guid>https://dev.to/astaykov/encryption-in-transit-is-not-end-to-end-encryption-block-unsanctioned-genai-access-5a3j</guid>
      <description>&lt;h2&gt;
  
  
  Encryption in transit is not end-to-end encryption
&lt;/h2&gt;

&lt;p&gt;The distinction is simple and it matters more in GenAI than in most other areas of technology. TLS protects a connection while data is moving across a network segment. It does not mean the application data is protected from every intermediary that handles the request. A reverse proxy, gateway, WAF, or API layer can terminate TLS, inspect the payload, and then forward it onward. That is still encryption in transit. It is not end-to-end encryption.&lt;/p&gt;

&lt;p&gt;This is the real security question behind the AI risk conversation. If an employee pastes a secret into a third-party AI assistant, the data leaves the trust boundary of the organization. At that point, the provider, the proxy, and any downstream service can observe, store, or process it. The issue is not whether the connection was protected while it crossed the public Internet. The issue is whether the organization still controls the data lifecycle.&lt;/p&gt;

&lt;p&gt;NIST describes TLS as protecting data during electronic dissemination across the Internet in the classic sense, and that is accurate. But it is not the same as saying the data remains protected from every system that participates in the request path. &lt;a href="https://csrc.nist.gov/pubs/sp/800-52/r2/final" rel="noopener noreferrer"&gt;NIST SP 800-52 Revision 2&lt;/a&gt; is careful about transport security, not end-to-end application protection.&lt;/p&gt;

&lt;p&gt;There is a quiet assumption baked into many security conversations, and it usually goes unspoken: if the data is encrypted on the wire, the problem is solved. It is not. The network path can be encrypted on every hop while the gateway still sees the plaintext. That is the core distinction.&lt;/p&gt;

&lt;p&gt;The common gateway topology looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant Client as Browser/App
    participant Gateway
    participant Backend
    Client-&amp;gt;&amp;gt;Gateway: TLS connection 1
    Note over Gateway: TLS terminates here&amp;lt;br/&amp;gt;plaintext exists here
    Gateway-&amp;gt;&amp;gt;Backend: TLS connection 2&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;That diagram has two separate cryptographic relationships. The browser is not cryptographically protected all the way to the backend service. It is protected to the gateway, and then the gateway creates a second protected connection to the backend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TLS connection 1:
Browser/App &amp;lt;=================&amp;gt; Gateway

TLS connection 2:
Gateway &amp;lt;=================&amp;gt; Backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a valid transport-security architecture. It is not an end-to-end content-protection architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  True end-to-end encryption protects the payload from the gateway
&lt;/h2&gt;

&lt;p&gt;End-to-end encryption changes the location of the cryptographic boundary. The gateway can still route the request, enforce some policy, and observe metadata. It cannot decrypt the protected application payload.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant Client as Browser/App
    participant Gateway
    participant Backend as Backend Service
    Client-&amp;gt;&amp;gt;Client: encrypt(payload, backend key)
    Client-&amp;gt;&amp;gt;Gateway: TLS plus encrypted payload
    Note over Gateway: Gateway transports ciphertext&amp;lt;br/&amp;gt;Gateway cannot decrypt
    Gateway-&amp;gt;&amp;gt;Backend: TLS plus encrypted payload
    Backend-&amp;gt;&amp;gt;Backend: decrypt(payload)&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;In that model, the application content is protected before it reaches the gateway. The gateway sees an HTTP request, but the sensitive body is opaque unless it has the backend-controlled decryption material.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BASE64_ENCRYPTED_BLOB..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gateway might see routing data and headers like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /api/messages
Host: service.example
Authorization: ...
Content-Type: application/octet-stream

8gh29HFn4k...     &amp;lt;-- opaque encrypted content
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That distinction is not academic. It determines whether the gateway is a reader of the data or a carrier of ciphertext.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Architecture&lt;/th&gt;
&lt;th&gt;Data encrypted on wire?&lt;/th&gt;
&lt;th&gt;Gateway sees plaintext?&lt;/th&gt;
&lt;th&gt;E2E encrypted?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Client -&amp;gt; TLS gateway -&amp;gt; HTTP backend&lt;/td&gt;
&lt;td&gt;Partially&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client -&amp;gt; TLS gateway -&amp;gt; TLS backend&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client encrypts payload -&amp;gt; gateway -&amp;gt; backend decrypts&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The gateway is the control plane, not just a door
&lt;/h2&gt;

&lt;p&gt;This is where the real security story starts. A gateway is not merely a convenience layer between users and services. It is often the central choke point for policy enforcement, authentication, logging, routing, and transformation. In enterprise architecture, that makes it a high-value concentration point for sensitive data and a privileged path to the Internet.&lt;/p&gt;

&lt;p&gt;The design pattern is common across Azure and enterprise stacks. An application gateway, API gateway, or reverse proxy does not just pass traffic. It terminates connections, reads headers, inspects payloads, enforces policies, and often writes transaction metadata to logging, monitoring, and analytics systems. &lt;a href="https://learn.microsoft.com/en-us/azure/application-gateway/application-gateway-tcp-tls-overview" rel="noopener noreferrer"&gt;Azure Application Gateway TCP/TLS proxy overview&lt;/a&gt; describes exactly this behavior: the gateway acts as a reverse proxy and can proxy TCP and TLS traffic as part of the service architecture. In API management terms, the platform sits in front of backend APIs and shapes how traffic moves and is governed. &lt;a href="https://learn.microsoft.com/en-us/azure/api-management/api-management-key-concepts" rel="noopener noreferrer"&gt;What is Azure API Management?&lt;/a&gt; describes the gateway as an operational control layer, not just a network convenience.&lt;/p&gt;

&lt;p&gt;That matters because gateway functionality is never isolated from data exposure. The same component that handles TLS, routing, and policy enforcement also sees the application content unless the design explicitly encrypts beyond that boundary. If the gateway is visible to the enterprise as a single control point, then the enterprise also has to treat it as a single blast-radius concentration point.&lt;/p&gt;

&lt;p&gt;This is the part many organizations miss. Endpoint controls are designed for the client devices, not for the central trust boundary that sits between the organization and the Internet. The endpoint can be managed, patched, and monitored. The gateway is different. It is a shared service used by many applications and many users. It often sits near the edge of the trust boundary, and it often has deep visibility into the same data that the browser or mobile app sends.&lt;/p&gt;

&lt;p&gt;That makes the gateway a strategic security object. It is both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a concentration point for highly valuable company information, because it sees data from many users, workloads, and applications, and&lt;/li&gt;
&lt;li&gt;a gateway to the Internet, because it provides the path that traffic uses to leave the enterprise boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you treat the gateway as purely an infrastructure detail, you miss the real architectural risk. You are treating a privileged control plane as if it were just a traffic relay.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trust boundary is the thing that matters
&lt;/h2&gt;

&lt;p&gt;The key concept here is the trust boundary. A prompt sent to a public AI assistant crosses the enterprise trust boundary whether or not the network path is encrypted. In many cases, the user is not just calling a service. They are handing their data to a third party and the provider decides what it does with the data next. The organization may not control the log retention, model training settings, contract terms, or retention policy.&lt;/p&gt;

&lt;p&gt;The same architectural reality appears in an internal enterprise API gateway. If a backend service has to see customer data to do its job, then the gateway is an important intermediary but not the authoritative security boundary. The real security question is where the data becomes visible and who can access it.&lt;/p&gt;

&lt;p&gt;This is exactly why the phrase "encrypted in transit" is not enough. It says the data is protected while traveling between endpoints. It does not say the data is protected from the endpoints themselves. A TLS connection between browser and gateway and then between gateway and backend can be secure and still leave the gateway in the clear. The gateway can observe the plaintext, and that is enough to change the risk model.&lt;/p&gt;

&lt;p&gt;The honest answer is that you do not need a single broken protocol to create a dangerous exposure pattern. You need a better boundary model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the GenAI risk is different
&lt;/h2&gt;

&lt;p&gt;The GenAI category widens that problem because it combines two factors that are easy to ignore when taken separately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;employees are under pressure to move quickly and show productivity, and&lt;/li&gt;
&lt;li&gt;the AI assistant experience is designed to feel like a normal consumer product, not a corporate system with controls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination is toxic. People paste code, customer data, pricing files, internal proposals, and architecture notes into tools that are not part of the approved enterprise trust domain. Sometimes it is a free tier. Sometimes it is a personal subscription. Sometimes it is because the company license is exhausted or the app is simply more convenient. Sometimes it is because the employee is trying to solve a real business problem under pressure and there is no policy or review path.&lt;/p&gt;

&lt;p&gt;This problem is not theoretical. The recent AI supply-chain and exposure stories show the broader pattern: the path to an AI model is no longer just an application call. It is a software supply chain, a routing layer, a credential boundary, and sometimes a logging sink. The article about the LiteLLM compromise describes a malicious package supply-chain story in which compromised open-source tooling resulted in secrets theft and credential exposure from CI/CD and runtime environments. The investigation is presented as a real-world supply-chain incident, with the article citing forensic research from Snyk, Trend Micro, and Cycode and describing the malicious package behavior in detail. &lt;a href="https://www.infostealers.com/article/largest-ai-supply-chain-breach-of-2026-litellm-hack-impacts-thousands-of-global-enterprises-claim-your-ethical-disclosure/" rel="noopener noreferrer"&gt;InfoStealers on the LiteLLM incident&lt;/a&gt; and the linked Snyk, Trend Micro, and Cycode coverage point to the same risk class: if a tool sits in the path to the model, it becomes part of the exposure surface.&lt;/p&gt;

&lt;p&gt;The Pandaily story about a reported 6 TB LLM router log data exposure should be treated carefully. It is not the same as a definitive forensic closure on every claim, but it reinforces the same basic truth: AI router layers, model gateways, and data processing intermediaries are becoming valuable operational targets. &lt;a href="https://pandaily.com/china-llm-router-logs-6tb-credential-leak-researcher-claim" rel="noopener noreferrer"&gt;Pandaily claim on LLM router logs&lt;/a&gt; is a useful cautionary example even if the final attribution and scope remain contested.&lt;/p&gt;

&lt;p&gt;The broader point is not that every AI service is malicious. The point is that once a prompt, file, or API key crosses into an unvetted service, the organization no longer controls the trust boundary. The data becomes part of a new processing environment with a different contract, a different logging model, and a different risk posture.&lt;/p&gt;

&lt;p&gt;This is why the enterprise question is not "does this service use TLS?" It is "what is the data-handling contract and where is the trust boundary?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Strong AI governance already has an identity plane
&lt;/h2&gt;

&lt;p&gt;The governance answer is not only "block the app." It is also "move the control plane to the identity layer." Microsoft Entra Global Secure Access gives organizations a cloud-delivered security service edge that can enforce policy around private and internet access based on identity-aware controls, rather than relying only on endpoint posture. &lt;a href="https://learn.microsoft.com/en-us/entra/global-secure-access/overview" rel="noopener noreferrer"&gt;What is Global Secure Access?&lt;/a&gt; frames the problem in exactly this way: the modern workforce needs an identity-aware, cloud-delivered network perimeter.&lt;/p&gt;

&lt;p&gt;That matters because AI governance is not just a DLP conversation. It is also an access-governance conversation. &lt;a href="https://learn.microsoft.com/en-us/entra/id-governance/entitlement-management-overview" rel="noopener noreferrer"&gt;What is entitlement management?&lt;/a&gt; describes how organizations can automate access request workflows, assignments, reviews, and expiration across apps, groups, and SharePoint resources. Governance fails when access is granted on an ad hoc basis and never reviewed. Entitlement management is how you turn AI access into a time-bound, auditable trust decision.&lt;/p&gt;

&lt;p&gt;And the last layer is user authentication. If the employee is authenticating with a phishing-resistant method such as a passkey, Face ID, or Windows Hello, the enterprise is materially reducing the chance that the decision to use a third-party AI tool is driven by a credential theft event. &lt;a href="https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-passwordless" rel="noopener noreferrer"&gt;Passwordless authentication in Microsoft Entra&lt;/a&gt; is the modern model here: secure, modern credentials reduce the risk that a weak human factor becomes the path into an unauthorized AI workflow.&lt;/p&gt;

&lt;p&gt;You do not solve unsanctioned GenAI risk by adding one more checkbox to a policy page. You solve it by combining: a real trust boundary, policy enforcement at the edge, time-bound access, and phishing-resistant identity. That is how you turn a convenience problem into a governed control plane.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real answer is not a policy slogan. It is a system design rule
&lt;/h2&gt;

&lt;p&gt;Most organizations already know the first half of this story. They know they should not permit employees to dump customer data into a random public AI tool. The harder question is how to make that true in practice without breaking productivity.&lt;/p&gt;

&lt;p&gt;The answer is simple but not trivial: build an approved AI path and block the rest.&lt;/p&gt;

&lt;p&gt;That means a clear policy that treats unsanctioned GenAI like a data exfiltration control issue, not an employee-choice convenience issue. It means approved enterprise services with explicit contracts, data handling statements, and retention boundaries. It means DLP rules that detect prompts, attachments, and API keys. It means browser and network controls that prevent direct access to unapproved AI endpoints. It means a governance process that makes it easy to use the sanctioned tool and hard to use the unsanctioned one.&lt;/p&gt;

&lt;p&gt;This is also where the gateway again becomes central. If the gateway is the high-value choke point, then the policy should be enforced there. If an organization treats the gateway as a neutral network device, it will miss the operational fact that the gateway is one of the highest-value security assets in the environment. The gateway is where you can identify unauthorized model traffic, inspect prompts in transit, block exfiltration, and enforce control-plane policy at the edge. That is not a minor operational detail. It is a foundational design decision.&lt;/p&gt;

&lt;p&gt;The issue is not that "maybe one employee clicked the wrong thing." It is that the gateway is already the place where a lot of sensitive information is concentrated. Endpoint controls do not apply to the gateway. Endpoint controls do not protect the shared trust boundary. They protect the device. The gateway is not the device. The gateway is the enterprise chokepoint.&lt;/p&gt;

&lt;p&gt;This is why the right default posture is to ask a sharper question before approving a new AI use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what data crosses the enterprise boundary?&lt;/li&gt;
&lt;li&gt;which service receives it?&lt;/li&gt;
&lt;li&gt;what logging and retention obligations are in force?&lt;/li&gt;
&lt;li&gt;what downstream processing and model training settings apply?&lt;/li&gt;
&lt;li&gt;what is the contractual and legal boundary for the data?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answers are vague, the answer is no. If the service is not part of the approved enterprise trust domain, it should not receive business data.&lt;/p&gt;

&lt;h2&gt;
  
  
  And that means the governance question is not about the browser. It is about the trust contract
&lt;/h2&gt;

&lt;p&gt;A lot of AI governance conversations still focus on the user experience. They ask whether a feature is easy to use, whether employees can paste a prompt into a chat box, and whether the assistant is helpful. That is not the right security frame. The question is whether the organization is handing data to a service that it can trust, monitor, and govern.&lt;/p&gt;

&lt;p&gt;That is the point behind the Azure AI guidance: approved enterprise services are designed around data boundaries and customer data handling commitments. &lt;a href="https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/openai/data-privacy" rel="noopener noreferrer"&gt;Data, privacy, and security for Models sold by Azure in Microsoft Foundry&lt;/a&gt; lays out the important contract: Microsoft explicitly states that Azure OpenAI does not use customer data to retrain models for the foundation model service, which is a materially different security and governance posture from a personal or unsanctioned AI app.&lt;/p&gt;

&lt;p&gt;The organization is not just buying a productivity feature. It is buying a trust contract. A service that does not provide a clear data-handling boundary cannot be treated as a sanctioned enterprise AI tool just because it is convenient.&lt;/p&gt;

&lt;p&gt;The policy implication is direct. If you do not have a clear control plane, a policy boundary, and a trusted data-handling contract for the AI service, then the enterprise should not allow business data into it. That is the architectural rule. The app might be encrypted. The gateway might be protected. The logs might be clean. But if the organization does not control the data lifecycle, it does not control the risk.&lt;/p&gt;

&lt;p&gt;The gateway is a privileged point in your architecture. You should design around it as if it were a crown jewel. If it sits on an unsanctioned AI path, the enterprise data is already outside the trust boundary. That is not a training issue. It is a governance issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://csrc.nist.gov/pubs/sp/800-52/r2/final" rel="noopener noreferrer"&gt;NIST SP 800-52 Revision 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/global-secure-access/overview" rel="noopener noreferrer"&gt;What is Global Secure Access?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/id-governance/entitlement-management-overview" rel="noopener noreferrer"&gt;What is entitlement management?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-passwordless" rel="noopener noreferrer"&gt;Passwordless authentication in Microsoft Entra&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/application-gateway/application-gateway-tcp-tls-overview" rel="noopener noreferrer"&gt;Azure Application Gateway TCP/TLS proxy overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/api-management/api-management-key-concepts" rel="noopener noreferrer"&gt;What is Azure API Management?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/openai/data-privacy" rel="noopener noreferrer"&gt;Data, privacy, and security for Models sold by Azure in Microsoft Foundry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.infostealers.com/article/largest-ai-supply-chain-breach-of-2026-litellm-hack-impacts-thousands-of-global-enterprises-claim-your-ethical-disclosure/" rel="noopener noreferrer"&gt;Largest AI Supply Chain Breach of 2026: LiteLLM Hack Impacts Thousands of Global Enterprises&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://snyk.io/blog/poisoned-security-scanner-backdooring-litellm/" rel="noopener noreferrer"&gt;Snyk: Poisoned security scanner backdooring LiteLLM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.trendmicro.com/en_us/research/26/c/inside-litellm-supply-chain-compromise.html" rel="noopener noreferrer"&gt;Trend Micro: Inside the LiteLLM supply-chain compromise&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cycode.com/blog/lite-llm-supply-chain-attack/" rel="noopener noreferrer"&gt;Cycode: LiteLLM supply-chain attack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pandaily.com/china-llm-router-logs-6tb-credential-leak-researcher-claim" rel="noopener noreferrer"&gt;Pandaily: Researcher claims 6TB China LLM router logs exposed enterprise credentials&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>privacy</category>
      <category>azure</category>
    </item>
    <item>
      <title>How an AWS-hosted Entra Agent ID drops the client secret</title>
      <dc:creator>Anton Staykov</dc:creator>
      <pubDate>Wed, 16 Sep 2026 10:30:00 +0000</pubDate>
      <link>https://dev.to/astaykov/how-an-aws-hosted-entra-agent-id-drops-the-client-secret-1d4k</link>
      <guid>https://dev.to/astaykov/how-an-aws-hosted-entra-agent-id-drops-the-client-secret-1d4k</guid>
      <description>&lt;p&gt;After I published &lt;a href="https://dev.to/astaykov/one-identity-two-clouds-an-aws-hosted-agent-that-authorizes-with-microsoft-entra-agent-id-2bkl"&gt;One identity, two clouds: an AWS-hosted agent that authorizes with Microsoft Entra Agent ID&lt;/a&gt;, most of the follow-up questions were not about the cross-cloud authorization model.&lt;/p&gt;

&lt;p&gt;They were about the client secret.&lt;/p&gt;

&lt;p&gt;Where should it be stored? How should it be rotated? Should it live in AWS Secrets Manager? What happens when it expires?&lt;/p&gt;

&lt;p&gt;Those are sensible production questions. They were also questions about a demonstration compromise, not the architecture I wanted people to take away. The point of the sample was to show that an agent hosted in AWS can carry a governed Microsoft Entra Agent ID. The secret was plumbing used to keep the first proof of concept focused.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/astaykov/agentid-agentcore/releases/tag/v2.0" rel="noopener noreferrer"&gt;Release v2.0 of the sample&lt;/a&gt; removes that distraction. The agent now uses &lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation" rel="noopener noreferrer"&gt;Microsoft Entra workload identity federation&lt;/a&gt; with &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_oidc_outbound.html" rel="noopener noreferrer"&gt;AWS IAM Outbound Identity Federation&lt;/a&gt;. AWS attests to the workload with a short-lived JSON Web Token (JWT). Microsoft Entra accepts that JWT only when it matches an explicitly configured trust relationship.&lt;/p&gt;

&lt;p&gt;There is no Entra client secret in the runtime path.&lt;/p&gt;

&lt;p&gt;The interesting part is not that a secret moved somewhere safer. It is that the two clouds stopped sharing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The client secret was never the identity
&lt;/h2&gt;

&lt;p&gt;An Agent ID blueprint needs to authenticate before it can acquire tokens for the agent identities it governs. Microsoft documents federated identity credentials, certificates, and client secrets as supported blueprint credential types, while the agent identity itself does not hold a credential of its own (&lt;a href="https://learn.microsoft.com/entra/agent-id/agent-identities#authorizing-agent-identities" rel="noopener noreferrer"&gt;Agent identities in Microsoft Entra Agent ID&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The first version authenticated the blueprint with a client secret. That proved the Agent ID token flow, but it also introduced a long-lived value with the usual lifecycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate it in Microsoft Entra.&lt;/li&gt;
&lt;li&gt;transport it to AWS;&lt;/li&gt;
&lt;li&gt;protect it at rest and in deployment;&lt;/li&gt;
&lt;li&gt;expose it to the process that needs it;&lt;/li&gt;
&lt;li&gt;rotate it before expiry; and&lt;/li&gt;
&lt;li&gt;coordinate the old and new values without interrupting the agent.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A capable secret manager improves several of those steps. It does not remove them. &lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation#why-use-workload-identity-federation" rel="noopener noreferrer"&gt;Microsoft's workload identity federation guidance&lt;/a&gt; makes the recommendation explicit: configure Entra to trust tokens from the workload's external identity provider, then exchange those tokens for Microsoft identity platform access tokens.&lt;/p&gt;

&lt;p&gt;For this sample, the external identity provider is not another directory bolted onto the design. It is AWS itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What version 2.0 changes
&lt;/h2&gt;

&lt;p&gt;The core agent architecture remains the same. The agent still runs in &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agents-tools-runtime.html" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Runtime&lt;/a&gt;, still has a Microsoft Entra Agent ID, and still obtains resource-specific tokens for downstream calls.&lt;/p&gt;

&lt;p&gt;Only the blueprint authentication step changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before, the AgentCore process presented an Entra client secret.&lt;/li&gt;
&lt;li&gt;Now, the process asks AWS Security Token Service for a short-lived web identity token representing its IAM role.&lt;/li&gt;
&lt;li&gt;Microsoft Entra validates that AWS token against a federated identity credential (FIC) configured on the Agent ID blueprint.&lt;/li&gt;
&lt;li&gt;After that validation, the established Agent ID blueprint-to-agent token flow continues.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Microsoft now lists AWS workloads using IAM Outbound Identity Federation as a supported workload identity federation scenario (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation#supported-scenarios" rel="noopener noreferrer"&gt;Workload identity federation concepts&lt;/a&gt;). AWS documents the corresponding &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_oidc_outbound.html" rel="noopener noreferrer"&gt;outbound identity federation setup&lt;/a&gt; and the AWS Security Token Service (STS) &lt;a href="https://docs.aws.amazon.com/STS/latest/APIReference/API_GetWebIdentityToken.html" rel="noopener noreferrer"&gt;&lt;code&gt;GetWebIdentityToken&lt;/code&gt; API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant Agent as AgentCore runtime
    participant STS as AWS STS
    participant Entra as Microsoft Entra token endpoint
    participant API as Downstream API

    Agent-&amp;gt;&amp;gt;STS: GetWebIdentityToken using the runtime IAM role
    STS--&amp;gt;&amp;gt;Agent: Short-lived AWS-signed JWT
    Agent-&amp;gt;&amp;gt;Entra: Blueprint token request with AWS JWT as client_assertion
    Entra-&amp;gt;&amp;gt;Entra: Match issuer, subject, and audience
    Entra--&amp;gt;&amp;gt;Agent: Blueprint token (T1)
    Agent-&amp;gt;&amp;gt;Entra: Agent token request with T1 as client_assertion
    Entra--&amp;gt;&amp;gt;Agent: Agent identity access token
    Agent-&amp;gt;&amp;gt;API: Call with agent identity access token&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;There are two assertions here, and conflating them hides the actual security model.&lt;/p&gt;

&lt;p&gt;The AWS JWT proves which AWS workload is calling. The blueprint token proves that the authenticated blueprint is allowed to acquire a token for a particular agent identity. Neither token is the final downstream access token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure AWS to attest to the workload
&lt;/h2&gt;

&lt;p&gt;AWS IAM Outbound Identity Federation lets an IAM principal request an OpenID Connect (OIDC)-compatible token for an external service. The account must first have outbound web identity federation enabled, and the AgentCore runtime role must be allowed to call &lt;code&gt;sts:GetWebIdentityToken&lt;/code&gt; (&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_oidc_outbound.html" rel="noopener noreferrer"&gt;Enabling AWS IAM Outbound Identity Federation&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The permission should constrain what the role can request. In this design, the audience is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api://AzureADTokenExchange
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the audience Microsoft recommends for an external assertion presented to its token exchange (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation-create-trust#important-considerations-and-restrictions" rel="noopener noreferrer"&gt;Configure an app to trust an external identity provider&lt;/a&gt;). Restricting the AWS permission to this audience prevents the role from using the same permission to mint arbitrary outbound identity tokens for unrelated relying parties.&lt;/p&gt;

&lt;p&gt;The runtime obtains the assertion from STS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;sts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_web_identity_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Audience&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api://AzureADTokenExchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;SigningAlgorithm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RS256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DurationSeconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;aws_assertion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WebIdentityToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The method and response shape are defined by the AWS &lt;a href="https://docs.aws.amazon.com/STS/latest/APIReference/API_GetWebIdentityToken.html" rel="noopener noreferrer"&gt;&lt;code&gt;GetWebIdentityToken&lt;/code&gt; API&lt;/a&gt;. A five-minute lifetime is not a universal requirement, but it keeps the assertion useful only for the immediate exchange and limits the value of a captured token.&lt;/p&gt;

&lt;p&gt;The token has three claims that matter to the Entra trust:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;iss&lt;/code&gt; identifies the account-specific AWS outbound identity federation issuer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sub&lt;/code&gt; identifies the AWS IAM principal, in this case the AgentCore runtime role.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aud&lt;/code&gt; identifies Microsoft Entra's token exchange as the intended recipient.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS signs the token and publishes the metadata Microsoft Entra needs to validate it. The application does not create or sign the JWT itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure Entra to trust one AWS role
&lt;/h2&gt;

&lt;p&gt;The corresponding federated identity credential belongs on the Agent ID blueprint's application registration. It contains the expected issuer, subject, and audience:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"agentcore-runtime"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;account-specific-id&amp;gt;.tokens.sts.global.api.aws"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::&amp;lt;account-id&amp;gt;:role/&amp;lt;agentcore-runtime-role&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"audiences"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"api://AzureADTokenExchange"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are not descriptive labels. Microsoft Entra compares them with the &lt;code&gt;iss&lt;/code&gt;, &lt;code&gt;sub&lt;/code&gt;, and &lt;code&gt;aud&lt;/code&gt; claims in the incoming AWS token. The values must match exactly and case-sensitively (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation#how-it-works" rel="noopener noreferrer"&gt;Workload identity federation concepts&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That &lt;strong&gt;exact&lt;/strong&gt; match is the authorization boundary.&lt;/p&gt;

&lt;p&gt;Do not set the subject to an account-wide wildcard. Federated identity credentials do not support wildcards anyway, and that limitation is useful here (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation-create-trust#important-considerations-and-restrictions" rel="noopener noreferrer"&gt;Configure an app to trust an external identity provider&lt;/a&gt;). Trust the runtime role that represents this workload. If another role needs the same blueprint, make that an explicit trust decision rather than an accidental side effect of a broad pattern.&lt;/p&gt;

&lt;p&gt;The trust can now be stated without cloud branding:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Accept a token from this issuer, for this subject, intended for this audience.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the part workload identity federation standardizes. Microsoft Entra does not need the AWS access key. AWS does not need the Entra client secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  The most important code is in the MSAL constructors
&lt;/h2&gt;

&lt;p&gt;The raw HTTP requests explain the protocol, but they are not how version 2.0 implements it. The important part of the sample is how it composes two long-lived &lt;a href="https://learn.microsoft.com/python/api/msal/msal.application.confidentialclientapplication?view=msal-py-latest" rel="noopener noreferrer"&gt;&lt;code&gt;msal.ConfidentialClientApplication&lt;/code&gt;&lt;/a&gt; instances with two callable client-assertion providers.&lt;/p&gt;

&lt;p&gt;There are two documentation layers behind this code. The Microsoft Authentication Library (MSAL) team's &lt;a href="https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/How-to-Use-FIC-and-FMI-in-Agentic-Scenarios" rel="noopener noreferrer"&gt;FIC and federated managed identity (FMI) guidance for agentic scenarios&lt;/a&gt; describes the Agent ID-specific blueprint, &lt;code&gt;fmi_path&lt;/code&gt;, blueprint token (T1), and child-agent exchange. The &lt;a href="https://learn.microsoft.com/python/api/msal/msal.application.confidentialclientapplication?view=msal-py-latest" rel="noopener noreferrer"&gt;MSAL Python &lt;code&gt;ConfidentialClientApplication&lt;/code&gt; reference&lt;/a&gt; describes the generic confidential-client constructor and its &lt;code&gt;client_credential&lt;/code&gt; parameter. The v2.0 sample joins those two layers by supplying functions as the client assertions.&lt;/p&gt;

&lt;p&gt;That distinction matters because a client assertion is short-lived. Passing the current JWT as a static string would make the MSAL client depend on an assertion that eventually expires. Passing a function lets MSAL ask for an assertion when it actually needs to send a request to the token endpoint.&lt;/p&gt;

&lt;p&gt;The resulting chain is:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant Agent as Agent CCA
    participant AgentProvider as _agent_assertion()
    participant Blueprint as Blueprint CCA
    participant AwsProvider as _get_blueprint_assertion()
    participant STS as AWS STS

    Agent-&amp;gt;&amp;gt;AgentProvider: Request client assertion
    AgentProvider-&amp;gt;&amp;gt;Blueprint: Acquire T1 for fmi_path
    Blueprint-&amp;gt;&amp;gt;AwsProvider: Request client assertion
    AwsProvider-&amp;gt;&amp;gt;STS: GetWebIdentityToken
    STS--&amp;gt;&amp;gt;AwsProvider: Fresh AWS JWT
    AwsProvider--&amp;gt;&amp;gt;Blueprint: AWS client assertion
    Blueprint-&amp;gt;&amp;gt;Blueprint: Exchange AWS JWT for T1
    Blueprint--&amp;gt;&amp;gt;AgentProvider: T1
    AgentProvider--&amp;gt;&amp;gt;Agent: T1 client assertion
    Agent-&amp;gt;&amp;gt;Agent: Continue OBO request&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;There is no special MSAL class for "AWS-hosted Agent ID." Both objects are normal confidential clients. Their &lt;code&gt;client_credential&lt;/code&gt; configuration delegates assertion acquisition to application code.&lt;/p&gt;

&lt;h3&gt;
  
  
  The blueprint confidential client application delegates its credential to AWS STS
&lt;/h3&gt;

&lt;p&gt;The blueprint confidential client application (CCA) is created once per process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_blueprint_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfidentialClientApplication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;BLUEPRINT_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;client_credential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_assertion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_get_blueprint_assertion&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AUTHORITY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what is passed as &lt;code&gt;client_assertion&lt;/code&gt;. It is the function object &lt;code&gt;_get_blueprint_assertion&lt;/code&gt;, not the result of calling that function.&lt;/p&gt;

&lt;p&gt;The constructor therefore does not contact AWS STS and does not capture one JWT forever. It stores a callable that can provide the credential later. This extends the generic &lt;a href="https://learn.microsoft.com/python/api/msal/msal.application.confidentialclientapplication?view=msal-py-latest" rel="noopener noreferrer"&gt;&lt;code&gt;client_credential&lt;/code&gt; assertion mechanism&lt;/a&gt; with a provider backed by AWS. In the &lt;a href="https://github.com/astaykov/agentid-agentcore/blob/v2.0/agent/src/agent.py" rel="noopener noreferrer"&gt;v2.0 implementation&lt;/a&gt;, that provider asks STS for a five-minute assertion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_get_blueprint_assertion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_ensure_sts_client&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get_web_identity_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Audience&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FIC_AUDIENCE&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;SigningAlgorithm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RS256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;DurationSeconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WebIdentityToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MSAL invokes the provider when it needs a client assertion for an Entra token-endpoint request. The application deliberately does not add another cache around the AWS JWT. MSAL caches the Entra token produced by the exchange, while a later token-endpoint request can obtain a fresh AWS assertion.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;*args, **kwargs&lt;/code&gt; signature is intentional. The sample tolerates MSAL Python versions that invoke the assertion provider with no arguments and versions that provide assertion context. The provider does not need that context because its issuer, audience, signing algorithm, and lifetime are fixed by the workload-federation configuration.&lt;/p&gt;

&lt;p&gt;This is the first delegation point:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When the blueprint client needs to authenticate, delegate credential production to AWS STS.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The blueprint remains the confidential client from Entra's perspective. AWS STS is simply the component that supplies its short-lived proof.&lt;/p&gt;

&lt;h3&gt;
  
  
  The agent client delegates its assertion to the blueprint client
&lt;/h3&gt;

&lt;p&gt;The second constructor repeats the pattern at the next identity boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_agent_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfidentialClientApplication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;AGENT_IDENTITY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;client_credential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_assertion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_agent_assertion&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AUTHORITY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This client represents the child Agent ID. It cannot ask AWS STS for its client assertion directly because AWS attests to the runtime role, while the Entra FIC is configured on the blueprint. The agent client's assertion must be T1, the token that proves the blueprint-to-agent relationship.&lt;/p&gt;

&lt;p&gt;Its provider therefore delegates back to the persistent blueprint client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_agent_assertion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;blueprint_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_ensure_blueprint_app&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;t1_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;blueprint_app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquire_token_for_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api://AzureADTokenExchange/.default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;fmi_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AGENT_IDENTITY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;t1_result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TokenAcquisitionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FMI Stage 1 failed: {} - {}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;t1_result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;t1_result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error_description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;t1_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;fmi_path&lt;/code&gt; value tells Entra which child Agent ID the blueprint is acquiring T1 for. This is the Agent ID-specific extension described in the MSAL team's &lt;a href="https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/How-to-Use-FIC-and-FMI-in-Agentic-Scenarios" rel="noopener noreferrer"&gt;FIC and FMI agentic-scenario guidance&lt;/a&gt;, not an ordinary resource scope or OAuth OBO parameter. If the blueprint CCA must call the token endpoint, it invokes its own &lt;code&gt;_get_blueprint_assertion&lt;/code&gt; provider and receives a fresh AWS JWT. &lt;code&gt;_agent_assertion&lt;/code&gt; then returns the resulting T1 to the agent CCA.&lt;/p&gt;

&lt;p&gt;This is the second delegation point:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When the agent client needs to authenticate, delegate assertion production to the blueprint client.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The two callbacks model the two trust transitions directly in code:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    OBO["Agent CCA&amp;lt;br/&amp;gt;OBO request"]
    AP["_agent_assertion()"]
    BP["Blueprint CCA&amp;lt;br/&amp;gt;FMI request"]
    AWS["_get_blueprint_assertion()"]
    STS["AWS STS"]

    OBO --&amp;gt;|"needs T1"| AP
    AP --&amp;gt; BP
    BP --&amp;gt;|"needs AWS assertion"| AWS
    AWS --&amp;gt; STS
    STS --&amp;gt;|"AWS JWT"| AWS
    AWS --&amp;gt; BP
    BP --&amp;gt;|"T1"| AP
    AP --&amp;gt; OBO&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;This is dependency injection applied to client authentication. MSAL owns token acquisition, protocol parameters, and its token cache. The application owns how a valid assertion is produced at each boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Construct once, refresh through the callbacks
&lt;/h3&gt;

&lt;p&gt;Both confidential clients are lazy-initialized singletons in the container process. That is not just a small optimization.&lt;/p&gt;

&lt;p&gt;Reusing the blueprint CCA preserves its in-memory cache for T1. Reusing the agent CCA preserves its in-memory cache for downstream user tokens. Constructing either CCA for every request would discard the corresponding cache and force avoidable token-endpoint calls.&lt;/p&gt;

&lt;p&gt;The locks around both constructors make the initialization safe when concurrent invocations reach a newly started process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;_blueprint_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_blueprint_app&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_blueprint_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfidentialClientApplication&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The inner check matters because another invocation might have initialized the client while the current invocation waited for the lock.&lt;/p&gt;

&lt;p&gt;The callbacks do not mean AWS STS is called for every tool call. The normal path is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;try the relevant MSAL cache;&lt;/li&gt;
&lt;li&gt;return a usable cached token when one exists;&lt;/li&gt;
&lt;li&gt;invoke the assertion provider only when MSAL needs a token-endpoint request; and&lt;/li&gt;
&lt;li&gt;let that provider produce a current assertion rather than reuse an expired string.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is why the constructor wiring is the center of the implementation. The code does not contain a scheduled client-secret rotation job because the credential lifecycle has become part of token acquisition itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Assertion delegation is not user delegation
&lt;/h3&gt;

&lt;p&gt;The word "delegation" can hide two different mechanisms in this sample.&lt;/p&gt;

&lt;p&gt;The callback functions delegate &lt;em&gt;credential production&lt;/em&gt; between Python components. &lt;code&gt;_get_blueprint_assertion&lt;/code&gt; supplies the AWS workload assertion, and &lt;code&gt;_agent_assertion&lt;/code&gt; supplies T1. No user identity is involved in either callback.&lt;/p&gt;

&lt;p&gt;OAuth 2.0 on-behalf-of (OBO) user delegation happens later, when the persistent agent CCA calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tr_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent_app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquire_token_on_behalf_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;user_assertion&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;inbound_user_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point MSAL has two different inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the agent CCA's callable &lt;code&gt;client_assertion&lt;/code&gt;, which produces T1 and authenticates the Agent ID; and&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user_assertion&lt;/code&gt;, which is the inbound user token and carries the delegated user context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;T1 answers, "Which agent is making this request, and may this blueprint act for it?" The user assertion answers, "On behalf of which signed-in user is the agent acting?" The final resource token combines those parts of the flow for the requested downstream scope.&lt;/p&gt;

&lt;p&gt;Keeping these inputs separate is what makes the sample useful. The AWS workload assertion is not treated as a user token. T1 is not treated as the downstream access token. The inbound user token is not used as the agent credential. Each assertion has one job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exchange the AWS assertion for the blueprint token
&lt;/h2&gt;

&lt;p&gt;Underneath those MSAL constructors, the AgentCore runtime presents the AWS JWT to the Microsoft identity platform token endpoint as a client assertion. This is the &lt;a href="https://learn.microsoft.com/entra/identity-platform/v2-oauth2-client-creds-grant-flow#third-case-access-token-request-with-a-federated-credential" rel="noopener noreferrer"&gt;client credentials flow with a federated credential&lt;/a&gt;, not a custom token format or a vendor-specific backchannel.&lt;/p&gt;

&lt;p&gt;For an Agent ID blueprint, the first request also identifies the target agent identity through &lt;code&gt;fmi_path&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://login.microsoftonline.com/&amp;lt;tenant-id&amp;gt;/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id=&amp;lt;agent-blueprint-client-id&amp;gt;
&amp;amp;scope=api://AzureADTokenExchange/.default
&amp;amp;grant_type=client_credentials
&amp;amp;client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&amp;amp;client_assertion=&amp;lt;aws-sts-jwt&amp;gt;
&amp;amp;fmi_path=&amp;lt;agent-identity-client-id&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Microsoft documents the &lt;code&gt;fmi_path&lt;/code&gt; parameter as the instruction that tells the blueprint which agent identity it is acting for (&lt;a href="https://learn.microsoft.com/entra/agent-id/autonomous-agent-authentication-authorization-flow#request-a-token-for-the-agent-identity-blueprint" rel="noopener noreferrer"&gt;Authenticate and acquire tokens for autonomous agents&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;If the AWS token's signature is valid and its issuer, subject, and audience match the federated identity credential, Entra returns the blueprint token, often called T1 in the Agent ID flow.&lt;/p&gt;

&lt;p&gt;That is where the workload federation step ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continue with the Agent ID token exchange
&lt;/h2&gt;

&lt;p&gt;T1 is not the token the agent sends to Microsoft Graph, an MCP server, or another protected API. The runtime uses T1 as the client assertion in a second request, this time as the agent identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://login.microsoftonline.com/&amp;lt;tenant-id&amp;gt;/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id=&amp;lt;agent-identity-client-id&amp;gt;
&amp;amp;scope=&amp;lt;downstream-resource&amp;gt;/.default
&amp;amp;grant_type=client_credentials
&amp;amp;client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&amp;amp;client_assertion=&amp;lt;agent-blueprint-token-T1&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second request is the documented &lt;a href="https://learn.microsoft.com/entra/agent-id/autonomous-agent-authentication-authorization-flow#request-an-agent-identity-token" rel="noopener noreferrer"&gt;agent identity token request&lt;/a&gt;. The resulting access token represents the Agent ID and is scoped to the downstream resource.&lt;/p&gt;

&lt;p&gt;This separation matters operationally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS proves the runtime role to the blueprint.&lt;/li&gt;
&lt;li&gt;The blueprint proves its authority to act for the child agent identity.&lt;/li&gt;
&lt;li&gt;Microsoft Entra issues a resource-specific token for that agent identity.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Workload identity federation replaces the blueprint credential. It does not flatten the Agent ID parent-child model, bypass consent, or grant the agent new downstream permissions. The agent still needs the appropriate application permissions and administrator authorization described in the &lt;a href="https://learn.microsoft.com/entra/agent-id/autonomous-agent-authentication-authorization-flow#grant-application-permissions" rel="noopener noreferrer"&gt;autonomous agent authorization flow&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Authentication proves which workload and which agent are involved. Authorization still decides what that agent may do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What became safer, and what did not
&lt;/h2&gt;

&lt;p&gt;The most visible gain is the removal of a long-lived Entra client secret from the AWS runtime. There is no secret value to copy into deployment configuration, retrieve from a secret store, rotate on a calendar, or accidentally leave valid after the workload changes.&lt;/p&gt;

&lt;p&gt;The assertion is also bound to the AWS identity that requested it. Microsoft Entra accepts it because the configured subject identifies the expected IAM role, not because the bearer knows a shared string.&lt;/p&gt;

&lt;p&gt;But "secretless" should not be read as "trustless" or "credential-free."&lt;/p&gt;

&lt;p&gt;The AgentCore runtime's access to its IAM role is now the root of trust. A process that can run as that role and call &lt;code&gt;GetWebIdentityToken&lt;/code&gt; can request the same AWS assertion. The AWS role assignment, its trust policy, the permission to call STS, and the audience and duration conditions therefore need the same review you would give any production identity boundary.&lt;/p&gt;

&lt;p&gt;Short-lived tokens also remain bearer material while they are valid. Do not log the AWS assertion, T1, or the final access token. Do not pass them into model context or tool output. Cache them only for their useful lifetime and keep token acquisition in the trusted application layer.&lt;/p&gt;

&lt;p&gt;Federation removes an entire secret lifecycle. It does not excuse weak runtime isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes worth testing
&lt;/h2&gt;

&lt;p&gt;The happy path is short. The useful tests are mostly about proving that the trust is narrow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Change the audience
&lt;/h3&gt;

&lt;p&gt;Request an AWS token for an audience other than &lt;code&gt;api://AzureADTokenExchange&lt;/code&gt;. The Entra exchange should fail because the incoming &lt;code&gt;aud&lt;/code&gt; no longer matches the federated identity credential. Audience checking is a required part of the configured trust (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation-create-trust#important-considerations-and-restrictions" rel="noopener noreferrer"&gt;Configure an app to trust an external identity provider&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Run under another IAM role
&lt;/h3&gt;

&lt;p&gt;Obtain a valid AWS token as a different role. Signature validation should still succeed, but the token exchange should fail because the &lt;code&gt;sub&lt;/code&gt; claim does not match. This test demonstrates the difference between trusting AWS as an issuer and trusting every AWS workload.&lt;/p&gt;

&lt;h3&gt;
  
  
  Point to another AWS issuer
&lt;/h3&gt;

&lt;p&gt;Use a token from another AWS account's outbound identity issuer. The subject might look familiar, but the &lt;code&gt;iss&lt;/code&gt; value will differ. Entra uses the issuer and subject combination to identify the external workload (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation-create-trust#important-considerations-and-restrictions" rel="noopener noreferrer"&gt;Configure an app to trust an external identity provider&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Request a permission the agent does not have
&lt;/h3&gt;

&lt;p&gt;Keep the workload federation valid and ask for a downstream scope or application permission that has not been granted. The Agent ID token request should fail independently of AWS authentication. This is evidence that workload authentication has not bypassed the Agent ID authorization model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remove &lt;code&gt;sts:GetWebIdentityToken&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The failure should happen in AWS before any call reaches Microsoft Entra. That gives operators a clean boundary when diagnosing incidents: AWS controls whether the runtime can obtain an assertion, while Entra controls whether that assertion is trusted and what the resulting Agent ID may access.&lt;/p&gt;

&lt;h2&gt;
  
  
  The configuration is the architecture
&lt;/h2&gt;

&lt;p&gt;The code change in version 2.0 is not large. That is exactly why the configuration deserves attention.&lt;/p&gt;

&lt;p&gt;The security properties live in four places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the IAM role attached to the AgentCore runtime;&lt;/li&gt;
&lt;li&gt;the role's permission and conditions for &lt;code&gt;sts:GetWebIdentityToken&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the Entra federated identity credential's exact issuer, subject, and audience; and&lt;/li&gt;
&lt;li&gt;the permissions granted to the resulting Agent ID.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reviewing only the Python token requests misses three of those four boundaries.&lt;/p&gt;

&lt;p&gt;The first version asked, "Can an AWS-hosted agent authenticate and authorize as a Microsoft Entra Agent ID?" The answer was yes.&lt;/p&gt;

&lt;p&gt;Version 2.0 asks the production question: "Can it do that without distributing a Microsoft credential into AWS?" The answer is also yes.&lt;/p&gt;

&lt;p&gt;AWS attests to the workload it runs. Microsoft Entra decides whether to trust that attestation. The Agent ID blueprint then acquires a token for the child agent identity through its established flow.&lt;/p&gt;

&lt;p&gt;No shared secret crosses the cloud boundary. Trust does.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/astaykov/agentid-agentcore/releases/tag/v2.0" rel="noopener noreferrer"&gt;Release v2.0 of astaykov/agentid-agentcore&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation" rel="noopener noreferrer"&gt;Microsoft Entra workload identity federation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation-create-trust" rel="noopener noreferrer"&gt;Configure an app to trust an external identity provider&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/agent-id/autonomous-agent-authentication-authorization-flow" rel="noopener noreferrer"&gt;Authenticate and acquire tokens for autonomous agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/How-to-Use-FIC-and-FMI-in-Agentic-Scenarios" rel="noopener noreferrer"&gt;How to use FIC and FMI in agentic scenarios&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/python/api/msal/msal.application.confidentialclientapplication?view=msal-py-latest" rel="noopener noreferrer"&gt;MSAL Python ConfidentialClientApplication&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_oidc_outbound.html" rel="noopener noreferrer"&gt;AWS IAM Outbound Identity Federation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/STS/latest/APIReference/API_GetWebIdentityToken.html" rel="noopener noreferrer"&gt;AWS STS GetWebIdentityToken&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>entraagentid</category>
      <category>workloadidentity</category>
      <category>aws</category>
      <category>cloudsecurity</category>
    </item>
    <item>
      <title>Your API gateway is not an identity provider, and promoting it to one is a liability</title>
      <dc:creator>Anton Staykov</dc:creator>
      <pubDate>Tue, 08 Sep 2026 10:37:00 +0000</pubDate>
      <link>https://dev.to/astaykov/your-api-gateway-is-not-an-identity-provider-and-promoting-it-to-one-is-a-liability-3eoh</link>
      <guid>https://dev.to/astaykov/your-api-gateway-is-not-an-identity-provider-and-promoting-it-to-one-is-a-liability-3eoh</guid>
      <description>&lt;p&gt;Nobody disputes that the API gateway belongs at the center of an enterprise architecture. It is the discovery surface, the throttle, the routing layer, the audit point, and the place where a platform team decouples consumers from the services they consume. That is not the problem.&lt;/p&gt;

&lt;p&gt;The problem starts the moment a gateway stops validating tokens and starts issuing them. At that point it is no longer a gateway. It is an identity provider. And in most estates I have looked at, it is not governed, audited, monitored, or operated as one. It is operated as middleware.&lt;/p&gt;

&lt;p&gt;So let me put my position at the top rather than build to it. &lt;strong&gt;It is not the API gateway's job to be an identity provider.&lt;/strong&gt; The gateway does discovery, governance, quota, decoupling, routing, and pre-authorization at the edge. It does not mint identity. Everything that follows is a reason why that line exists and an accounting of what it costs to cross it.&lt;/p&gt;

&lt;p&gt;This article is a critical review of my own earlier piece, &lt;a href="https://dev.to/astaykov/the-industry-does-not-have-an-agent-identity-propagation-standard-yet-3fmf"&gt;The industry does not have an agent identity propagation standard yet&lt;/a&gt;. In that article I drew a cross-provider agent flow, and I drew it clean, because I wanted the propagation problem to be legible. The simplification did its job. It also hid the thing I now think matters most: in a real enterprise topology, the gateway quietly becomes an extra authorization server, and nobody counts it.&lt;/p&gt;

&lt;p&gt;Let me be honest about what this is. It is not a migration plan. It is a view, and an attempt to raise awareness of a risk that gets dismissed in design reviews with a shrug and the phrase "that is just how our platform works." I know these are brownfield estates. I know a re-platform does not land in a quarter, or in a year. I also know that most of the decisions that produced this shape were rational at the time they were made, taken by competent people under real constraints. None of that changes what the resulting architecture is. It is a liability, it deserves to be named as one, and naming it is not where the work stops.&lt;/p&gt;

&lt;p&gt;So here is the recommendation the rest of this article argues for, stated before the argument. &lt;strong&gt;Start consolidating your identity providers now, and keep driving the count down until the gateway is not one of them.&lt;/strong&gt; That is not a personal preference. Microsoft's &lt;a href="https://learn.microsoft.com/security/zero-trust/cisa-zero-trust-maturity-model-identity" rel="noopener noreferrer"&gt;CISA Zero Trust Maturity Model guidance for the identity pillar&lt;/a&gt; recommends consolidation of identity providers, identity stores, and identity management systems as the way to establish an identity foundation at all, and the &lt;a href="https://learn.microsoft.com/entra/standards/memo-22-09-enterprise-wide-identity-management-system" rel="noopener noreferrer"&gt;enterprise-wide identity management guidance for M-22-09&lt;/a&gt; puts the target plainly: as few managed identity systems as possible, with a consolidation plan behind them. Slow is acceptable. Not started is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The words I use are OAuth words
&lt;/h2&gt;

&lt;p&gt;Before the argument, the vocabulary. I am not inventing terms here, and I want the disagreement to be about architecture rather than about definitions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc6749#section-1.1" rel="noopener noreferrer"&gt;RFC 6749 section 1.1&lt;/a&gt; defines four roles, and every one of them shows up in this article. The &lt;em&gt;resource owner&lt;/em&gt; is the entity capable of granting access to a protected resource, which in this article is Bob. The &lt;em&gt;resource server&lt;/em&gt; hosts the protected resource and accepts access tokens. The &lt;em&gt;client&lt;/em&gt; is the application making requests on the resource owner's behalf. The &lt;em&gt;authorization server&lt;/em&gt; issues tokens after authenticating the resource owner and obtaining authorization. &lt;a href="https://openid.net/specs/openid-connect-core-1_0.html" rel="noopener noreferrer"&gt;OpenID Connect Core 1.0&lt;/a&gt; sits on top of that as the identity layer, which is how a client learns who Bob actually is rather than merely that some authorization exists.&lt;/p&gt;

&lt;p&gt;Four claims carry the whole argument.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sub&lt;/code&gt; is the subject, the entity the token is about. &lt;a href="https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2" rel="noopener noreferrer"&gt;RFC 7519 section 4.1.2&lt;/a&gt; defines it as the principal that is the subject of the JWT, and states plainly that the value is only guaranteed to be unique within the issuer's scope. Hold on to that sentence. It becomes the crux of this article.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;client_id&lt;/code&gt; identifies the client the token was issued to. &lt;a href="https://www.rfc-editor.org/rfc/rfc8693#section-4.3" rel="noopener noreferrer"&gt;RFC 8693 section 4.3&lt;/a&gt; defines it as the client identifier of the party to whom the token was issued, and &lt;a href="https://www.rfc-editor.org/rfc/rfc9068#section-2.2" rel="noopener noreferrer"&gt;RFC 9068 section 2.2&lt;/a&gt; makes it a required claim in a standards-compliant JWT access token. Microsoft Entra spells the same idea as &lt;code&gt;appid&lt;/code&gt; in v1.0 tokens and &lt;code&gt;azp&lt;/code&gt; in v2.0 tokens, which the &lt;a href="https://learn.microsoft.com/entra/identity-platform/access-token-claims-reference" rel="noopener noreferrer"&gt;access token claims reference&lt;/a&gt; documents alongside the rest of the claim set.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;act&lt;/code&gt; is the actor. &lt;a href="https://www.rfc-editor.org/rfc/rfc8693#section-4.1" rel="noopener noreferrer"&gt;RFC 8693 section 4.1&lt;/a&gt; defines it as the party acting on behalf of the subject, and it nests, so a chain of delegation can be expressed as a chain of &lt;code&gt;act&lt;/code&gt; claims.&lt;/p&gt;

&lt;p&gt;The rule that follows from those definitions is small, and it is the one people skip. &lt;strong&gt;&lt;code&gt;sub&lt;/code&gt; answers who authorized this, and the client and actor claims answer who is carrying that authorization.&lt;/strong&gt; Those are two different questions with two different answers, and an architecture that collapses them into one has stopped being able to answer either.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gateway's job is governance, not authorization
&lt;/h2&gt;

&lt;p&gt;Start with the version of the world where everything is tidy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flffvi8it42k8yxc13uie.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flffvi8it42k8yxc13uie.png" alt="Simplified architecture with one API gateway and one identity provider, where the gateway routes traffic and every resource is registered with the same authorization server" width="799" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The estate is deliberately small. There is an AI agent. There are two remote MCP servers, A and B. There is a REST API that the agent calls directly through a local tool. There is a single-page application that Bob uses. There is Bob. Behind the MCP servers there are the systems they actually talk to, another REST API and a database. I have left the LLM out of the picture entirely, because the model is not an identity, and the REST API stands in well enough for "a thing the agent calls that has its own access control."&lt;/p&gt;

&lt;p&gt;One assumption holds this picture together, and I want it stated rather than implied. In this version, &lt;strong&gt;the gateway is not an authorization server.&lt;/strong&gt; Every resource in the estate is registered with the same identity provider. The gateway sits in the traffic path and does gateway things.&lt;/p&gt;

&lt;p&gt;What are gateway things? Discovery, so consumers can find an API without asking a human. Governance, so the platform team can see what is being called and by whom. Quotas and rate limiting, so one badly written client does not take down a shared dependency. Decoupling, so a backend can move without breaking every caller. Routing, transformation, and versioning. The &lt;a href="https://learn.microsoft.com/azure/api-management/authentication-authorization-overview" rel="noopener noreferrer"&gt;Azure API Management authentication and authorization overview&lt;/a&gt; describes exactly this shape of responsibility, and it is a good shape.&lt;/p&gt;

&lt;p&gt;Pre-authorization at the edge belongs in that list too. A gateway that checks whether an inbound token is well formed, unexpired, signed by a trusted issuer, and carrying the audience it should be carrying is doing valuable work. The &lt;a href="https://learn.microsoft.com/azure/api-management/validate-jwt-policy" rel="noopener noreferrer"&gt;validate-jwt policy&lt;/a&gt; is precisely the right shape of control, a cheap early sanity check that rejects obvious garbage before it reaches a backend. I have no argument with any of that.&lt;/p&gt;

&lt;p&gt;Here is where I draw the line. It is not the gateway's job to decide whether the SPA may invoke the agent. It is not the gateway's job to decide whether the agent may invoke MCP Server A rather than MCP Server B. Those are authorization decisions about business capability, and they belong to an authorization server that owns the identities and the entitlements involved. &lt;strong&gt;A gateway that pre-authorizes is enforcing a decision. A gateway that authorizes is making one, and those are not the same architectural role.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A gateway that issues its own tokens has gone past both. It is not enforcing a decision and it is not making one. It is manufacturing the subject that every downstream decision will be made about. That is the identity provider's job, it is the only job on this page the gateway should not have, and no amount of policy configuration turns it into a gateway concern.&lt;/p&gt;

&lt;h2&gt;
  
  
  A plausible execution path
&lt;/h2&gt;

&lt;p&gt;Now run a request through it, and count the hops honestly.&lt;/p&gt;

&lt;p&gt;Bob signs in to the SPA (1). During that sign-in he also authorizes the SPA to invoke the AI agent on his behalf. He is the resource owner, the SPA is the client, and the agent is a resource he is delegating access to.&lt;/p&gt;

&lt;p&gt;Bob types a prompt. The SPA invokes the agent (2), carrying Bob's delegated authorization.&lt;/p&gt;

&lt;p&gt;The agent validates the request and builds a plan. The plan has three branches. It calls a local tool that hits a REST API (3). It calls MCP Server A (4), and MCP Server A must in turn call a downstream REST API (5) to do its work. It calls MCP Server B (6), and MCP Server B must run a query against a database (7).&lt;/p&gt;

&lt;p&gt;Because the agent is acting on Bob's behalf and not on its own account, Bob's identity has to survive every one of those hops. That is not a nice-to-have. If the downstream REST API at step 5 cannot tell that Bob is the reason it is being called, it cannot apply Bob's entitlements, and the agent has just become a confused deputy with a wider reach than any human user. So Bob must hold entitlements in the agent, in the REST API at step 3, in MCP Server A, in the REST API at step 5, in MCP Server B, and in the database at step 7.&lt;/p&gt;

&lt;p&gt;The MCP hops are not a matter of preference. The &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization" rel="noopener noreferrer"&gt;MCP authorization specification&lt;/a&gt; treats MCP servers as OAuth 2.1 resource servers and states that a server MUST NOT pass through the token it received to an upstream service. Each hop is a fresh authorization decision by design, and the spec is right to require it. Token pass-through destroys audience binding and turns every server in the chain into an amplifier for whatever token happens to arrive.&lt;/p&gt;

&lt;p&gt;I want to be clear that this is not a contrived example built to make a point. It is the ordinary shape of agentic work. An agent that cannot call more than one tool, or whose tools never call anything themselves, is a demo. Anything doing real work in an enterprise looks like this or worse, and "worse" usually means more hops, more providers, and more places where the chain is only as strong as the weakest assertion in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  In the ideal world, one authorization server sees all seven token requests
&lt;/h2&gt;

&lt;p&gt;Suppose the estate is greenfield and someone with authority made the obvious call: everything registers with one authorization server. Bob exists there as a user. The SPA is registered there as a client. The agent has its own identity there, distinct from Bob's. The REST APIs, both MCP servers, and the database all register there as resource servers with their own scopes.&lt;/p&gt;

&lt;p&gt;Seven token requests, and every one of them is an entitlement question with an answer.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The SPA authenticates Bob. Bob needs an entitlement to use the SPA at all.&lt;/li&gt;
&lt;li&gt;The SPA obtains delegated authorization to invoke the agent. Bob needs an entitlement to the agent, and the SPA needs to be authorized to ask for it on his behalf.&lt;/li&gt;
&lt;li&gt;The agent obtains a token for the REST API. Bob needs an entitlement in that API, and the agent needs to be permitted to request it.&lt;/li&gt;
&lt;li&gt;The agent obtains a delegated token for MCP Server A.&lt;/li&gt;
&lt;li&gt;MCP Server A obtains a token for the downstream REST API on Bob's behalf. Both MCP Server A and Bob need entitlements there, and they are separate checks.&lt;/li&gt;
&lt;li&gt;The agent obtains a delegated token for MCP Server B.&lt;/li&gt;
&lt;li&gt;MCP Server B obtains a token for the database on Bob's behalf, with the same double check.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This pattern has a name, and the standards already carry language for it. &lt;a href="https://www.rfc-editor.org/rfc/rfc8693#section-1.1" rel="noopener noreferrer"&gt;RFC 8693 section 1.1&lt;/a&gt; distinguishes delegation, where the acting party is given permission to act on the subject's behalf and both parties stay visible in the token, from impersonation, where the acting party disappears and the resource server cannot tell the difference. Microsoft's &lt;a href="https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow" rel="noopener noreferrer"&gt;on-behalf-of flow&lt;/a&gt; is the same idea with a vendor implementation attached.&lt;/p&gt;

&lt;p&gt;Now count how many times Bob appears as &lt;code&gt;sub&lt;/code&gt; in that chain. Seven times, issued by one issuer, with one meaning. There is one policy engine, one place where entitlements live, one enforcement surface, and therefore exactly one place where the entire chain can be denied. Revoke Bob at the authorization server and every hop downstream loses its footing on the next token request. Investigate an incident and there is one issuance log to correlate against.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One authorization server, all resources registered with it, one policy engine.&lt;/strong&gt; Almost nobody reading this is standing there today, and I am not pretending otherwise. It remains the direction every estate should be actively moving in, and the distance from it is a number you should be able to say out loud: how many authorization servers issue tokens your systems will accept, and which of them has an owner and a retirement date. Microsoft's &lt;a href="https://learn.microsoft.com/entra/architecture/migration-best-practices" rel="noopener noreferrer"&gt;guidance on migrating apps and authentication to Microsoft Entra ID&lt;/a&gt; exists because that consolidation is a program of work rather than a configuration change. Programs of work have to be started before they can finish.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real world runs four authorization servers, not three
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi5fv11zomwrpm6s4z7tx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi5fv11zomwrpm6s4z7tx.png" alt="Real world architecture where the SPA authenticates with IDP 0, the API gateway issues its own tokens from its own security token service, and downstream services trust IDP 1 and IDP 2" width="799" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first reality is that you do not have one identity provider. You have several, with users in some of them, resources in others, and a partial overlap that nobody has fully mapped. And I do not mean the tidy split people reach for first, the on-premises directory alongside the cloud directory with a sync between them. That one at least has a documented relationship and an owner. I mean the genuine fragmentation: an acquired business unit with its own tenant, a SaaS platform with its own embedded authorization server, a legacy estate fronted by a federation product that predates everyone currently on the team, and a partner integration that authenticates against something nobody has permission to change.&lt;/p&gt;

&lt;p&gt;Every one of those has a story attached, and most of the stories are true. None of them makes the shape permanent. The number of identity providers in an estate is a design choice that got made incrementally, one acquisition and one integration at a time, and it comes back down the same way. Treat that number as a metric with an owner, not as weather.&lt;/p&gt;

&lt;p&gt;The second reality is what teams did about it. To hide that fragmentation from application developers, they made the gateway an authorization server. Not as an act of vandalism, and not against the documentation either. Fronting a backend with the gateway as the token audience is a supported, documented pattern, and the &lt;a href="https://learn.microsoft.com/azure/api-management/authentication-authorization-overview" rel="noopener noreferrer"&gt;APIM authentication and authorization overview&lt;/a&gt; covers scenarios in exactly that family. It works. It ships. It removes a class of problem from every application team at once, which is why it wins the design review.&lt;/p&gt;

&lt;p&gt;Walk the same flow through that topology.&lt;/p&gt;

&lt;p&gt;Step 1. Bob signs in to the SPA against IDP 0. So far, unchanged.&lt;/p&gt;

&lt;p&gt;Step 2. The agent is not directly reachable. It sits behind the gateway. So the resource server registered in IDP 0 is not the agent, it is the gateway. The SPA requests a token to invoke "something in the gateway," and IDP 0 has no opinion about which something, because the gateway is a single registered audience covering many backends.&lt;/p&gt;

&lt;p&gt;Step 2.1. This is the step that does not appear in anybody's architecture diagram, and it is the reason I am writing this. The gateway validates the incoming token from IDP 0, runs its own policy, and then issues its own token from its own security token service. From here inward, the estate no longer runs on IDP 0 tokens. It runs on gateway tokens.&lt;/p&gt;

&lt;p&gt;Step 3 is skipped here on purpose. I am keeping the numbering aligned with the ideal-world walkthrough so the two can be compared line by line, and the direct REST API call adds nothing that the MCP branches do not already show.&lt;/p&gt;

&lt;p&gt;Step 4. The agent is now free of external authorization servers, which was the entire selling point. It validates a gateway STS token and calls MCP Server A through the gateway. Clean, simple, and completely dependent on the gateway being right.&lt;/p&gt;

&lt;p&gt;Step 5. MCP Server A has to call a REST API that does not trust IDP 0 or the gateway. It trusts IDP 1. So MCP Server A registers as a client in IDP 1 and requests a token there. And that request has to assert three things at once: that Bob exists in IDP 1, that this Bob is the same Bob who signed in to IDP 0 at step 1, and that he holds entitlements in that REST API. Three assertions, and only the third is a normal authorization check.&lt;/p&gt;

&lt;p&gt;Steps 6 and 7. The same again, with MCP Server B, IDP 2, and the database behind it. Another registration, another mapping, another set of three assertions.&lt;/p&gt;

&lt;p&gt;Count the authorization servers in that picture. IDP 0, IDP 1, IDP 2, and the gateway's own STS. &lt;strong&gt;Four.&lt;/strong&gt; Ask an architect how many identity providers are in the flow and you will usually be told three, because the fourth one is drawn as a rectangle with a networking icon on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  There are two places inside the gateway where Bob changes identity
&lt;/h2&gt;

&lt;p&gt;This is the part I want people to sit with.&lt;/p&gt;

&lt;p&gt;At step 2.1, Bob's authorization from IDP 0 becomes "Bob's" authorization from the gateway STS. At step 5, that gateway assertion becomes the basis for asserting Bob in IDP 1. Two translations, neither of them visible in the request path, neither of them producing an artifact that an auditor would recognize as an identity event.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    Bob["Bob&amp;lt;br/&amp;gt;IDP 0"]
    SPA["SPA"]
    GW["API&amp;lt;br/&amp;gt;gateway"]
    Agent["Agent"]
    MCPA["MCP&amp;lt;br/&amp;gt;Server A"]
    API["REST API&amp;lt;br/&amp;gt;trusts IDP 1"]

    Bob --&amp;gt;|"1. sign-in&amp;lt;br/&amp;gt;delegates&amp;lt;br/&amp;gt;to SPA"| SPA
    SPA --&amp;gt;|"2. token&amp;lt;br/&amp;gt;iss: IDP 0&amp;lt;br/&amp;gt;aud: gateway"| GW
    GW --&amp;gt;|"TRANSLATION 1&amp;lt;br/&amp;gt;step 2.1&amp;lt;br/&amp;gt;gateway STS&amp;lt;br/&amp;gt;mints Bob"| Agent
    Agent --&amp;gt;|"4. routed&amp;lt;br/&amp;gt;via gateway"| MCPA
    MCPA --&amp;gt;|"TRANSLATION 2&amp;lt;br/&amp;gt;step 5&amp;lt;br/&amp;gt;asserts Bob&amp;lt;br/&amp;gt;into IDP 1"| API

    linkStyle 2 stroke:#d62728,stroke-width:3px
    linkStyle 4 stroke:#d62728,stroke-width:3px&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Remember what &lt;a href="https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2" rel="noopener noreferrer"&gt;RFC 7519 section 4.1.2&lt;/a&gt; actually says about &lt;code&gt;sub&lt;/code&gt;. The value is unique within the issuer's scope. Not globally. Not across your estate. Within the issuer. So when the gateway STS emits a token whose &lt;code&gt;sub&lt;/code&gt; reads like Bob, that value means whatever the gateway says it means, and when MCP Server A turns that into an IDP 1 assertion, the identity has been through two mappings that no standard is validating for you.&lt;/p&gt;

&lt;p&gt;The consequence is one sentence long. &lt;strong&gt;Whoever controls the gateway decides who Bob is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two questions follow, and in every estate where I have asked them, neither has had a satisfying answer. First, how do you trace that the Bob who signed in at step 1 is the Bob whose entitlements were used at step 5? The correlation exists, if it exists at all, in gateway policy configuration and gateway logs, which are owned by a different team than the one that owns the identity logs. Second, if Bob at step 1 is compromised, what locks down Bob at step 5? Disabling the account in IDP 0 stops new sign-ins. It does not tell the gateway STS to stop minting, and it does not tell IDP 1 anything at all, because IDP 1 was never told that its Bob and IDP 0's Bob were related.&lt;/p&gt;

&lt;p&gt;Compare that with what the standards are actually asking for. &lt;a href="https://www.rfc-editor.org/rfc/rfc8707" rel="noopener noreferrer"&gt;RFC 8707 resource indicators&lt;/a&gt; exists so a client can name the resource it wants a token for, and so the authorization server can bind the token to that audience. The &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices" rel="noopener noreferrer"&gt;MCP security best practices&lt;/a&gt; require a server to reject any token that was not issued for it, and name the confused deputy problem explicitly. Both controls point the same direction: narrow the audience, keep the issuer meaningful, refuse tokens minted for somebody else. A gateway that re-mints identity at the edge is doing the opposite of audience binding. It is deliberately widening the audience so that everything behind it can stop caring.&lt;/p&gt;

&lt;p&gt;So here is the position, and I hold it without qualification. In trying to make things simpler behind the gateway, the industry made them worse, because it pushed the gateway into acting as an identity provider without ever admitting that is what happened. The simplicity is real. It was purchased with a trust anchor, and the invoice has not been read. A gateway that mints identity is not a pragmatic shortcut with a documentation page behind it. It is an unowned authorization server sitting in the middle of your estate, and it should not be there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four policy engines is a liability, and it should be called one
&lt;/h2&gt;

&lt;p&gt;I will concede the same ground twice, because it is fair ground. These are brownfield systems. They carry years of integrations, contracts, and behavior that nobody has fully documented. The gateway STS does not come out next sprint, and I am not asking anyone to pretend otherwise.&lt;/p&gt;

&lt;p&gt;The concession explains why this is hard. It does not excuse leaving it alone. Two things have to happen, and they run in parallel rather than in sequence. The first is naming the liability, because the first control you apply to a liability is writing its real name into a register that somebody reviews. The second is reducing the number of authorization servers in the estate on a schedule that has an owner, starting with the one that was never classified as an authorization server in the first place.&lt;/p&gt;

&lt;p&gt;Four authorization servers means four policy engines. Four places where entitlements are managed, with four different models for expressing them. Four places where human and non-human identities are provisioned and deprovisioned, with four different joiner, mover, leaver processes, one of which is a configuration file. Four places where a broad and fragile trust relationship gets created, and only three of them will show up in an identity governance review.&lt;/p&gt;

&lt;p&gt;The ownership asymmetry is the part that should worry you most. The gateway belongs to the application platform team. It does not belong to security, and it very rarely belongs to IAM. So the controls that IDP 0, IDP 1, and IDP 2 are subject to, privileged access review, signing key custody and rotation, conditional access on administrative operations, change approval on trust configuration, alerting on issuance policy changes, are simply not applied to the fourth authorization server. Not because anyone decided to exempt it. Because nobody classified it as one.&lt;/p&gt;

&lt;p&gt;Now look at it the way an adversary does. IDP 1 gets you the users and resources in IDP 1. The gateway STS gets you an assertion that IDP 1 will accept, an assertion that IDP 2 will accept, and an assertion that every service behind the gateway will accept without further question. &lt;strong&gt;To an attacker, that gateway is worth more than IDP 1.&lt;/strong&gt; It is also patched on an application team's schedule, monitored with application telemetry, and administered by people whose access was never reviewed as identity provider access, because on the asset inventory it is middleware.&lt;/p&gt;

&lt;h2&gt;
  
  
  RFC 8693 does not fix a broken trust anchor
&lt;/h2&gt;

&lt;p&gt;Whenever I make this argument, somebody reaches for token exchange, so let me address it directly rather than leave it sitting as an unstated rebuttal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8693" rel="noopener noreferrer"&gt;RFC 8693&lt;/a&gt; is a good specification and I am not arguing against using it. It gives you a real protocol for a security token service: a &lt;code&gt;subject_token&lt;/code&gt; describing the party on whose behalf the request is made, an optional &lt;code&gt;actor_token&lt;/code&gt; describing the party doing the acting, and a token type system that lets you convert between formats deliberately instead of by convention. It gives you the &lt;code&gt;act&lt;/code&gt; claim as a nested, inspectable delegation history, so a resource server can see the whole chain rather than a flattened identity. It even gives you &lt;a href="https://www.rfc-editor.org/rfc/rfc8693#section-4.4" rel="noopener noreferrer"&gt;&lt;code&gt;may_act&lt;/code&gt;&lt;/a&gt;, a claim stating which party is authorized to become the actor for a subject, which is genuinely the right primitive for constraining delegation.&lt;/p&gt;

&lt;p&gt;Here is what it does not give you: a reason to believe the mapping. RFC 8693 supplies mechanics for exchanging one token for another. It explicitly places the trust model and the policy governing exchanges outside its scope, and it imposes no requirements on how a deployment decides that this subject in this issuer corresponds to that subject in that issuer. That decision is yours. In the architecture above, the answer to "why should IDP 1 believe this is Bob" is that somebody configured a mapping in the gateway's STS, carefully, once, some time ago, and it has been correct ever since as far as anyone knows.&lt;/p&gt;

&lt;p&gt;The specification is candid about where that leads. It warns that when an authorization server does not authenticate the client in a token exchange, a compromised or leaked token can be leveraged through the STS into other tokens, and it tells you to require client authentication and to constrain who may exchange what. Apply that warning to a gateway STS that fronts an entire estate and the exposure takes an obvious shape. Control the gateway STS, or control anything that can persuade it to mint, and you can obtain a token for any user in any provider that trusts it. Not through a vulnerability. Through the feature.&lt;/p&gt;

&lt;p&gt;So the closing rule is the one I would defend in any design review. &lt;strong&gt;Token exchange is a mechanism for propagating identity. It is not a mechanism for establishing who is allowed to assert it.&lt;/strong&gt; The propagation problem is largely solved, and the standards are in decent shape. The assertion problem is the one your architecture answered by accident.&lt;/p&gt;

&lt;p&gt;Which brings me back to where I started, and I will state it once more without softening it. &lt;strong&gt;It is not the API gateway's job to be an identity provider.&lt;/strong&gt; Discovery, governance, quotas, decoupling, routing, versioning, and pre-authorization at the edge are the gateway's work, and a good gateway is genuinely excellent at all of it. Minting the subject that the rest of the estate authorizes against was never on that list and should not have been added to it.&lt;/p&gt;

&lt;p&gt;Two actions follow, and they are not alternatives to each other.&lt;/p&gt;

&lt;p&gt;Start the first one this quarter. If the gateway is asserting identity today, govern it as an identity provider, because the only other option available right now is an ungoverned one. Put it in the identity asset inventory. Review its administrators as privileged identity administrators. Rotate and custody its signing keys the way you custody an identity provider's. Put change approval on its trust configuration and alerting on its issuance policy. Feed its issuance logs into the same correlation surface as every other issuer, so a chain from step 1 to step 5 can actually be reconstructed.&lt;/p&gt;

&lt;p&gt;Then fund the second one. Count the authorization servers your estate trusts, put a name and a date against each one you intend to retire, and drive that number down until the gateway is out of the identity business entirely and back to being the very good gateway it already is. That work is slow, it is unglamorous, and it will outlast several reorganizations. Start it anyway, because the count does not fall on its own.&lt;/p&gt;

&lt;p&gt;And hold the line on new build while you do it. Nothing greenfield gets to add another authorization server, and nothing new gets fronted by a gateway that mints its own tokens. If you will not govern the gateway as an identity provider, do not let it assert identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/astaykov/the-industry-does-not-have-an-agent-identity-propagation-standard-yet-3fmf"&gt;The industry does not have an agent identity propagation standard yet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc6749#section-1.1" rel="noopener noreferrer"&gt;RFC 6749 section 1.1, OAuth 2.0 roles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openid.net/specs/openid-connect-core-1_0.html" rel="noopener noreferrer"&gt;OpenID Connect Core 1.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2" rel="noopener noreferrer"&gt;RFC 7519 section 4.1.2, the sub claim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8693" rel="noopener noreferrer"&gt;RFC 8693, OAuth 2.0 Token Exchange&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8693#section-1.1" rel="noopener noreferrer"&gt;RFC 8693 section 1.1, delegation and impersonation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8693#section-4.1" rel="noopener noreferrer"&gt;RFC 8693 section 4.1, the act claim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8693#section-4.3" rel="noopener noreferrer"&gt;RFC 8693 section 4.3, the client_id claim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8693#section-4.4" rel="noopener noreferrer"&gt;RFC 8693 section 4.4, the may_act claim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9068#section-2.2" rel="noopener noreferrer"&gt;RFC 9068 section 2.2, required claims in JWT access tokens&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8707" rel="noopener noreferrer"&gt;RFC 8707, Resource Indicators for OAuth 2.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity-platform/access-token-claims-reference" rel="noopener noreferrer"&gt;Microsoft Entra access token claims reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow" rel="noopener noreferrer"&gt;Microsoft identity platform on-behalf-of flow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/security/zero-trust/cisa-zero-trust-maturity-model-identity" rel="noopener noreferrer"&gt;CISA Zero Trust Maturity Model for the identity pillar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/standards/memo-22-09-enterprise-wide-identity-management-system" rel="noopener noreferrer"&gt;Memo 22-09 enterprise-wide identity management system&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/architecture/migration-best-practices" rel="noopener noreferrer"&gt;Migrate application authentication to Microsoft Entra ID&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/azure/api-management/authentication-authorization-overview" rel="noopener noreferrer"&gt;Azure API Management authentication and authorization overview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>apigateway</category>
      <category>oauth</category>
      <category>enterprisearchitecture</category>
      <category>aiagents</category>
    </item>
    <item>
      <title>AI agents need an authorization domain beyond OAuth</title>
      <dc:creator>Anton Staykov</dc:creator>
      <pubDate>Thu, 03 Sep 2026 10:03:00 +0000</pubDate>
      <link>https://dev.to/astaykov/ai-agents-need-an-authorization-domain-beyond-oauth-5167</link>
      <guid>https://dev.to/astaykov/ai-agents-need-an-authorization-domain-beyond-oauth-5167</guid>
      <description>&lt;p&gt;OAuth 2.0 is not broken because an AI agent uses it. It remains the right protocol family for issuing and presenting access tokens, expressing delegation, and binding a client to an authorization transaction. &lt;a href="https://www.rfc-editor.org/rfc/rfc9700.html" rel="noopener noreferrer"&gt;RFC 9700&lt;/a&gt; still recommends restricted tokens, audience checks, and sender constraints where applicable. That is not the problem.&lt;/p&gt;

&lt;p&gt;The problem is the quiet assumption that a valid token and a permitted API operation fully answer whether an agent should perform a particular consequential action now. An agent can turn one user request into a chain of tool calls, select values that the user did not enumerate, and reach a side effect that no scope string can describe by itself. &lt;strong&gt;A valid grant is not a standing instruction to proceed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The industry needs a distinct &lt;em&gt;agent-authorization domain&lt;/em&gt; between token issuance and the business operation. It combines the authorization server's proof of grant and delegation, a fine-grained policy engine's model of principal-action-resource semantics, and a context- and risk-driven decision engine. The resource still enforces the result. No one component should impersonate all the others.&lt;/p&gt;

&lt;p&gt;This is also where a future interoperability standard could matter. It would give security teams, agent developers, and application operators one plane for describing an action, carrying an evaluated disposition, and proving what the PEP enforced. Without a shared direction, each vendor will define agent authorization semantics differently. The result is fragmented controls, inconsistent decisions across tool boundaries, and complexity that does not improve the transaction. &lt;strong&gt;This is a proposed future direction, not an existing standard.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  OAuth proves a grant, not the meaning of an agent transaction
&lt;/h2&gt;

&lt;p&gt;OAuth defines the roles that matter here: an authorization server issues access tokens after a grant, and a resource server accepts a token to protect a resource. &lt;a href="https://www.rfc-editor.org/rfc/rfc6749.html" rel="noopener noreferrer"&gt;RFC 6749&lt;/a&gt; deliberately leaves the resource owner, client, authorization server, and resource server with separate responsibilities. That separation remains valuable when the client happens to be an agent.&lt;/p&gt;

&lt;p&gt;A protected API must validate a token intended for it before using it as proof of authorization. &lt;a href="https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens" rel="noopener noreferrer"&gt;Microsoft's access-token guidance&lt;/a&gt; is equally clear that the API owns token validation and that clients should treat access tokens as opaque. OAuth security guidance also says tokens should be restricted to the minimum privileges, resources, and actions required, with resource servers enforcing those restrictions for every request. &lt;a href="https://www.rfc-editor.org/rfc/rfc9700.html" rel="noopener noreferrer"&gt;RFC 9700&lt;/a&gt; defines that as a core best practice.&lt;/p&gt;

&lt;p&gt;That is standardized token and delegation mechanics. It does not supply a universal semantic model for questions such as: Is this payroll export for an approved case? Is the new payee related to the current supplier? Has this agent already attempted the same action through another tool? Should a proposed bulk deletion be staged rather than committed? Those are business- and transaction-specific questions. They require data the authorization server neither owns nor should be expected to infer.&lt;/p&gt;

&lt;p&gt;OAuth Rich Authorization Requests is a useful bridge, not the destination. &lt;code&gt;authorization_details&lt;/code&gt; lets a client request structured authorization data that an authorization server and protected resource can interpret, including resource, action, and transaction attributes. &lt;a href="https://www.rfc-editor.org/rfc/rfc9396.html" rel="noopener noreferrer"&gt;RFC 9396&lt;/a&gt; gives the protected API and authorization server that shared vocabulary. It does not turn a bearer or sender-constrained token into a live model of an agent's tool chain, business purpose, or current risk evidence. &lt;strong&gt;Use OAuth to establish the authorized envelope. Do not turn the token into a mutable risk dossier.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fine-grained policy gives the transaction its business meaning
&lt;/h2&gt;

&lt;p&gt;An agent-authorization domain needs a policy decision point that understands the action as the business sees it, rather than only as an HTTP method against a route. Cedar is one example: its policies evaluate a principal, action, resource, entity attributes, and transient request context. &lt;a href="https://docs.cedarpolicy.com/" rel="noopener noreferrer"&gt;Cedar's language documentation&lt;/a&gt; describes this model and its separation of policy from application code. Similar fine-grained authorization systems can fill the same role.&lt;/p&gt;

&lt;p&gt;That distinction matters because &lt;code&gt;POST /vendors/88421&lt;/code&gt; is not a useful authorization verb. &lt;code&gt;vendor.bank-account.change&lt;/code&gt; is. It can have a resource owner, a value change, a financial consequence, a required approver, and a rule that applies to this action but not to a vendor-address correction. The policy layer answers whether the authenticated agent, acting within a valid delegated or application grant, may propose this precise action on this precise resource.&lt;/p&gt;

&lt;p&gt;The policy decision point must receive trustworthy attributes, not a bag of agent-generated labels. The resource owner supplies canonical resource attributes. The workflow supplies the operation and the proposed effect. The identity layer supplies validated principal and grant attributes. The agent can supply a declared purpose, but a policy should treat that as an assertion until an authoritative workflow record corroborates it. &lt;strong&gt;The agent may describe intent. The authorization domain decides which facts are authoritative.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;validated identity and grant     canonical business data
             |                            |
             v                            v
      authorization server --&amp;gt; policy decision point
                                      |
agent tool plan and request context ---+--&amp;gt; policy result: permitted or denied
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://microsoft.github.io/agent-governance-toolkit/ARCHITECTURE/" rel="noopener noreferrer"&gt;Agent Governance Toolkit architecture&lt;/a&gt; is a concrete application-layer reference architecture for this enforcement shape. It evaluates agent actions against policy before execution and separates policy, identity and trust, runtime controls, tool-call interception, and audit components. That makes it useful implementation evidence for putting a governance gate before an agent action. It is not a platform-neutral authorization protocol, and its component specifications and conformance tests describe the toolkit's own architecture rather than a shared interoperability contract. &lt;strong&gt;Reference architectures can prove an enforcement pattern. They do not, by themselves, standardize the decision exchange between systems.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is already more precise than broad scopes. It is still not the complete decision for an autonomous actor, because permission semantics do not determine whether the current circumstances make execution acceptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static policy needs bounded intelligent judgment for changing context
&lt;/h2&gt;

&lt;p&gt;Static policies remain necessary deterministic guardrails. They define non-negotiable constraints: which principals may act, which business actions are categorically prohibited, which resources require an approval, and which conditions must always deny or stage an effect. Cedar, for example, evaluates explicit principal, action, resource, attribute, and request-context inputs against authored policies. &lt;a href="https://docs.cedarpolicy.com/" rel="noopener noreferrer"&gt;Its language documentation&lt;/a&gt; describes that decision model. &lt;strong&gt;An adaptive system may not weaken a hard policy constraint.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Static policies alone cannot safely resolve every agent-authorization question. An autonomous actor encounters changing, incomplete, and cross-domain context that a policy author cannot enumerate exhaustively in advance. A bounded intelligent decision system can synthesize that evidence and propose, or make, a transaction decision only within an explicit authority envelope. The resource PEP must enforce that envelope, log the evidence and outcome, and reject decisions that are expired, unbound to the proposed effect, or outside its authority. The system must be tested and evaluated for its intended decisions, with human escalation for consequential or ambiguous cases. &lt;a href="https://www.nist.gov/itl/ai-risk-management-framework" rel="noopener noreferrer"&gt;NIST's AI Risk Management Framework&lt;/a&gt; provides voluntary guidance for governing and evaluating AI systems, not an authorization protocol. &lt;strong&gt;Adaptive judgment can operate inside policy. It cannot replace policy ownership or resource enforcement.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The third component is therefore a context- and risk-driven decision engine. It evaluates signals that are time-bound, cumulative, or external to the grant: the sensitivity and volume of data, destination reputation, a sequence of failed attempts, an unusual operation rate, an open investigation, or a human approval that has expired. The engine returns a bounded disposition for this transaction. It does not mint access tokens, reinterpret OAuth grants, silently rewrite authorization policy, or become the policy owner for every resource.&lt;/p&gt;

&lt;p&gt;This boundary is particularly important for insider risk. Microsoft Purview Insider Risk Management correlates service and third-party indicators to identify potentially malicious or inadvertent activity, then routes evidence through alerts, triage, investigation, and action. &lt;a href="https://learn.microsoft.com/en-us/purview/insider-risk-management" rel="noopener noreferrer"&gt;Its documented workflow&lt;/a&gt; is an investigative and response system, not a generic inline API authorization server. The &lt;em&gt;Risky Agents&lt;/em&gt; template detects signals including risky prompts, sensitive responses, access to sensitive or priority SharePoint files, risky websites, external sharing, and activity above an agent baseline for supported agent platforms. &lt;a href="https://learn.microsoft.com/en-us/purview/insider-risk-management-policy-templates" rel="noopener noreferrer"&gt;The policy-template documentation&lt;/a&gt; defines that scope.&lt;/p&gt;

&lt;p&gt;Those signals are useful evidence. They are not enough, by themselves, to dictate every API outcome. A risk system may know an agent's activity is unusual; the resource owner knows whether this operation changes a bank account, publishes a document, or merely reads a record. &lt;strong&gt;Risk supplies a disposition. The business policy supplies the meaning.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent proposes a business action
             |
             +--&amp;gt; authorization server: is the grant and delegation valid?
             |
             +--&amp;gt; policy decision point: may this principal propose this action on this resource?
             |
             +--&amp;gt; risk context engine: should this transaction proceed, hold, or require review now?
             |
             +--&amp;gt; resource policy enforcement point: commit, deny, hold, or escalate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resource or application &lt;em&gt;policy enforcement point (PEP)&lt;/em&gt; owns the final side effect because it owns the target state. This placement avoids asking an authorization server to understand every business transaction and avoids asking an insider-risk program to become an unbounded authorization service.&lt;/p&gt;

&lt;h2&gt;
  
  
  A proposed decision contract must bind decisions to the proposed effect
&lt;/h2&gt;

&lt;p&gt;An agent-authorization decision cannot be &lt;code&gt;allow&lt;/code&gt; in the abstract. A proposed future &lt;em&gt;decision contract&lt;/em&gt; must bind to the proposed effect, the relevant version of policy, the evaluated context, and a short validity period. Otherwise, an agent can obtain a decision for a harmless request and reuse it for a different one.&lt;/p&gt;

&lt;p&gt;Consider a procurement agent that receives a valid delegated grant to update supplier records. It proposes changing supplier &lt;code&gt;88421&lt;/code&gt; from one bank account to another. The policy engine verifies that the agent is allowed to propose this kind of change for that supplier. The risk engine sees that the destination account is new, the amount of pending payments is material, and the agent has already attempted a similar change through a second workflow. The PEP stages the update rather than committing it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transaction_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6bfbdbd2-0f82-4b44-a27a-e1d939bc0d38"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"agent:vendor-onboarding"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vendor.bank-account.change"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vendor:88421"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"proposed_effect_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:4d2c..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"decision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"permit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"policy_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vendor-change-v3"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"risk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"disposition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reason_codes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"NEW_DESTINATION"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DUPLICATE_ATTEMPT"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-01T14:20:00Z"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enforcement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"outcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"staged"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"required_approver"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"finance-controller"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hash stands for a normalized representation of the proposed change, not a raw prompt. The PEP must recompute it before committing, reject an expired disposition, and record the policy and risk versions it relied on. This is ordinary transaction integrity applied to an agent decision, not a new claim about what OAuth tokens contain.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;allow&lt;/code&gt;, &lt;code&gt;deny&lt;/code&gt;, &lt;code&gt;hold&lt;/code&gt;, and &lt;code&gt;escalate&lt;/code&gt; are more useful than a single numerical score. &lt;code&gt;allow&lt;/code&gt; permits the PEP to perform the already-authorized operation. &lt;code&gt;deny&lt;/code&gt; prevents it. &lt;code&gt;hold&lt;/code&gt; preserves a resumable state without the external side effect. &lt;code&gt;escalate&lt;/code&gt; routes a specifically described proposed effect to a named approver or workflow. This vocabulary is an example of a proposed product contract, not a standard. Any such contract must be deterministic enough to test and audit. &lt;strong&gt;A risk score is evidence. A disposition is an enforceable decision.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Delegation does not erase the human authority boundary
&lt;/h2&gt;

&lt;p&gt;Delegated authority is paramount when an agent operates in a human account context or on behalf of a human. OAuth defines the resource owner as the entity capable of granting access and models the client as a separate role. &lt;a href="https://www.rfc-editor.org/rfc/rfc6749.html" rel="noopener noreferrer"&gt;RFC 6749&lt;/a&gt; preserves that distinction even when the client acts with a delegated grant. The agent may execute within that grant. It does not become the human principal, nor does an earlier authorization settle every later consequential choice.&lt;/p&gt;

&lt;p&gt;Long-running operations make the boundary visible. An agent can collect evidence, coordinate tools, and reach a decision point hours after the human's initiating instruction. A future architecture should define a &lt;em&gt;human-authority backchannel&lt;/em&gt; through which the agent can present the exact proposed effect, receive an approval, rejection, clarification, or cancellation, and bind that response to the transaction before the PEP commits. &lt;/p&gt;

&lt;p&gt;The backchannel must be independent of agent-generated prose. The PEP or workflow service should construct the request from canonical action, resource, effect, deadline, and consequence data, then record the human response with the transaction. A chat message that says "looks good" is not enough unless the system can bind it to one immutable proposed effect. &lt;strong&gt;Delegated execution is not delegated sovereignty. The human retains authority over the consequential commitment.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with transactions that have a reversible boundary
&lt;/h2&gt;

&lt;p&gt;The sharp edge is timing. An asynchronous investigation system cannot safely become an inline decision point unless its latency, availability, freshness, and failure behavior are explicitly engineered for that job. Purview's documented workflow moves from policy matches to alerts, reviewer triage, cases, and actions. &lt;a href="https://learn.microsoft.com/en-us/purview/insider-risk-management" rel="noopener noreferrer"&gt;Microsoft's workflow documentation&lt;/a&gt; supports a different operational purpose from a low-latency PEP call.&lt;/p&gt;

&lt;p&gt;Begin where the application has a natural pending state. A customer-export agent can assemble the candidate set, evaluate sensitivity labels and destination, then hold the delivery link until a data owner approves. A knowledge-management agent can stage an external publication or bulk deletion and submit the exact diff for review. A payments agent can save a proposed bank-account change while preventing release of pending payments until the required approval arrives. For long-running delegated work, this pending state is the practical location for a proposed human-authority backchannel.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;authorize grant -&amp;gt; evaluate business policy -&amp;gt; evaluate risk context
       -&amp;gt; stage proposed effect -&amp;gt; approve or reject -&amp;gt; commit once
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design creates a real control point before the state change. Trying to reverse an external email, payment, or published document after an alert appears is incident response, not authorization. &lt;strong&gt;The PEP is the last trustworthy place to stop a consequential transaction.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Record observed facts, not an invented provenance story
&lt;/h2&gt;

&lt;p&gt;Every agent transaction needs a durable record at the PEP. It should identify the authenticated agent principal, validated grant reference, initiating workflow or user context when observed, action, resource, proposed-effect hash, policy result, risk disposition, enforcement outcome, and correlation identifiers. That lets an investigation connect agent traces, authorization events, application telemetry, and a case without claiming that an access token proves who authored every intermediate model decision.&lt;/p&gt;

&lt;p&gt;The privacy boundary belongs in the design, not the postmortem. Purview describes privacy-by-design controls including default pseudonymization, role-based access control, and audit logs. &lt;a href="https://learn.microsoft.com/en-us/purview/insider-risk-management" rel="noopener noreferrer"&gt;Microsoft's overview&lt;/a&gt; documents those safeguards. An agent-authorization record should minimize sensitive context, separate access to raw content from access to decision metadata, and retain each according to a documented purpose and schedule. &lt;strong&gt;Observability is not permission to copy every prompt into every audit system.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;NIST's AI Risk Management Framework is voluntary guidance for incorporating trustworthiness considerations into the design, development, use, and evaluation of AI systems. &lt;a href="https://www.nist.gov/itl/ai-risk-management-framework" rel="noopener noreferrer"&gt;NIST AI RMF&lt;/a&gt; is useful here as a governance frame, but it does not substitute for a transaction protocol or a resource-specific enforcement decision. Risk governance and agent authorization must meet at the operation, where both can be made concrete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not make the authorization server carry the whole system
&lt;/h2&gt;

&lt;p&gt;It is tempting to solve the agent problem by packing richer claims into tokens or by making the authorization server query every risk source. That centralizes business semantics, changing risk data, and resource-specific side-effect knowledge in a component that cannot reliably own them. The result is either a fragile token schema or a shadow business-policy engine hidden behind token issuance.&lt;/p&gt;

&lt;p&gt;Keep the four responsibilities legible. The authorization server establishes a valid grant and delegation boundary. Static policy defines deterministic, non-negotiable constraints for the named principal, business action, and resource. The bounded context and risk engine synthesizes admissible evidence into a time-bound disposition inside its explicit authority envelope. The PEP binds those outcomes to the proposed effect, enforces the envelope, records the decision, and controls the commit.&lt;/p&gt;

&lt;p&gt;That is the proposed evolution. Current authorization standards and static policies remain necessary, and fine-grained authorization frameworks already solve a meaningful part of the semantic problem. The additional requirement is a bounded intelligent decision capability for context that is changing, incomplete, or spread across domains, with testing, logging, resource enforcement, and a human backchannel for consequential or ambiguous commitments. A future interoperability standard could define common decision and evidence exchanges while preserving resource-specific policy, the authority envelope, and the human authority boundary. This is an architectural argument, not a claim that a standard already defines it.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc6749.html" rel="noopener noreferrer"&gt;RFC 6749: The OAuth 2.0 Authorization Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9396.html" rel="noopener noreferrer"&gt;RFC 9396: OAuth 2.0 Rich Authorization Requests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9700.html" rel="noopener noreferrer"&gt;RFC 9700: Best Current Practice for OAuth 2.0 Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens" rel="noopener noreferrer"&gt;Access tokens in the Microsoft identity platform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/purview/insider-risk-management" rel="noopener noreferrer"&gt;Learn about Insider Risk Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/purview/insider-risk-management-policy-templates" rel="noopener noreferrer"&gt;Learn about Insider Risk Management policy templates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cedarpolicy.com/" rel="noopener noreferrer"&gt;Cedar Policy Language Reference Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nist.gov/itl/ai-risk-management-framework" rel="noopener noreferrer"&gt;NIST AI Risk Management Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://microsoft.github.io/agent-governance-toolkit/ARCHITECTURE/" rel="noopener noreferrer"&gt;Agent Governance Toolkit architecture&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>oauth</category>
      <category>identitygovernance</category>
      <category>aiagents</category>
    </item>
    <item>
      <title>Authorization options for your AI agents with Entra Agent ID</title>
      <dc:creator>Anton Staykov</dc:creator>
      <pubDate>Thu, 27 Aug 2026 09:43:00 +0000</pubDate>
      <link>https://dev.to/astaykov/authorization-options-for-your-ai-agents-with-entra-agent-id-4p0k</link>
      <guid>https://dev.to/astaykov/authorization-options-for-your-ai-agents-with-entra-agent-id-4p0k</guid>
      <description>&lt;p&gt;Authorization in Entra has changed, and most of that change is not visible in the product UI. The boundaries have always been there; they are just no longer optional side-quests. When you architect an Entra Agent ID, you are not choosing &lt;em&gt;one&lt;/em&gt; authorization mechanism. You are composing five of them, each with its own trust surface. Understanding that composition is the difference between a loosely guarded identity and one that respects the principle of least privilege.&lt;/p&gt;

&lt;p&gt;Consent is authorization, not authentication. But consent is only the start. After you have granted the app permission to call Microsoft Graph, the hard work is to ensure it calls Graph with the minimum scope it actually needs, at the exact moment it needs it, and only on behalf of the principals you intended. That is what this article addresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  User assignment as your outermost gate
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/assign-user-or-group-access-portal" rel="noopener noreferrer"&gt;User assignment&lt;/a&gt; is the first boundary you should establish. It is blunt, but it is not optional. When you require user or group assignment to an enterprise application, you are saying: "Only the principals I explicitly list in this Entra app object can even attempt to authenticate to this workload."&lt;/p&gt;

&lt;p&gt;For user-facing applications, this is straightforward. You assign Alice and Bob to the app, and only Alice and Bob can sign in.&lt;/p&gt;

&lt;p&gt;For an AI agent, user assignment works similarly. You do not assign the agent's identity to an app. Instead, you assign the &lt;em&gt;users&lt;/em&gt; who are allowed to delegate to or authenticate to the agent. In other words that assignment controls which users can invoke the agent or have the agent act on their behalf.&lt;/p&gt;

&lt;p&gt;Here is the critical part: user assignment gates access at two levels. First, it controls which users can authenticate to the agent itself. Second, and less obvious, it also gates downstream resource access. If a user is not explicitly assigned to a downstream API or resource, the agent cannot access that resource on behalf of that user—even if the agent's identity is assigned to that API. User assignment is not negotiable at any layer of the call chain.&lt;/p&gt;

&lt;p&gt;If you do not require assignment, Entra will still respect the OAuth 2.0 consent framework. However, it will allow any user in your tenant (or any user in a federated tenant, if you have configured cross-tenant access) to attempt authentication. The app sees the request, asks for consent, the user grants it (or the admin pre-consents), and the app gets a token. User assignment closes that loophole. It says: "No request reaches this workload unless the requester is already in my approved list—and no downstream resource request on behalf of that user will succeed if the user is not approved there either."&lt;/p&gt;

&lt;p&gt;For AI agents deployed at scale across teams or business units, user assignment is your guard rail. Without it, you are depending on the app and downstream APIs to reject requests from unknown principals. The better defense is to never admit an unapproved principal to the conversation in the first place, and to enforce that control consistently across all resources the agent might access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; &lt;a href="https://learn.microsoft.com/en-us/entra/identity-platform/howto-restrict-your-app-to-a-set-of-users" rel="noopener noreferrer"&gt;User assignment requirement&lt;/a&gt; works for both Agent Identity Blueprints and individual agent identities. However, there is currently no Entra Admin Center UX that exposes user assignment functionality — only the Microsoft Graph API. For practical user assignment at scale, consider using &lt;a href="https://dev.to/astaykov/access-packages-are-the-control-plane-for-entra-agent-id-permissions-4kph"&gt;Access Packages&lt;/a&gt; to facilitate and manage user assignments for AI agents. Another limitation worth mentioning is that at time of writing of this article, only Agent Identity Blueprints support custom roles definitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conditional access as authorization policy
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/identity/conditional-access/agent-id" rel="noopener noreferrer"&gt;Conditional access&lt;/a&gt; is a policy engine that controls token acquisition. Unlike app roles or group claims, which are attributes embedded in a token, conditional access is evaluated &lt;em&gt;before&lt;/em&gt; the token is issued. If a policy is violated, the agent does not get a token.&lt;/p&gt;

&lt;p&gt;Here are concrete examples of conditional access for AI agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom Security Attribute filters:&lt;/strong&gt; "If the app is tagged with &lt;code&gt;Agents.Approved:True&lt;/code&gt; then allow access." The agent cannot acquire a token unless it is tagged as approved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk-based controls:&lt;/strong&gt; "If Entra's identity protection detects a risky sign-in pattern, require additional verification or deny access." In on-behalf-of type of acces, the user's authentication context and user's offline calculated risks are evaluated (unusual location, impossible travel, leaked credentials, etc.), and access is blocked if that evaluation crosses threshold. In autonomous agent access the agent's risk is evaluated and access is blocked if agent's risk crosses defined threshold. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device compliance:&lt;/strong&gt; "The device must be compliant with Intune policies." If the user is invoking the AI Agents from a device that is not enrolled or is marked non-compliant, the request is blocked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conditional access distinguishes two data access patterns that are critical for AI agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Autonomous access:&lt;/strong&gt; The agent acts on its own behalf (application flow). Conditional access evaluates the agent's context (where it is running, what device it is on, what time it is).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-behalf-of (OBO) user access:&lt;/strong&gt; The agent acts on behalf of a user. Conditional access evaluates &lt;em&gt;the user's&lt;/em&gt; context (where the user is, their device, their authentication method), not the agent's context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Critical limitation:&lt;/strong&gt; At the time of writing, Conditional Access accounts for the &lt;em&gt;User&lt;/em&gt; (subject) and &lt;em&gt;Target&lt;/em&gt; (resource), but not the &lt;em&gt;AI Agent&lt;/em&gt; (actor) - when the agents accesses data on-behalf-of the user. This means CA policies cannot directly distinguish between two different agents making requests in the same context - the same user is invoking two different agents, and both happen to require a token to access Work IQ, the conditional access sees these requests identically. &lt;/p&gt;

&lt;p&gt;See &lt;a href="https://learn.microsoft.com/en-us/entra/identity/conditional-access/agent-id" rel="noopener noreferrer"&gt;Conditional Access for Agents in Microsoft Entra&lt;/a&gt; and &lt;a href="https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-autonomous-agents?tabs=use-the-enhanced-object-picker" rel="noopener noreferrer"&gt;Recommended policies for autonomous agents&lt;/a&gt; for detailed policy patterns.&lt;/p&gt;

&lt;p&gt;The sharp edge is timing. Conditional access gates token acquisition. If your agent acquires a long-lived token and then starts running outside the approved perimeter, the existing token still works. Conditional access is a gate at the door, not a checkpoint at every step. If you need continuous context verification, you need a different mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  App roles as delegated authorization claims
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-apps" rel="noopener noreferrer"&gt;App roles&lt;/a&gt; are a mechanism for the application, or the AI agent, to define its own authorization model. You define a set of roles in your app registration (or Agent Identity Blueprint). You assign users, groups, or service principals to those roles. When the principal authenticates, Entra puts the role membership in the &lt;code&gt;roles&lt;/code&gt; claim of the ID token (or the access token, depending on how you configure the app).&lt;/p&gt;

&lt;p&gt;For a human-facing app, this is straightforward. Your app checks: "Is the user in the &lt;code&gt;Manager&lt;/code&gt; role? If yes, show the budget page. If no, hide it." Entra does the assignment; your app does the enforcement.&lt;/p&gt;

&lt;p&gt;For an AI agent, the model is the same. You can assign humen to a role defined by the Agent Blueprint. In this case, your AI Agent can take authorization decisions for what funcitonality should be avialable to the user. Or, in case of the autonomous agent, you assign the agent's identity to a role defined by downstream API. Then, when your agent acquires a token on behalf of itself (application flow), that token contains the roles assigned to the service principal. Your APIs can inspect the token and make authorization decisions.&lt;/p&gt;

&lt;p&gt;Here is an example. You have an agent that needs to read data from one API and another agent that needs to read and write. You define two app roles in the API app registration: &lt;code&gt;DataReader&lt;/code&gt; and &lt;code&gt;DataWriter&lt;/code&gt;. You assign the read-only agent's service principal to the &lt;code&gt;DataReader&lt;/code&gt; role. You assign the read-write agent to the &lt;code&gt;DataWriter&lt;/code&gt; role. When each agent calls your API, your API code checks the &lt;code&gt;roles&lt;/code&gt; claim. If the token does not include &lt;code&gt;DataWriter&lt;/code&gt;, the write operation fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Current UX limitations:&lt;/strong&gt; The Entra Admin Center does not directly expose a UI for defining roles on Agent Identity Blueprints. Roles can be defined and managed via the &lt;a href="https://learn.microsoft.com/en-us/entra/identity-platform/reference-app-manifest" rel="noopener noreferrer"&gt;app manifest&lt;/a&gt;. Role assignment to users and groups can be done via the Microsoft Graph API or, more scalably, via &lt;a href="https://dev.to/astaykov/access-packages-are-the-control-plane-for-entra-agent-id-permissions-4kph"&gt;Access Packages&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important distinction:&lt;/strong&gt; While the "require user assignment" property exists for Agent Identities, there is no real application scenario where interactive authentication to an Agent happens outside of the Agent Identity Blueprint. The Agent Identity Blueprint is the object that defines the authentication and authorization contract for interactive authentications. The way to add role assignments for an Agent Identity Blueprint is via Access Packages (Add Applications). Requiring explicit role assignment for individual agent identities makes sense in an autonomous agent-to-agent interactions where you want strict controls over the autonomous invokation of AI Agents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App-only roles:&lt;/strong&gt; App-only roles are defined on the Blueprint and can be assigned to other agents or clients. When assigned to other workloads, these roles appear in the access token with the Blueprint as the audience (&lt;code&gt;aud&lt;/code&gt; claim). This allows fine-grained authorization delegation between agents.&lt;/p&gt;

&lt;p&gt;The important detail is that &lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-protected-web-api-app-configuration" rel="noopener noreferrer"&gt;your protected API must be configured to emit roles in the access token&lt;/a&gt;. This requires a specific configuration step. Many teams forget it. The app roles exist, the assignment exists, but the token arrives empty because the app registration does not emit roles into tokens meant for APIs.&lt;/p&gt;

&lt;p&gt;Also note: app roles are defined per app registration, not globally. If you have two APIs, each needs its own set of roles. There is no central app role store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Group membership claims as attribute-based gates
&lt;/h2&gt;

&lt;p&gt;Group membership claims are different from app roles. When you &lt;a href="https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-autonomous-agents" rel="noopener noreferrer"&gt;emit group claims&lt;/a&gt;, Entra places the groups that the principal belongs to directly into the token.&lt;/p&gt;

&lt;p&gt;Here is the distinction. App roles are defined by the application and assigned by Entra admins within the scope of that app registration. Group claims are based on the broader Entra directory. The principal is a member of the group regardless of the app. The app just asks: "What groups is this principal a member of?" Entra answers by listing them in the token.&lt;/p&gt;

&lt;p&gt;This is useful for attribute-based access control (ABAC). Imagine your organization has groups like &lt;code&gt;Sales-Americas&lt;/code&gt;, &lt;code&gt;Sales-EMEA&lt;/code&gt;, &lt;code&gt;Engineering&lt;/code&gt;, and &lt;code&gt;Finance&lt;/code&gt;. You assign users and workload identities to these groups. Your API receives a token, extracts the &lt;code&gt;groups&lt;/code&gt; claim, and makes decisions based on group membership. "If the token contains the &lt;code&gt;Sales-Americas&lt;/code&gt; group, grant access to the Americas sales data. Otherwise, deny."&lt;/p&gt;

&lt;p&gt;The value of group-based authorization is that it decouples application-specific roles from organizational structure. If you need to add a new group or restructure existing groups, you do not need to change app role definitions. You adjust group membership. The group claim mechanism is still sending the data; your API just interprets it differently.&lt;/p&gt;

&lt;p&gt;There is a configuration step here too. You must tell Entra to emit group claims into tokens for a specific app. By default, groups are not included. Once enabled, Entra sends the group IDs (and optionally display names, but prefer IDs for immutability).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important: Overage limits and scope configuration.&lt;/strong&gt; When you enable group claims, you must configure scope carefully. Entra has fixed limits on how many group claims can fit in a token:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OIDC (OpenID Connect):&lt;/strong&gt; 200 groups maximum&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SAML:&lt;/strong&gt; 150 groups maximum&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a principal belongs to more groups than the limit, Entra sends an "overage claim" instead of individual group IDs. Your API then needs to call Microsoft Graph to fetch the full list. This adds latency and complexity. To avoid overage, &lt;strong&gt;emit only "assigned groups"&lt;/strong&gt; (groups explicitly assigned to the application / blueprint). Be explicit about which groups matter for authorization decisions, and assign only those groups to the target app or agent identity blueprint. This keeps tokens small, predictable, and avoids the overage claim penalty.&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://learn.microsoft.com/en-us/security/zero-trust/develop/configure-tokens-group-claims-app-roles" rel="noopener noreferrer"&gt;Configure group claims and app roles in tokens&lt;/a&gt; for detailed guidance on group claim scope and immutable identifiers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Access packages as the unified control plane
&lt;/h2&gt;

&lt;p&gt;Access packages are where the other mechanisms converge. An &lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/governance/access-packages-create-lifecycle-policies" rel="noopener noreferrer"&gt;access package&lt;/a&gt; is not just governance UI. It is programmable policy choreography. When you assign a principal to an access package, you can configure what happens automatically: group membership additions, app role assignments, Azure role assignments, and so on. All of it coordinated, all of it time-bounded, all of it auditable.&lt;/p&gt;

&lt;p&gt;For an AI agent, access packages are a control plane. You define an access package called "Agent-Production-Access". When you assign an agent's service principal to that package, Entra automatically adds the service principal to the groups that the agent needs, assigns the correct app roles to the agent, and even delegated permissions grants (consent) for downstream APIs and MCP Servers. When the access expires, all of it unwinds. The groups are removed. The app roles are stripped, the consent is undone. The agent loses access.&lt;/p&gt;

&lt;p&gt;This is different from manually assigning groups and roles one by one or going through admin cosnent workflow. With access packages, you are declaring a coherent policy: "This is what production access means for this type of workload. If the workload is approved, give it everything in this package. If it is denied or if the time expires, take everything away." The access package is a unit of authorization. It is atomic in the sense that you do not pick and choose which parts of it to apply. It is an all-or-nothing proposition.&lt;/p&gt;

&lt;p&gt;For enterprises running multiple AI agents, access packages scale the authorization story. You create one access package per authorization context (production, staging, read-only partners, etc.). You assign agents to packages based on their needs. Governance reviews the packages on a schedule and approves or denies. The machinery runs itself.&lt;/p&gt;

&lt;p&gt;There is also integration with approval workflows. An access package can require approval before assignment. You can route approvals to a specific manager or group. You can set expiration dates and schedule re-approval. For AI agents running in production, this is the audit trail that compliance teams expect. "Show me who approved this agent to have access to production data and when that approval expires."&lt;/p&gt;

&lt;h2&gt;
  
  
  Best practices on group emit scope and immutable identifiers
&lt;/h2&gt;

&lt;p&gt;When you enable group claims, you need to decide: should Entra emit &lt;em&gt;all&lt;/em&gt; groups the principal is a member of, or only &lt;em&gt;assigned&lt;/em&gt; groups?&lt;/p&gt;

&lt;p&gt;The answer is clear: &lt;strong&gt;use only groups assigned to the application&lt;/strong&gt;. In a typical enterprise environment, grown over the yers, single user can be member of several thousands of groups. You cannot have a token with six thousand groups in it. And it makes no sense to go over six thousand groups to make authorization decision. Choosing "assigned groups" is tighter and mandatory for production. You explicitly assign the groups relevant for this app to that app. Other groups the principal happens to belong to stay out of the token. This keeps tokens predictable, avoids overage claims, and simplifies authorization logic.&lt;/p&gt;

&lt;p&gt;Immutable identifiers are a separate but equally important consideration. Entra can emit group display names or group IDs in the token. Display names are human-readable but fragile. If a group is renamed, the token still carries the old display name until the token expires. Your API might silently fail to recognize the group and deny access. Or worse - someone may create a group with name that they will grant them access to the app. Group IDs are immutable. A group ID never changes, even if the display name changes. For production authorization logic, prefer group IDs. Your API logic becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if token.groups contains "80d61f4e-4c9e-4f8f-9e1f-2e8f3d5c6b7a":
  grant access
else:
  deny
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if token.groups contains "Sales-Americas":
  grant access
else:
  deny
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first version survives a group rename. The second does not. For large organizations managing many agents and APIs, using immutable group IDs and assigned-only scope is the only way to keep authorization logic maintainable and reliable at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Composing the authorization stack
&lt;/h2&gt;

&lt;p&gt;These mechanisms do not compete. They stack. Here is a realistic composition:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://docs.azure.cn/en-us/entra/identity/enterprise-apps/what-is-access-management#requiring-user-assignment-for-an-app" rel="noopener noreferrer"&gt;Requiring explicit assignment&lt;/a&gt; gates whether the principal can even attempt authentication.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview" rel="noopener noreferrer"&gt;Conditional access policies&lt;/a&gt; check location, device, and other context.&lt;/li&gt;
&lt;li&gt;The token arrives with &lt;a href="https://learn.microsoft.com/en-us/entra/identity-platform/howto-add-app-roles-in-apps" rel="noopener noreferrer"&gt;app roles&lt;/a&gt; and &lt;a href="https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-fed-group-claims" rel="noopener noreferrer"&gt;group claims&lt;/a&gt; embedded.&lt;/li&gt;
&lt;li&gt;Your API inspects the token, checks app roles and groups, and makes fine-grained decisions.&lt;/li&gt;
&lt;li&gt;All of this is orchestrated through an &lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/agent-access-packages" rel="noopener noreferrer"&gt;access package&lt;/a&gt; that defines the coherent policy for a given context (production, partner, read-only, etc.).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each layer has a different trust surface. User assignment is coarse. Conditional access is context-based. App roles and groups are attribute-based. Access packages are policy-coordinated. Together, they form a defense in depth.&lt;/p&gt;

&lt;p&gt;When you are designing authorization for an AI agent, ask yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should this agent be able to authenticate at all right now? (User assignment)&lt;/li&gt;
&lt;li&gt;Under what conditions (location, device, time) should token acquisition be allowed? (Conditional access)&lt;/li&gt;
&lt;li&gt;If the token is issued, what roles and group memberships should it carry? (App roles and group claims)&lt;/li&gt;
&lt;li&gt;How is the agent's access provisioned, reviewed, and revoked? (Access packages and lifecycle management)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can answer each question with a specific, enforceable policy, you have authorization architecture. If you are relying on the API layer alone to deny requests, you are carrying unnecessary risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/assign-user-or-group-access-portal" rel="noopener noreferrer"&gt;User assignment to enterprise applications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/identity/conditional-access/agent-id" rel="noopener noreferrer"&gt;Conditional Access for Agents in Microsoft Entra&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-autonomous-agents?tabs=use-the-enhanced-object-picker" rel="noopener noreferrer"&gt;Recommended policies for autonomous agents in Microsoft Entra&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-apps" rel="noopener noreferrer"&gt;How to add app roles in apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/identity-platform/reference-app-manifest" rel="noopener noreferrer"&gt;Understanding the app manifest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-protected-web-api-app-configuration" rel="noopener noreferrer"&gt;Protected Web API app configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/security/zero-trust/develop/configure-tokens-group-claims-app-roles" rel="noopener noreferrer"&gt;Configure group claims and app roles in tokens&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/develop/how-to-emit-saml-claims-based-on-application-roles" rel="noopener noreferrer"&gt;How to emit SAML claims based on application roles&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>entraagentid</category>
      <category>identitygovernance</category>
      <category>microsoftentra</category>
      <category>aiagents</category>
    </item>
    <item>
      <title>Flexibility with admin consent in Entra: how to scale consent flows for AI agents</title>
      <dc:creator>Anton Staykov</dc:creator>
      <pubDate>Tue, 25 Aug 2026 11:43:00 +0000</pubDate>
      <link>https://dev.to/astaykov/flexibility-with-admin-consent-in-entra-how-to-scale-consent-flows-for-ai-agents-2h1p</link>
      <guid>https://dev.to/astaykov/flexibility-with-admin-consent-in-entra-how-to-scale-consent-flows-for-ai-agents-2h1p</guid>
      <description>&lt;p&gt;Most organizations treat tenant-wide admin consent in Microsoft Entra as a binary decision. Either a central identity team approves every request, or somebody receives a broad administrative role so the queue can move faster.&lt;/p&gt;

&lt;p&gt;That is a false choice.&lt;/p&gt;

&lt;p&gt;Microsoft Entra supports a more precise control model: define which consent requests are acceptable in an app consent policy, bind that policy to a custom directory role, and delegate only the authority to approve requests inside that envelope. The role action that makes this possible is &lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/custom-consent-permissions#granting-permissions-to-apps-on-behalf-of-all-admin-consent" rel="noopener noreferrer"&gt;&lt;code&gt;microsoft.directory/servicePrincipals/managePermissionGrantsForAll.{id}&lt;/code&gt;&lt;/a&gt;, where &lt;code&gt;{id}&lt;/code&gt; identifies the policy that constrains the consent authority.&lt;/p&gt;

&lt;p&gt;That is not a smaller version of Global Administrator. It is a different architectural idea: &lt;em&gt;delegate the decision without delegating the directory&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;AI agents make this distinction urgent. As organizations create more agent identities and authorize them against Microsoft Graph and other APIs, consent stops being an occasional setup task. It becomes a recurring control-plane operation. Scaling that operation with broad roles is the identity equivalent of solving traffic congestion by issuing everyone a master key.&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual admin consent model does not scale
&lt;/h2&gt;

&lt;p&gt;Tenant-wide admin consent is deliberately sensitive. A grant can authorize an application to access organizational data or perform privileged operations across the tenant, so Microsoft recommends reviewing the requested permissions carefully before granting it (&lt;a href="https://learn.microsoft.com/entra/identity/enterprise-apps/grant-admin-consent" rel="noopener noreferrer"&gt;grant tenant-wide admin consent&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The operational response is usually centralization. Every request lands with a small identity or security team. That team validates the application, checks the requested permissions, finds the business owner, and either approves or rejects the request.&lt;/p&gt;

&lt;p&gt;Centralization gives you consistency, but it also creates a queue. The queue grows with every SaaS integration, internal application, automation workload, and AI agent. Eventually the organization reaches for a built-in role such as Application Administrator or Cloud Application Administrator to distribute the work.&lt;/p&gt;

&lt;p&gt;That relieves the queue by expanding authority. It does not make the consent decision more precise.&lt;/p&gt;

&lt;p&gt;Microsoft Entra already has a control surface for that precision. It is just underused.&lt;/p&gt;

&lt;h2&gt;
  
  
  An app consent policy is a permission envelope
&lt;/h2&gt;

&lt;p&gt;An &lt;a href="https://learn.microsoft.com/entra/identity/enterprise-apps/manage-app-consent-policies" rel="noopener noreferrer"&gt;app consent policy&lt;/a&gt; describes the conditions under which a consent event is allowed. A policy contains one or more include condition sets and can contain exclude condition sets. A request must match at least one include condition set and must not match any exclude condition set for the policy to pass.&lt;/p&gt;

&lt;p&gt;That evaluation model matters. You are not handing a delegate a list of instructions and hoping they follow it. You are placing a policy boundary in the authorization path.&lt;/p&gt;

&lt;p&gt;The condition sets can describe characteristics such as the permission type, the resource application, specific permissions, client applications, and publisher status (&lt;a href="https://learn.microsoft.com/graph/api/resources/permissiongrantconditionset?view=graph-rest-1.0" rel="noopener noreferrer"&gt;permission grant condition set resource&lt;/a&gt;). This lets an organization express consent envelopes such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;delegated &lt;code&gt;Mail.Read&lt;/code&gt; and &lt;code&gt;Files.Read&lt;/code&gt; permissions for Microsoft Graph,&lt;/li&gt;
&lt;li&gt;selected application permissions exposed by one internal finance API,&lt;/li&gt;
&lt;li&gt;permissions requested only by explicitly identified client applications,&lt;/li&gt;
&lt;li&gt;delegated permissions from verified publishers, with sensitive resources excluded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important word is &lt;em&gt;selected&lt;/em&gt;. The policy does not need to authorize every permission exposed by an API. It can describe the exact delegated scopes and application roles the organization has decided are appropriate for this delegation model (&lt;a href="https://learn.microsoft.com/entra/identity/enterprise-apps/manage-app-consent-policies#app-consent-policy-segments" rel="noopener noreferrer"&gt;manage app consent policies&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Today, Microsoft exposes this policy model through Microsoft Graph. The relevant resources are &lt;code&gt;permissionGrantPolicy&lt;/code&gt; and &lt;code&gt;permissionGrantConditionSet&lt;/code&gt;, exposed under &lt;code&gt;/policies/permissionGrantPolicies&lt;/code&gt; (&lt;a href="https://learn.microsoft.com/entra/identity/enterprise-apps/manage-app-consent-policies#manage-app-consent-policies" rel="noopener noreferrer"&gt;manage app consent policies with Microsoft Graph&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The custom role binds authority to the envelope
&lt;/h2&gt;

&lt;p&gt;The policy defines what may be approved. The custom role defines who may approve it.&lt;/p&gt;

&lt;p&gt;Microsoft documents a specific custom-role permission for tenant-wide admin consent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;microsoft.directory/servicePrincipals/managePermissionGrantsForAll.{id}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;{id}&lt;/code&gt; suffix is not decoration. It binds the role permission to an app consent policy. The role assignee can grant tenant-wide consent for delegated permissions and application permissions only when the consent request satisfies that policy (&lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/custom-consent-permissions#granting-permissions-to-apps-on-behalf-of-all-admin-consent" rel="noopener noreferrer"&gt;app consent permissions for custom roles&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That gives architects a much better delegation primitive than "make this person a Cloud Application Administrator." The delegate receives a defined consent capability, not general application administration authority.&lt;/p&gt;

&lt;p&gt;For example, a data platform team could receive authority to grant a fixed set of delegated scopes against an internal analytics API. A Microsoft 365 integration team could receive a different role tied to a policy for a small set of approved Graph permissions. Neither team needs a role whose authority extends across unrelated application-management operations.&lt;/p&gt;

&lt;p&gt;Custom roles and app consent permissions require Microsoft Entra ID P1. The Entra admin center does not currently support adding these app-consent permissions to a custom role definition, so this configuration is available through Microsoft Graph rather than the Entra admin center UX (&lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/custom-consent-permissions" rel="noopener noreferrer"&gt;license and tooling requirements&lt;/a&gt;). That is implementation friction, but it does not weaken the control model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Policy authorship and consent execution are different jobs
&lt;/h2&gt;

&lt;p&gt;The strongest version of this architecture separates two responsibilities.&lt;/p&gt;

&lt;p&gt;The central identity or security team authors the permission envelope. It decides which resource applications, delegated scopes, application roles, client properties, and exclusions belong in the policy. That team also controls the custom role definition and its assignments through the documented Microsoft Entra role-management interfaces (&lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/custom-create" rel="noopener noreferrer"&gt;create a custom role&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The delegated administrator evaluates individual requests and grants consent only when Entra determines that the request fits the referenced policy.&lt;/p&gt;

&lt;p&gt;Those are not the same privilege. Microsoft exposes separate custom-role permissions for creating, reading, updating, and deleting permission grant policies, distinct from &lt;code&gt;managePermissionGrantsForAll.{id}&lt;/code&gt; (&lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/custom-consent-permissions#managing-app-consent-policies" rel="noopener noreferrer"&gt;managing app consent policies through custom roles&lt;/a&gt;). A consent delegate should not automatically receive policy-authoring permissions.&lt;/p&gt;

&lt;p&gt;That separation is the control.&lt;/p&gt;

&lt;p&gt;If the same operator can widen the allowlist and approve the newly allowed request, the policy becomes a speed bump. If one team defines the envelope and another operates inside it, the policy becomes an enforceable delegation boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch for alternate paths around the policy
&lt;/h2&gt;

&lt;p&gt;There is one architectural trap worth stating plainly: app consent policies are not the only authorization mechanism that can allow a principal to grant permissions.&lt;/p&gt;

&lt;p&gt;Microsoft documents that policies are evaluated independently and that a principal needs only one authorization path that permits a consent event. Ownership of a resource service principal and broad Microsoft Graph application permissions can provide other ways to create grants in some scenarios (&lt;a href="https://learn.microsoft.com/entra/identity/enterprise-apps/manage-app-consent-policies#multiple-policies-or-authorization-mechanisms-to-grant-consent" rel="noopener noreferrer"&gt;multiple policies or authorization mechanisms&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That means the restricted role is only as restricted as the delegate's &lt;em&gt;effective&lt;/em&gt; authority.&lt;/p&gt;

&lt;p&gt;Do not combine the policy-bound consent role with broad application-management permissions and then describe the result as least privilege. Review the delegate's other directory roles, Microsoft Graph permissions, ownership assignments, and group-derived access. The consent policy should be the intended authorization path, not one path among several wider ones.&lt;/p&gt;

&lt;p&gt;This is also why role naming matters. "Finance API Consent Approver" communicates a bounded function. "Application Admin Lite" invites future privilege creep. Names do not enforce security, but they tell future operators whether a role is a contract or a convenience.&lt;/p&gt;

&lt;h2&gt;
  
  
  A domain-based consent operating model
&lt;/h2&gt;

&lt;p&gt;The practical enterprise model is not one policy for every application and not one policy for the entire tenant. Both extremes create maintenance problems.&lt;/p&gt;

&lt;p&gt;Define envelopes around stable trust boundaries. A boundary might be a business domain, an API platform, a class of low-impact delegated permissions, or a controlled set of client applications. The condition-set model supports matching on multiple properties, so the policy can reflect more than a permission name alone (&lt;a href="https://learn.microsoft.com/graph/api/resources/permissiongrantconditionset?view=graph-rest-1.0" rel="noopener noreferrer"&gt;permission grant condition set properties&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Then assign the corresponding custom role to administrators who understand that domain. They can evaluate business purpose and operational context without receiving unrelated directory authority.&lt;/p&gt;

&lt;p&gt;This changes the central identity team's job. It stops being the human endpoint for every consent request and becomes the designer of consent boundaries. The team owns policy quality, role lifecycle, exception handling, and periodic review. Domain administrators operate within the encoded boundary.&lt;/p&gt;

&lt;p&gt;The result is not decentralized consent without governance. It is &lt;em&gt;federated consent with a centrally defined authorization ceiling&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI agents turn a useful feature into an architectural requirement
&lt;/h2&gt;

&lt;p&gt;Microsoft Entra Agent ID supports delegated and application permission models for agents. Delegated authorization is represented by an &lt;code&gt;oAuth2PermissionGrant&lt;/code&gt;, while application authorization is represented by an &lt;code&gt;appRoleAssignment&lt;/code&gt;; Microsoft also documents direct creation of these objects and consent-based flows for granting agents access (&lt;a href="https://learn.microsoft.com/entra/agent-id/grant-agent-access-microsoft-365" rel="noopener noreferrer"&gt;grant agents access to Microsoft 365 resources&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That means agent adoption feeds the same consent control plane already used for applications, but at a different operational scale. A blueprint may produce many agent identities, and different agents may need different access as they move from experimentation into production (&lt;a href="https://learn.microsoft.com/entra/agent-id/key-concepts" rel="noopener noreferrer"&gt;Agent ID key concepts&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The wrong response is to multiply broadly privileged administrators at the same rate.&lt;/p&gt;

&lt;p&gt;The better response is to decide which permission combinations are routine, defensible, and delegable, then encode those combinations in consent policies. High-risk or unusual requests stay with the central authority. Known patterns move through policy-bound delegates.&lt;/p&gt;

&lt;p&gt;This complements the argument in &lt;a href="https://dev.to/astaykov/why-consent-is-the-trust-contract-for-entra-agent-id-c03"&gt;Why consent is the trust contract for Entra Agent ID&lt;/a&gt;. Consent still answers who authorized an agent, for which resource, and with what permissions. Granular delegation answers the next operational question: who is trusted to make that authorization decision without receiving power over the rest of the tenant?&lt;/p&gt;

&lt;p&gt;There is an important current boundary. Microsoft states that custom Microsoft Entra roles cannot be assigned to agent identities (&lt;a href="https://learn.microsoft.com/entra/agent-id/authorization-agent-id#microsoft-entra-role-assignments-for-agent-identities" rel="noopener noreferrer"&gt;Microsoft Entra role assignments for agent identities&lt;/a&gt;). This pattern therefore does not make an AI agent the holder of the restricted consent role. It makes the consent process around AI agents more scalable for supported human role assignees.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question this architecture creates
&lt;/h2&gt;

&lt;p&gt;Once consent policy becomes an enforceable envelope, a more interesting question appears.&lt;/p&gt;

&lt;p&gt;What if an AI agent could assess a consent request, inspect the requested permissions and business context, compare the request with organizational policy, and recommend approval or rejection? What if, one day, the platform supported that agent making the decision inside a boundary it could not rewrite?&lt;/p&gt;

&lt;p&gt;That is not the capability described here. Current Agent IDs cannot hold custom Entra roles, and a probabilistic reasoning system should not be mistaken for an authorization boundary (&lt;a href="https://learn.microsoft.com/entra/agent-id/authorization-agent-id" rel="noopener noreferrer"&gt;Agent ID authorization restrictions&lt;/a&gt;). But granular consent delegation gives that future discussion a serious starting point. The policy remains deterministic. The permission ceiling remains centrally owned. The assessment could become intelligent.&lt;/p&gt;

&lt;p&gt;The immediate opportunity is less speculative and more valuable: stop treating admin consent as authority that can only be centralized or broadly delegated. Microsoft Entra already lets you define the exact consent envelope and hand out only the key that fits that lock.&lt;/p&gt;

&lt;p&gt;As AI agents increase the volume and variety of consent decisions, that flexibility stops being an obscure role-management feature. It becomes part of the architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity/enterprise-apps/manage-app-consent-policies" rel="noopener noreferrer"&gt;Manage app consent policies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/graph/api/resources/permissiongrantconditionset?view=graph-rest-1.0" rel="noopener noreferrer"&gt;Permission grant condition set resource type&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/custom-consent-permissions" rel="noopener noreferrer"&gt;App consent permissions for custom roles in Microsoft Entra ID&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/custom-create" rel="noopener noreferrer"&gt;Create a custom role in Microsoft Entra ID&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity/enterprise-apps/grant-admin-consent" rel="noopener noreferrer"&gt;Grant tenant-wide admin consent to an application&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/graph/permissions-grant-via-msgraph" rel="noopener noreferrer"&gt;Grant or revoke API permissions programmatically&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/agent-id/authorization-agent-id" rel="noopener noreferrer"&gt;Authorization in Microsoft Entra Agent ID&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/agent-id/grant-agent-access-microsoft-365" rel="noopener noreferrer"&gt;Grant agents access to Microsoft 365 resources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/agent-id/key-concepts" rel="noopener noreferrer"&gt;Microsoft Entra Agent ID key concepts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aiagents</category>
      <category>zerotrust</category>
      <category>microsoftentra</category>
      <category>cloudsecurity</category>
    </item>
    <item>
      <title>Dynamic client registration and CIMD are not a trust framework</title>
      <dc:creator>Anton Staykov</dc:creator>
      <pubDate>Mon, 17 Aug 2026 10:37:00 +0000</pubDate>
      <link>https://dev.to/astaykov/dynamic-client-registration-and-cimd-are-not-a-trust-framework-fh7</link>
      <guid>https://dev.to/astaykov/dynamic-client-registration-and-cimd-are-not-a-trust-framework-fh7</guid>
      <description>&lt;p&gt;The MCP specification ships with a strong opinion about how AI clients should onboard with authorization servers: dynamically, at runtime, without pre-coordination (&lt;a href="https://modelcontextprotocol.io/specification/latest/basic/authorization" rel="noopener noreferrer"&gt;MCP authorization spec&lt;/a&gt;). The AI community has largely nodded along. Enterprise security teams have largely not been consulted.&lt;/p&gt;

&lt;p&gt;That mismatch is the subject of this article.&lt;/p&gt;

&lt;p&gt;DCR and CIMD are genuinely useful protocol mechanisms. They are not a trust framework. And in an era where supply chain attacks are the dominant enterprise threat vector, confusing the two is a governance failure waiting for a specific client name to trigger it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What DCR was designed to remove
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7591" rel="noopener noreferrer"&gt;RFC 7591 - OAuth 2.0 Dynamic Client Registration Protocol&lt;/a&gt; solves a real problem. The traditional OAuth onboarding sequence requires a developer to pre-register their client application with the authorization server, receive a &lt;code&gt;client_id&lt;/code&gt; and &lt;code&gt;client_secret&lt;/code&gt;, configure redirect URIs, and coordinate with whoever manages the identity provider before a single token can flow. For SaaS products, open developer ecosystems, and multi-tenant platforms, this ceremony is expensive. Teams wait days for provisioning. Integration partners hit identity-team backlogs. CI pipelines stall on human approvals.&lt;/p&gt;

&lt;p&gt;DCR automates the ceremony. The client POSTs a JSON body describing itself to the authorization server's &lt;code&gt;registration_endpoint&lt;/code&gt;. The server returns a &lt;code&gt;client_id&lt;/code&gt; and &lt;code&gt;client_secret&lt;/code&gt; immediately. No human approval. No pre-coordination. Developer velocity restored.&lt;/p&gt;

&lt;p&gt;That is a genuine improvement in the right context. Public MCP servers, open developer tooling, and hobbyist integrations benefit from it directly. The friction reduction is real, and dismissing it is wrong.&lt;/p&gt;

&lt;p&gt;The problem is not the protocol. The problem is the assumption that registration equals authorization, and that authorization equals trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CIMD adds to the picture
&lt;/h2&gt;

&lt;p&gt;Client Information Meta Data (CIMD) refers to the structured metadata fields a client submits in the DCR request, and through &lt;a href="https://datatracker.ietf.org/doc/html/rfc7592" rel="noopener noreferrer"&gt;RFC 7592 (Dynamic Client Registration Management)&lt;/a&gt;, the management endpoint that lets a registered client later read back and update its own registration record.&lt;/p&gt;

&lt;p&gt;The metadata fields are richer than they appear at first. A client can declare its &lt;code&gt;client_name&lt;/code&gt;, &lt;code&gt;client_uri&lt;/code&gt;, &lt;code&gt;redirect_uris&lt;/code&gt;, &lt;code&gt;grant_types&lt;/code&gt;, &lt;code&gt;scope&lt;/code&gt;, &lt;code&gt;logo_uri&lt;/code&gt;, and most interestingly a &lt;code&gt;software_statement&lt;/code&gt; (&lt;a href="https://datatracker.ietf.org/doc/html/rfc7591#section-2.3" rel="noopener noreferrer"&gt;RFC 7591 §2.3&lt;/a&gt;). A software statement is a signed JWT issued by a third party the authorization server trusts, asserting facts about the client software: who built it, what version it is, what it is intended to do. It is the closest thing RFC 7591 has to attestation.&lt;/p&gt;

&lt;p&gt;CIMD improves signal quality. It gives authorization servers structured, machine-readable metadata to work with. It makes client inventory possible in principle: if you log registrations, you at least know the name each client claimed. Software statements, when an authorization server validates them against a trusted issuer, move the model from "self-asserted identity" to "third-party asserted identity."&lt;/p&gt;

&lt;p&gt;The critical word is &lt;em&gt;when&lt;/em&gt;. Software statement validation is optional in RFC 7591. Most MCP server implementations do not enforce it. A client that omits the &lt;code&gt;software_statement&lt;/code&gt; field registers exactly as successfully as one that includes it. The registration endpoint cannot distinguish a vetted corporate tool from a novel command-line script someone assembled over a weekend and pointed at your server.&lt;/p&gt;

&lt;p&gt;CIMD raises the quality of information available at registration time. It does not answer whether your organization approved this client to exist here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The enterprise security gap
&lt;/h2&gt;

&lt;p&gt;Security teams in regulated industries (financial services, healthcare, critical infrastructure, government) operate on a fundamentally different assumption about software trust than DCR's default model. They run approved-software programs. Patch management is gated on testing and change-board approval. In the most security-mature environments I have worked in across thousands of enterprise security conversations, even Windows Updates are staged, validated, and controlled before rollout to the fleet.&lt;/p&gt;

&lt;p&gt;This is not bureaucratic paranoia. It is the appropriate operational posture given how supply chain attacks actually work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.cisa.gov/news-events/news/cisa-insights-solarwinds" rel="noopener noreferrer"&gt;SolarWinds&lt;/a&gt; demonstrated that a trusted vendor's signed software update is a viable attack vehicle. &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2024-3094" rel="noopener noreferrer"&gt;CVE-2024-3094 (XZ Utils)&lt;/a&gt; showed that a two-year social engineering campaign can insert a backdoor into a compression library that ships inside essentially every Linux distribution. The lesson from both is not "trust nothing." The lesson is that &lt;em&gt;how you establish and revoke software trust matters as much as whether a signature is present&lt;/em&gt;, and that self-asserted identity for software is precisely the attack surface adversaries aim for.&lt;/p&gt;

&lt;p&gt;Against that backdrop, consider what unrestricted DCR on an enterprise MCP server looks like. Any AI client (any command-line tool, any agentic orchestrator, any LLM-driven system) that discovers the server's &lt;code&gt;registration_endpoint&lt;/code&gt; can register itself, receive credentials, and start calling your server's tools. The server knows the client's claimed &lt;code&gt;client_name&lt;/code&gt;. It does not know whether your security team reviewed the client's code, whether the vendor passed your procurement process, whether the version running is the one that was reviewed, or whether the registration request is originating from inside your network perimeter or from an endpoint that was compromised last week.&lt;/p&gt;

&lt;p&gt;The SOC analyst's question is not theoretical: "Show me every MCP client registered on this server, who approved each one, under which policy, and when that approval was granted." Unrestricted DCR makes that question unanswerable, or worse, the answer is "anyone who sent a POST to the right endpoint."&lt;/p&gt;

&lt;p&gt;Small organizations face the same question. The compliance regime may be less formal, but the threat is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Entra's deliberate answer: explicit registration as a trust decision
&lt;/h2&gt;

&lt;p&gt;This is where the AI community tends to frame a gap. It is not a gap. It is architecture.&lt;/p&gt;

&lt;p&gt;Microsoft Entra ID has no &lt;a href="https://datatracker.ietf.org/doc/html/rfc7591" rel="noopener noreferrer"&gt;RFC 7591&lt;/a&gt; &lt;code&gt;registration_endpoint&lt;/code&gt;. There is no URL on &lt;code&gt;login.microsoftonline.com&lt;/code&gt; that accepts a DCR POST and returns &lt;code&gt;client_id&lt;/code&gt; credentials at runtime. Every application that can receive Entra-issued tokens must be explicitly registered, through the Azure portal, the Microsoft Graph API, the Azure CLI, or ARM templates, by a principal holding at minimum the &lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/permissions-reference#application-developer" rel="noopener noreferrer"&gt;Application Developer role&lt;/a&gt; (&lt;a href="https://learn.microsoft.com/entra/identity-platform/quickstart-register-app" rel="noopener noreferrer"&gt;register an application in Microsoft Entra ID&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That deliberate registration is not the end of the trust chain. It is the beginning of it. A registered application that has not been explicitly consented still receives no tokens. Consent in Entra is the authorization gate: the act that says "this application is authorized to call this API, with these permissions, under this policy." A well-named AI client with a convincing &lt;code&gt;client_name&lt;/code&gt; and a polished &lt;code&gt;client_uri&lt;/code&gt; gets nothing from Entra without a human trust decision in the chain. (For the full governance architecture behind this claim, see the article &lt;a href="https://dev.to/astaykov/why-consent-is-the-trust-contract-for-entra-agent-id-c03"&gt;Why consent is the trust contract for Entra Agent ID&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://modelcontextprotocol.io/specification/latest/basic/authorization" rel="noopener noreferrer"&gt;MCP authorization specification&lt;/a&gt; is careful here: it states that clients SHOULD attempt DCR when the authorization server's &lt;a href="https://datatracker.ietf.org/doc/html/rfc8414" rel="noopener noreferrer"&gt;RFC 8414&lt;/a&gt; metadata includes a &lt;code&gt;registration_endpoint&lt;/code&gt;. "Should," not "must." A well-behaved MCP client encountering an authorization server that publishes no &lt;code&gt;registration_endpoint&lt;/code&gt; (which is what every Entra tenant presents) falls back to static registration. That fallback is the intended behavior. The friction is intentional, and its source is a governance posture, not a missing feature.&lt;/p&gt;

&lt;p&gt;The honest tradeoff: Entra's model maximizes governance precision and eliminates runtime surprise about who can request tokens. It creates real friction for MCP clients that expect to self-register on first contact. That friction is a feature for regulated enterprise deployments. It is a genuine obstacle for open, developer-facing MCP servers where the audience is trusted developers and the stakes per registration are low. Both statements are true simultaneously.&lt;/p&gt;

&lt;h2&gt;
  
  
  APIM AI Gateway: the bridge, and where the gap remains
&lt;/h2&gt;

&lt;p&gt;When you place &lt;a href="https://learn.microsoft.com/azure/api-management/genai-gateway-capabilities" rel="noopener noreferrer"&gt;Azure API Management's AI Gateway&lt;/a&gt; in front of your MCP server, you gain a set of controls that together approach what a governance-controlled DCR layer would look like, without implementing one. Understanding exactly what APIM provides, and what it does not, is what allows you to design the right architecture rather than assume a gap has been closed when it has not.&lt;/p&gt;

&lt;p&gt;The first piece is discovery integration. APIM can expose the &lt;code&gt;.well-known/oauth-protected-resource&lt;/code&gt; endpoint defined in &lt;a href="https://datatracker.ietf.org/doc/html/rfc9728" rel="noopener noreferrer"&gt;RFC 9728&lt;/a&gt;, which tells DCR-capable MCP clients where to find the authorization server protecting this resource (&lt;a href="https://learn.microsoft.com/azure/api-management/secure-mcp-servers" rel="noopener noreferrer"&gt;secure access to MCP servers&lt;/a&gt;). When APIM points to Entra as the authorization server, the MCP client reaches Entra, discovers no &lt;code&gt;registration_endpoint&lt;/code&gt; in Entra's RFC 8414 metadata, and falls back to static registration. The discovery flow runs correctly. The governance gate holds. APIM participates in the DCR protocol dance without becoming a DCR endpoint.&lt;/p&gt;

&lt;p&gt;The second piece is runtime client enforcement via the &lt;a href="https://learn.microsoft.com/azure/api-management/validate-azure-ad-token-policy" rel="noopener noreferrer"&gt;&lt;code&gt;validate-azure-ad-token&lt;/code&gt; policy&lt;/a&gt;. This policy accepts a &lt;code&gt;&amp;lt;client-application-ids&amp;gt;&lt;/code&gt; block, an explicit allowlist of Entra application IDs whose tokens are valid at this gateway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;validate-azure-ad-token&lt;/span&gt; &lt;span class="na"&gt;tenant-id=&lt;/span&gt;&lt;span class="s"&gt;"your-entra-tenant-id"&lt;/span&gt;
    &lt;span class="na"&gt;header-name=&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;
    &lt;span class="na"&gt;failed-validation-httpcode=&lt;/span&gt;&lt;span class="s"&gt;"401"&lt;/span&gt;
    &lt;span class="na"&gt;failed-validation-error-message=&lt;/span&gt;&lt;span class="s"&gt;"Unauthorized. Access token is missing or invalid."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;client-application-ids&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;application-id&amp;gt;&lt;/span&gt;approved-mcp-client-app-id&lt;span class="nt"&gt;&amp;lt;/application-id&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/client-application-ids&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/validate-azure-ad-token&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A client that was registered in Entra and consented (both explicit human decisions), but whose application ID is absent from this list, still cannot pass the gateway. That is two distinct human approval steps between "an AI client exists" and "that AI client can invoke your MCP server tools." This is the runtime equivalent of an enterprise approved-software list, enforced at the gateway rather than in application code.&lt;/p&gt;

&lt;p&gt;For scenarios where Entra registration still introduces too much friction (third-party client integrations, short-lived automation, partner tooling), APIM's subscription key model is the highest-control alternative. Every MCP client consumer receives a key provisioned explicitly by an APIM administrator. No dynamic discovery. No self-registration. Full explicit provisioning per consumer, with gateway-level revocation that does not wait for a consent withdrawal to propagate through the identity layer.&lt;/p&gt;

&lt;p&gt;The observability dimension is underweighted in most governance architecture conversations. APIM routes all MCP tool invocations through a single plane with &lt;a href="https://learn.microsoft.com/azure/api-management/api-management-howto-app-insights" rel="noopener noreferrer"&gt;Application Insights integration&lt;/a&gt; and the &lt;a href="https://learn.microsoft.com/azure/api-management/llm-emit-token-metric-policy" rel="noopener noreferrer"&gt;&lt;code&gt;llm-emit-token-metric&lt;/code&gt; policy&lt;/a&gt; for per-consumer token tracking. The SOC analyst's question, "show me every client calling this server, what it consumed, and when", becomes answerable from a single observability plane. That audit trail is precisely what unrestricted DCR alone never provides.&lt;/p&gt;

&lt;p&gt;The honest gap deserves explicit naming. Neither APIM nor Entra today offers a governance-controlled RFC 7591 registration endpoint: a &lt;code&gt;/register&lt;/code&gt; that issues credentials only to clients that have passed a policy gate: an Entra consent check, an access package approval, or a security review workflow. APIM and Entra together approximate this behavior through a different mechanism: explicit Entra app registration as the consent prerequisite, gateway policy as the runtime enforcement layer. The interoperability friction with DCR-native MCP clients is real. The architectural slot for a "governed DCR endpoint" is currently unoccupied in the Microsoft stack.&lt;/p&gt;

&lt;p&gt;That gap is worth naming because it is where the tooling will likely evolve. An architecture where APIM exposes an RFC 7591-compliant &lt;code&gt;/register&lt;/code&gt; endpoint that, on receiving a DCR request, triggers an Entra app registration, initiates an admin consent workflow, and optionally assigns an access package, before returning credentials to the registering client, would close it elegantly. That architecture does not exist today as a native product feature. What exists is the layered governance model described above.&lt;/p&gt;

&lt;h2&gt;
  
  
  A governance stack, not a binary choice
&lt;/h2&gt;

&lt;p&gt;The practical architecture decision is not "DCR or no DCR." It is: what governance controls stand between a client expressing interest and a client receiving credentials?&lt;/p&gt;

&lt;p&gt;For open, developer-facing MCP servers where the audience is trusted developers, the data sensitivity is low, and the expected client population is diverse and changing, DCR with software statement validation and a lean consent model is appropriate. Lean does not mean absent: even here, logging registrations, validating software statements when present, and auditing active clients periodically are the minimum defensible posture.&lt;/p&gt;

&lt;p&gt;For enterprise MCP servers that expose organizational data, internal APIs, financial workflows, or records subject to regulatory requirements, the bar is different. Every client should be named. Every registration should be a deliberate trust decision. Every token scope should be consented explicitly under a policy that your security and governance teams own.&lt;/p&gt;

&lt;p&gt;APIM plus Entra delivers that model today. Entra's explicit registration and consent layer ensures no client receives tokens without a human decision in the chain. APIM's AI Gateway enforcement layer ensures no token-bearing client bypasses your runtime policy. The observability layer ensures that "who approved this, with which permissions, and when" remains answerable across the full client lifecycle.&lt;/p&gt;

&lt;p&gt;CIMD metadata enriches the picture on the way in: software statements, declared grant types, client URIs. Use them where your authorization server or gateway policy can act on them. Do not treat "richer metadata at registration time" as a substitute for governance state. Those are different properties, and conflating them is the precise mistake that makes a SOC analyst's life difficult six months after a pilot launches.&lt;/p&gt;

&lt;p&gt;DCR is useful. CIMD is useful. Neither is a trust framework. The security teams that built enterprise software approval programs did not do it for the friction. They did it because supply chain attacks are real, and the moment you stop asking "who approved this software to exist here" is the moment a well-named binary with a convincing &lt;code&gt;client_name&lt;/code&gt; receives credentials to your organizational data.&lt;/p&gt;

&lt;p&gt;APIM AI Gateway and Entra's explicit registration model is the closest the Microsoft stack comes today to answering that question at MCP server scale. The remaining gap is a named target. The governance floor is already in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7591" rel="noopener noreferrer"&gt;RFC 7591 - OAuth 2.0 Dynamic Client Registration Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7592" rel="noopener noreferrer"&gt;RFC 7592 - OAuth 2.0 Dynamic Client Registration Management Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc9728" rel="noopener noreferrer"&gt;RFC 9728 - OAuth 2.0 Protected Resource Metadata&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc8414" rel="noopener noreferrer"&gt;RFC 8414 - OAuth 2.0 Authorization Server Metadata&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/latest/basic/authorization" rel="noopener noreferrer"&gt;MCP authorization specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity-platform/quickstart-register-app" rel="noopener noreferrer"&gt;Register an application in Microsoft Entra ID&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/permissions-reference#application-developer" rel="noopener noreferrer"&gt;Application Developer role - Microsoft Entra&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/microsoft-365/copilot/extensibility/plugin-authentication-dynamic-client-registration" rel="noopener noreferrer"&gt;Configure dynamic client registration - Microsoft 365 Copilot extensibility&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/azure/api-management/genai-gateway-capabilities" rel="noopener noreferrer"&gt;AI gateway capabilities in Azure API Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/azure/api-management/secure-mcp-servers" rel="noopener noreferrer"&gt;Secure access to MCP servers in Azure API Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/azure/api-management/mcp-server-overview" rel="noopener noreferrer"&gt;MCP server overview in Azure API Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/azure/api-management/validate-azure-ad-token-policy" rel="noopener noreferrer"&gt;validate-azure-ad-token policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/azure/api-management/credentials-overview" rel="noopener noreferrer"&gt;About API credentials and credential manager in API Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/azure/api-management/api-management-howto-app-insights" rel="noopener noreferrer"&gt;Application Insights integration in API Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/azure/api-management/llm-emit-token-metric-policy" rel="noopener noreferrer"&gt;llm-emit-token-metric policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity-platform/permissions-consent-overview" rel="noopener noreferrer"&gt;Permissions and consent overview - Microsoft Entra&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cisa.gov/news-events/news/cisa-insights-solarwinds" rel="noopener noreferrer"&gt;CISA insights: SolarWinds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2024-3094" rel="noopener noreferrer"&gt;NVD CVE-2024-3094 (XZ Utils backdoor)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>oauth</category>
      <category>security</category>
      <category>zerotrust</category>
      <category>apimanagement</category>
    </item>
    <item>
      <title>Why consent is the trust contract for Entra Agent ID</title>
      <dc:creator>Anton Staykov</dc:creator>
      <pubDate>Wed, 12 Aug 2026 11:07:00 +0000</pubDate>
      <link>https://dev.to/astaykov/why-consent-is-the-trust-contract-for-entra-agent-id-c03</link>
      <guid>https://dev.to/astaykov/why-consent-is-the-trust-contract-for-entra-agent-id-c03</guid>
      <description>&lt;p&gt;If your AI agent can read customer mailboxes, create directory objects, or call internal APIs, your real architecture question is not "can it authenticate?" The hard question is "who authorized this, under which policy, for how long, and with which guardrails?" In Microsoft Entra, that question is answered through &lt;a href="https://learn.microsoft.com/en-us/entra/identity-platform/permissions-consent-overview" rel="noopener noreferrer"&gt;permissions and consent&lt;/a&gt;. That is why consent is not a user-experience detail. It is the trust contract.&lt;/p&gt;

&lt;p&gt;This matters even more for agent identities. Microsoft Entra Agent ID deliberately treats agents as a special operational class, with explicit authorization controls and blocked high-risk privileges rather than unconstrained app-like power (&lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/authorization-agent-id" rel="noopener noreferrer"&gt;authorization model&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/graph/api/resources/agentid-platform-overview?view=graph-rest-1.0" rel="noopener noreferrer"&gt;Graph overview for Agent ID&lt;/a&gt;). If your mental model is still "grant broad access once and move on," your governance will fail faster than your pilot succeeds.&lt;/p&gt;

&lt;p&gt;In [[A-transformational-AI-agent-does-not-start-with-the-permissions-it-needs]], I argued that strong agent platforms start narrow and earn scope in context. This article is the governance layer under that claim: why consent is the control surface, how to operate it at enterprise scale, and how access packages turn theory into an auditable runtime model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consent is authorization, not authentication
&lt;/h2&gt;

&lt;p&gt;Consent in Entra is the act that authorizes an application identity to access a protected resource with specific permissions (&lt;a href="https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/user-admin-consent-overview" rel="noopener noreferrer"&gt;user and admin consent overview&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/identity-platform/permissions-consent-overview" rel="noopener noreferrer"&gt;permissions and consent overview&lt;/a&gt;). Authentication answers "who are you?" Consent answers "what are you allowed to do against this API or resource?"&lt;/p&gt;

&lt;p&gt;That distinction sounds obvious until production pressure arrives. Teams often secure sign-in, configure Conditional Access, and then assume the authorization problem is solved. It is not. A signed-in actor without the right consent grant is still unauthorized, and a correctly consented actor can still be over-privileged if governance posture is weak (&lt;a href="https://learn.microsoft.com/en-us/entra/identity-platform/permissions-consent-overview" rel="noopener noreferrer"&gt;permissions and consent overview&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent?pivots=portal" rel="noopener noreferrer"&gt;configure user consent&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For mixed technical leadership, this is the key reframing: consent is where legal risk, security posture, and delivery velocity intersect. It is the formal boundary between "agent capability requested" and "agent capability approved."&lt;/p&gt;

&lt;h2&gt;
  
  
  User consent and admin consent are different risk gates
&lt;/h2&gt;

&lt;p&gt;Entra supports delegated permissions where an app or agent acts on behalf of a signed-in user, and application permissions where it acts autonomously without a user context (&lt;a href="https://learn.microsoft.com/en-us/entra/identity-platform/permissions-consent-overview#types-of-permissions" rel="noopener noreferrer"&gt;permissions and consent overview&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/grant-agent-access-microsoft-365" rel="noopener noreferrer"&gt;grant agents access&lt;/a&gt;). Those two modes map to different consent paths and different blast radii.&lt;/p&gt;

&lt;p&gt;User consent is viable when non-admin users are allowed to consent to the app and permission set by tenant policy (&lt;a href="https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/user-admin-consent-overview#user-consent" rel="noopener noreferrer"&gt;user and admin consent overview&lt;/a&gt;). Admin consent is required for application permissions and for delegated permissions that are admin-restricted or otherwise outside permitted user-consent boundaries (&lt;a href="https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/user-admin-consent-overview#admin-consent" rel="noopener noreferrer"&gt;user and admin consent overview&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/grant-agent-access-microsoft-365" rel="noopener noreferrer"&gt;grant agents access&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That is not bureaucracy. That is architecture. User consent is typically scoped to the user context. Admin consent can authorize tenant-wide impact. Treating those as interchangeable is how over-privileged agents are born.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consent policy is a leadership decision encoded in platform behavior
&lt;/h2&gt;

&lt;p&gt;Most organizations do not leave consent wide open, and they should not. Entra gives administrators tenant-wide choices: disable user consent entirely, allow only verified publishers plus selected low-impact permissions, or allow broader user consent where risk tolerance supports it (&lt;a href="https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent?pivots=portal" rel="noopener noreferrer"&gt;configure user consent&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/user-admin-consent-overview#user-consent-settings" rel="noopener noreferrer"&gt;user and admin consent overview&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;If user consent is limited, Entra can route blocked requests through the admin consent workflow so business demand is visible and reviewable instead of hidden in shadow tooling (&lt;a href="https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/user-admin-consent-overview#admin-consent-workflow" rel="noopener noreferrer"&gt;user and admin consent overview&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-admin-consent-workflow" rel="noopener noreferrer"&gt;configure admin consent workflow&lt;/a&gt;). This is where governance stops being a "no" function and becomes a control function.&lt;/p&gt;

&lt;p&gt;For leadership teams, the operational takeaway is clear: consent policy is not a tactical identity setting. It is a policy expression of enterprise risk appetite. Decide it explicitly, document it, and align agent deployment patterns to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consent phishing proves whether your model is real
&lt;/h2&gt;

&lt;p&gt;Consent phishing is the attack pattern where users are tricked into granting OAuth permissions to malicious applications, often through convincing app names and realistic prompts (&lt;a href="https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/protect-against-consent-phishing#what-is-consent-phishing" rel="noopener noreferrer"&gt;protect against consent phishing&lt;/a&gt;). This is not theoretical noise. It is exactly what happens when the organization treats consent as a user-interface click instead of a control plane.&lt;/p&gt;

&lt;p&gt;Microsoft guidance is explicit: harden user consent settings, favor verified publishers, audit app permissions, investigate suspicious grants, and integrate detection and remediation workflows (&lt;a href="https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/protect-against-consent-phishing#best-practices-for-hardening-against-consent-phishing-attacks" rel="noopener noreferrer"&gt;protect against consent phishing&lt;/a&gt;). If those controls are absent, your "agent security strategy" is just language.&lt;/p&gt;

&lt;p&gt;A mature model assumes two truths at once: legitimate agents need controlled permission growth, and adversaries will try to exploit the same consent mechanics. Governance has to handle both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delegated vs application permissions for agents, with protocol constructs that matter
&lt;/h2&gt;

&lt;p&gt;For agent identities, delegated mode is used when the agent acts in a user context. Application mode is used when the agent acts autonomously (&lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/authorization-agent-id#microsoft-graph-permissions-for-agent-ids" rel="noopener noreferrer"&gt;authorization in Agent ID&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/grant-agent-access-microsoft-365#when-to-use-delegated-or-application-permissions" rel="noopener noreferrer"&gt;grant agents access&lt;/a&gt;). That split directly affects token semantics.&lt;/p&gt;

&lt;p&gt;In delegated scenarios, effective scopes are carried in the &lt;code&gt;scp&lt;/code&gt; claim. In app-only scenarios, effective app permissions are carried in the &lt;code&gt;roles&lt;/code&gt; claim (&lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/agent-token-claims#core-token-claim-types" rel="noopener noreferrer"&gt;token claims for agents&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/grant-agent-access-microsoft-365#when-to-use-delegated-or-application-permissions" rel="noopener noreferrer"&gt;grant agents access&lt;/a&gt;). You do not need packet-level protocol traces to govern correctly, but you do need leadership alignment that these constructs define runtime authority.&lt;/p&gt;

&lt;p&gt;The advanced nuance for Agent ID is permission inheritance at blueprint level. This is not default behavior. Entra documents that inheritance only happens when you explicitly configure inheritable permissions on the blueprint and the corresponding permissions are actually granted on the blueprint principal (&lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/concept-inheritable-permissions" rel="noopener noreferrer"&gt;inheritable permissions concept&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/configure-inheritable-permissions-blueprints" rel="noopener noreferrer"&gt;configure inheritable permissions&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That should drive a stricter approval posture: treat blueprint consent as baseline capability setup, not as a full production access request. Keep blueprint consent narrow, and push broader or time-bound operational access through governed assignment workflows such as access packages (&lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/agent-access-packages" rel="noopener noreferrer"&gt;access packages for agent identities&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent ID already blocks high-risk permission paths, by design
&lt;/h2&gt;

&lt;p&gt;A healthy governance discussion should also acknowledge built-in platform guardrails. Agent IDs can use delegated and application permission models, but Entra explicitly blocks a range of high-risk Graph permissions for agents and restricts role assignment options compared to unconstrained identity types (&lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/authorization-agent-id" rel="noopener noreferrer"&gt;authorization in Agent ID&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/graph/api/resources/agentid-platform-overview?view=graph-rest-1.0#microsoft-graph-permissions-blocked-for-agents" rel="noopener noreferrer"&gt;Agent ID Graph overview blocked permissions&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This design choice reinforces the principle of least privilege for autonomous systems. It does not remove governance responsibility, but it reduces failure modes where someone "temporarily" grants sweeping directory control and forgets to remove it.&lt;/p&gt;

&lt;p&gt;In plain terms: the platform is already telling you the right operating model. Narrow by default. Escalate deliberately. Keep high-risk paths off the table unless there is an explicit and supported control route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Access packages are the practical admin-consent engine for agent scale
&lt;/h2&gt;

&lt;p&gt;Consent alone is necessary, but not sufficient, when you run fleets of agents. You need a repeatable, policy-owned mechanism for requesting, approving, granting, expiring, and reviewing access. For Entra Agent ID, access packages provide that mechanism (&lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/agent-access-packages" rel="noopener noreferrer"&gt;access packages for agent identities&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/id-governance/agent-id-governance-overview" rel="noopener noreferrer"&gt;governing agent identities&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This is why I keep connecting this to [[Access-Packages-Are-the-Control-Plane-for-Entra-Agent-ID-Permissions]]. Access packages let you operationalize consent and authorization as workflow, not as ad hoc privilege assignment. They can include directory roles, OAuth delegated and application permissions, and group memberships in one governed package (&lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/agent-access-packages" rel="noopener noreferrer"&gt;access packages for agent identities&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Crucially, policy can require one, two, or three approval stages, with explicit approver definitions and decision timeouts (&lt;a href="https://learn.microsoft.com/en-us/entra/id-governance/entitlement-management-access-package-approval-policy" rel="noopener noreferrer"&gt;approval policy settings&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/id-governance/entitlement-management-access-package-create#specify-approval-settings" rel="noopener noreferrer"&gt;access package create stages&lt;/a&gt;). That means you can model governance depth based on data sensitivity, not gut feel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamic approvals, SoD checks, and custom logic make governance adaptive
&lt;/h2&gt;

&lt;p&gt;Static approver lists are often too rigid for enterprise reality. Entra entitlement management supports dynamic approval determination through custom extensions and Logic Apps integration, so approvers can be derived through business logic at request time (&lt;a href="https://learn.microsoft.com/en-us/entra/id-governance/entitlement-management-dynamic-approval" rel="noopener noreferrer"&gt;dynamic approval&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/id-governance/entitlement-management-logic-apps-integration" rel="noopener noreferrer"&gt;Logic Apps integration&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Separation-of-duties controls let you mark access packages or groups as incompatible, preventing users or identities with one assignment from requesting conflicting assignments (&lt;a href="https://learn.microsoft.com/en-us/entra/id-governance/entitlement-management-access-package-incompatible" rel="noopener noreferrer"&gt;SoD checks&lt;/a&gt;). This is the difference between "we approved safely once" and "we remain safe over time."&lt;/p&gt;

&lt;p&gt;If you need one sentence for the board slide, use this: access packages are not just an approval screen. They are a programmable authorization lifecycle with governance state.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical operating model for mixed technical leadership
&lt;/h2&gt;

&lt;p&gt;If you lead a platform team, security team, or product portfolio touching agent identities, this model is battle-tested and aligned to Microsoft guidance:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with narrow required access&lt;/strong&gt; on blueprints and explicit inheritable-permission design (inheritance is opt-in, not automatic), so admins can review baseline intent clearly and avoid turning blueprint consent into broad standing access (&lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/concept-inheritable-permissions" rel="noopener noreferrer"&gt;inheritable permissions concept&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/configure-inheritable-permissions-blueprints" rel="noopener noreferrer"&gt;configure inheritable permissions&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set tenant consent policy intentionally&lt;/strong&gt; (usually verified-publisher plus low-risk where possible), and force higher-risk asks through admin review paths (&lt;a href="https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent?pivots=portal" rel="noopener noreferrer"&gt;configure user consent&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/user-admin-consent-overview" rel="noopener noreferrer"&gt;user and admin consent overview&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route runtime growth through access packages&lt;/strong&gt; instead of direct permanent grants, including approver stages, SoD, and expiry settings (&lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/agent-access-packages" rel="noopener noreferrer"&gt;access packages for agent identities&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/id-governance/entitlement-management-access-package-approval-policy" rel="noopener noreferrer"&gt;approval policy&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/id-governance/entitlement-management-access-package-incompatible" rel="noopener noreferrer"&gt;SoD checks&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat sponsor and owner accountability as first-class controls&lt;/strong&gt;, because governance is not complete if no human is accountable for extension and renewal decisions (&lt;a href="https://learn.microsoft.com/en-us/entra/id-governance/agent-id-governance-overview" rel="noopener noreferrer"&gt;governing agent identities&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume consent abuse attempts and instrument for response&lt;/strong&gt;, including consent-phishing hardening and suspicious-grant investigation workflows (&lt;a href="https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/protect-against-consent-phishing" rel="noopener noreferrer"&gt;protect against consent phishing&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This operating model gives engineering enough autonomy to ship and iterate, while preserving the control evidence leadership needs for audit and risk committees.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core decision
&lt;/h2&gt;

&lt;p&gt;You can run agents on standing privilege and hope nothing goes wrong. Many teams do exactly that during pilots. It looks fast right until the first real governance review.&lt;/p&gt;

&lt;p&gt;Or you can run agents on governed consent, with policy-owned approval paths, explicit token authority constructs, and time-bound assignment lifecycle. That model scales beyond demos because it aligns with how Entra is designed to authorize applications and agents (&lt;a href="https://learn.microsoft.com/en-us/entra/identity-platform/permissions-consent-overview" rel="noopener noreferrer"&gt;permissions and consent overview&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/authorization-agent-id" rel="noopener noreferrer"&gt;authorization in Agent ID&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/agent-access-packages" rel="noopener noreferrer"&gt;access packages for agent identities&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That is the underrated shift in enterprise agent architecture right now: the winning platform is not the one with the most tools. It is the one where consent remains the trust contract, even as agent capability grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/authorization-agent-id" rel="noopener noreferrer"&gt;Authorization in Microsoft Entra Agent ID&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/grant-agent-access-microsoft-365" rel="noopener noreferrer"&gt;Grant agents access to Microsoft 365 resources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/agent-token-claims" rel="noopener noreferrer"&gt;Token claims reference for agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/concept-inheritable-permissions" rel="noopener noreferrer"&gt;Inheritable permissions and required resource access in Microsoft Entra Agent ID&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/configure-inheritable-permissions-blueprints" rel="noopener noreferrer"&gt;Configure inheritable permissions for agent identity blueprints&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/identity-platform/permissions-consent-overview" rel="noopener noreferrer"&gt;Overview of permissions and consent in the Microsoft identity platform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/user-admin-consent-overview" rel="noopener noreferrer"&gt;Overview of user and admin consent&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent?pivots=portal" rel="noopener noreferrer"&gt;Configure how users consent to applications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-admin-consent-workflow" rel="noopener noreferrer"&gt;Configure the admin consent workflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/protect-against-consent-phishing" rel="noopener noreferrer"&gt;Protect against consent phishing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/agent-id/agent-access-packages" rel="noopener noreferrer"&gt;Access packages for Agent identities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/id-governance/agent-id-governance-overview" rel="noopener noreferrer"&gt;Governing Agent Identities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/id-governance/entitlement-management-access-package-approval-policy" rel="noopener noreferrer"&gt;Change approval and requestor information settings for an access package in entitlement management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/id-governance/entitlement-management-access-package-create" rel="noopener noreferrer"&gt;Create an access package in entitlement management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/id-governance/entitlement-management-dynamic-approval" rel="noopener noreferrer"&gt;Externally determine the approval requirements for an access package using custom extensions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/id-governance/entitlement-management-logic-apps-integration" rel="noopener noreferrer"&gt;Trigger Logic Apps with custom extensions in entitlement management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/id-governance/entitlement-management-access-package-incompatible" rel="noopener noreferrer"&gt;Configure separation of duties checks for an access package in entitlement management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/graph/api/resources/agentid-platform-overview?view=graph-rest-1.0" rel="noopener noreferrer"&gt;Microsoft Entra Agent ID APIs in Microsoft Graph overview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>entraagentid</category>
      <category>identitygovernance</category>
      <category>microsoftentra</category>
      <category>aiagents</category>
    </item>
    <item>
      <title>From JWT-SVID to Entra Agent ID: a working SPIFFE PoC</title>
      <dc:creator>Anton Staykov</dc:creator>
      <pubDate>Thu, 06 Aug 2026 11:19:00 +0000</pubDate>
      <link>https://dev.to/astaykov/from-jwt-svid-to-entra-agent-id-a-working-spiffe-poc-ic4</link>
      <guid>https://dev.to/astaykov/from-jwt-svid-to-entra-agent-id-a-working-spiffe-poc-ic4</guid>
      <description>&lt;p&gt;The architecture diagram was the easy part.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/astaykov/your-spiffe-workload-can-authenticate-as-an-entra-agent-id-but-mind-the-trust-gap-3969"&gt;Your SPIFFE workload can authenticate as an Entra Agent ID, but mind the trust gap&lt;/a&gt;, I argued that SPIFFE and Microsoft Entra Agent ID belong on opposite sides of a narrow trust contract. SPIRE proves which workload is running. Entra decides which governed agent identity that workload may use. A federated identity credential connects those statements through an exact issuer, subject, and audience match, which is how &lt;a href="https://learn.microsoft.com/graph/api/resources/federatedidentitycredentials-overview?view=graph-rest-1.0" rel="noopener noreferrer"&gt;Microsoft Entra workload identity federation validates an external JWT&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That article described the architecture and its trust gap. This one runs it.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/astaykov/agentid-spiffe" rel="noopener noreferrer"&gt;&lt;code&gt;agentid-spiffe&lt;/code&gt; proof of concept&lt;/a&gt; deploys a complete agent loop with SPIRE, public OIDC discovery, Microsoft Entra Agent ID, and a downstream Microsoft MCP Server for enterprise call. The interesting result is not the chat interface or the model. It is the moment a JWT-SVID issued to one attested process becomes the client assertion for an Agent Identity Blueprint, without a client secret or certificate copied into the workload.&lt;/p&gt;

&lt;p&gt;This is a conceptual walkthrough with selected configuration and code. It is deliberately not a SPIRE deployment guide. Follow the &lt;a href="https://github.com/astaykov/agentid-spiffe#readme" rel="noopener noreferrer"&gt;repository README&lt;/a&gt; to run the sample. Here, we are going to trace the identity as it changes hands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five SPIFFE concepts in one minute
&lt;/h2&gt;

&lt;p&gt;A &lt;em&gt;trust domain&lt;/em&gt; is the administrative and cryptographic boundary within which SPIFFE identities are issued. Its name forms the authority portion of every SPIFFE ID. The PoC uses &lt;code&gt;poc.local&lt;/code&gt;, so its workload identity begins with &lt;code&gt;spiffe://poc.local/&lt;/code&gt;. SPIFFE defines that URI as the stable name of a workload, independent of an IP address, host name, or cloud provider account (&lt;a href="https://spiffe.io/docs/latest/spiffe-about/spiffe-concepts/" rel="noopener noreferrer"&gt;SPIFFE concepts&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;SPIRE Server&lt;/em&gt; is the authority for that trust domain. It stores registration entries, evaluates attestation results, and signs the identity documents issued inside the domain. The &lt;em&gt;SPIRE Agent&lt;/em&gt; runs near workloads, attests to the server as a node, exposes the local Workload API, and performs workload attestation before returning identity documents to a process (&lt;a href="https://spiffe.io/docs/latest/spire-about/spire-concepts/" rel="noopener noreferrer"&gt;SPIRE concepts&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Workload attestation&lt;/em&gt; is the decision that connects runtime evidence to a registered SPIFFE ID. In this sample, the SPIRE Server registers a Unix selector for a process running as UID 0. When the FastAPI process calls the local Workload API, the SPIRE Agent evaluates that evidence and returns the identity registered for the matching selector. SPIRE documents selectors as the attested properties used to match a workload to a registration entry (&lt;a href="https://spiffe.io/docs/latest/deploying/registering/" rel="noopener noreferrer"&gt;registering workloads&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The returned identity can take two common forms. An X.509-SVID is a certificate suited to mutual TLS. A &lt;em&gt;JWT-SVID&lt;/em&gt; is a signed bearer token whose &lt;code&gt;sub&lt;/code&gt; is the workload's SPIFFE ID and whose &lt;code&gt;aud&lt;/code&gt; names the intended recipient (&lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md" rel="noopener noreferrer"&gt;JWT-SVID specification&lt;/a&gt;). This PoC needs the JWT form because Microsoft Entra accepts an external JWT as the client assertion in its &lt;a href="https://learn.microsoft.com/entra/identity-platform/v2-oauth2-client-creds-grant-flow#third-case-access-token-request-with-a-federated-credential" rel="noopener noreferrer"&gt;federated client credential flow&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That is all the SPIFFE theory required for this walkthrough. The rest is the handoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The components in play
&lt;/h2&gt;

&lt;p&gt;The PoC places several processes in one Azure Container App image so the identity path is easy to inspect. That packaging is a lab convenience, not an architectural recommendation (&lt;a href="https://github.com/astaykov/agentid-spiffe#architecture" rel="noopener noreferrer"&gt;repository architecture&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Inside the container, five components matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The SPIRE Server owns the &lt;code&gt;poc.local&lt;/code&gt; trust domain and signs JWT-SVIDs.&lt;/li&gt;
&lt;li&gt;The SPIRE Agent exposes a Unix socket implementing the Workload API.&lt;/li&gt;
&lt;li&gt;The SPIRE OIDC Discovery Provider publishes the JWT signing keys.&lt;/li&gt;
&lt;li&gt;FastAPI retrieves the JWT-SVID and performs the Entra token exchanges.&lt;/li&gt;
&lt;li&gt;The browser SPA supplies an incoming user token when the sample runs in user-context mode.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Outside the container, Microsoft Entra holds the agent identity blueprint, its blueprint principal, the child agent identity, the federated identity credential, and the delegated grants. The final Agent ID token is passed to the Microsoft MCP Server for enterprise. The model call proves that the token is usable, but it is downstream of the identity mechanism we care about.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SPIFFE workload plane                     Microsoft Entra control plane

+---------------------------+            +-----------------------------+
| FastAPI agent workload    |            | Agent Identity Blueprint    |
+---------+-----------------+            +-------------------+---------+
          |                                                  |
          | runtime evidence                                 |
          v                                                  |
+---------+-----------+                                      | Federated Identity
| SPIRE Agent         |                                      | Credential
| Workload API        |                                      |
+-------+-------------+                                      |
        | attestation                                        |
        v                                                    |
+-------+-----------------+      signing keys       +--------+--------+
| SPIRE Server            |------------------------&amp;gt;| OIDC metadata   |
| issues JWT-SVID         |                         | and public JWKS |
+-----------+-------------+                         +-------+---------+
            |                                               ^
            | short-lived JWT-SVID                          |
            v                                               | validates
+-----------+-------------+  JWT-SVID + fmi_path            |
| FastAPI agent workload  |--------------------------------&amp;gt;|
+-------+-----------------+                                 |
        ^                                                   |
        | T1 exchange token                                 |
        +---------------------------------------------------+
        |
        | T1 as child assertion
        v
+-------------+-------------+                +-----------------------------+
| Child agent identity      |---------------&amp;gt;| Downstream MCP resource     |
| requests resource token   |    Agent ID    | validates Entra token       |
+---------------------------+    token       +-----------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diagram contains two identity planes. The SPIFFE plane ends when the workload receives its JWT-SVID. The Entra plane begins when that JWT is presented as the blueprint's client assertion.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the process becomes a SPIFFE workload
&lt;/h2&gt;

&lt;p&gt;The container entrypoint starts the SPIRE Server, creates a join token for the co-located agent, and registers one workload entry (&lt;a href="https://github.com/astaykov/agentid-spiffe/blob/main/src/entrypoint.sh" rel="noopener noreferrer"&gt;entrypoint source&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spire-server entry create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-parentID&lt;/span&gt; spiffe://poc.local/agent/node1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-spiffeID&lt;/span&gt; spiffe://poc.local/agent/identity-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-selector&lt;/span&gt; unix:uid:0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that registration from right to left. A process matching &lt;code&gt;unix:uid:0&lt;/code&gt;, attested through the registered parent agent, receives &lt;code&gt;spiffe://poc.local/agent/identity-1&lt;/code&gt; as its workload identity. The SPIFFE ID is not generated from the process name or container name. It is the explicit result of registration policy and attested selectors, which is the model SPIRE uses to issue identities through its Workload API (&lt;a href="https://spiffe.io/docs/latest/deploying/registering/" rel="noopener noreferrer"&gt;SPIRE workload registration&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The selector is intentionally broad because every process in the image runs in the same lab boundary. A production deployment would use workload and node attestors that express the real platform boundary. That decision is outside this PoC. We only need a reliably attested process from which to request the JWT-SVID.&lt;/p&gt;

&lt;p&gt;The server configuration then defines the JWT issuer and signing profile (&lt;a href="https://github.com/astaykov/agentid-spiffe/blob/main/src/spire/server.conf" rel="noopener noreferrer"&gt;SPIRE server configuration in the PoC&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;trust_domain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"poc.local"&lt;/span&gt;
    &lt;span class="nx"&gt;jwt_issuer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"__OIDC_ISSUER_URL__"&lt;/span&gt;
    &lt;span class="nx"&gt;jwt_key_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"rsa-2048"&lt;/span&gt;
    &lt;span class="nx"&gt;default_jwt_svid_ttl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10m"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;trust_domain&lt;/code&gt; controls the SPIFFE ID namespace. &lt;code&gt;jwt_issuer&lt;/code&gt; becomes the JWT-SVID &lt;code&gt;iss&lt;/code&gt; claim. &lt;code&gt;jwt_key_type&lt;/code&gt; makes SPIRE sign the assertion with RSA so that the token uses RS256. These values are related, but only the issuer crosses directly into the Entra configuration.&lt;/p&gt;

&lt;p&gt;And this is where the most important interoperability detail appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  The issuer is a public protocol surface, not a string
&lt;/h2&gt;

&lt;p&gt;It is tempting to treat the federated identity credential's &lt;code&gt;issuer&lt;/code&gt; as an identifier. Configure the same URL in SPIRE and Entra, make the strings match, and move on.&lt;/p&gt;

&lt;p&gt;That is not enough.&lt;/p&gt;

&lt;p&gt;Microsoft Entra documents two requirements that make the issuer an operational endpoint. First, RS256 is the supported signing algorithm for workload identity federation assertions. Other algorithms might work, but Microsoft does not test them. A robust implementation therefore needs an RS256-signed JWT, which is why this PoC explicitly configures &lt;code&gt;jwt_key_type = "rsa-2048"&lt;/code&gt; (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation-considerations#supported-signing-algorithms-and-issuers" rel="noopener noreferrer"&gt;supported signing algorithms and issuers&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Second, the issuer must be a publicly reachable HTTPS URL that follows OIDC discovery. Microsoft Entra uses the configured issuer to retrieve the metadata and signing keys needed to validate the external token (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation-create-trust#other-identity-providers-example" rel="noopener noreferrer"&gt;configure trust with another identity provider&lt;/a&gt;). For the issuer used by this PoC, the primary discovery URL is formed by appending &lt;code&gt;/.well-known/openid-configuration&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Issuer
https://&amp;lt;container-app-host&amp;gt;/spiffe-oidc

Discovery document
https://&amp;lt;container-app-host&amp;gt;/spiffe-oidc/.well-known/openid-configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The discovery document points Entra to the public key set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;container-app-host&amp;gt;/spiffe-oidc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jwks_uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;container-app-host&amp;gt;/spiffe-oidc/keys"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject_types_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"public"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Entra follows &lt;code&gt;jwks_uri&lt;/code&gt;, finds the public key whose &lt;code&gt;kid&lt;/code&gt; matches the JWT-SVID header, and uses that key to verify the RS256 signature. The private signing key remains inside SPIRE. OIDC discovery publishes only the public half required by verifiers, following the standard discovery and JWKS model (&lt;a href="https://openid.net/specs/openid-connect-discovery-1_0.html" rel="noopener noreferrer"&gt;OpenID Connect Discovery 1.0&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This is why an exact &lt;code&gt;iss&lt;/code&gt; match can still fail. The URL might be spelled correctly while the discovery document is missing, inaccessible, malformed, or pointing at a JWKS that does not contain the active signing key. In that state, Entra knows &lt;em&gt;where&lt;/em&gt; the assertion claims to come from but cannot cryptographically prove that the issuer signed it.&lt;/p&gt;

&lt;p&gt;The sample runs SPIRE's OIDC Discovery Provider locally, then proxies its metadata and key set through the Container App's public HTTPS origin (&lt;a href="https://github.com/astaykov/agentid-spiffe/blob/main/src/backend/main.py" rel="noopener noreferrer"&gt;OIDC proxy implementation&lt;/a&gt;). The proxy rewrites &lt;code&gt;issuer&lt;/code&gt; and &lt;code&gt;jwks_uri&lt;/code&gt; to their public values, removes an empty authorization endpoint, populates &lt;code&gt;subject_types_supported&lt;/code&gt;, and adds &lt;code&gt;"use": "sig"&lt;/code&gt; to each published key for compatibility. It exposes both the OpenID Connect discovery path shown above and the RFC 8414 path form for an issuer containing a path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://&amp;lt;container-app-host&amp;gt;/.well-known/openid-configuration/spiffe-oidc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both documents describe the same issuer and point to the same JWKS. That extra route avoids relying on ambiguous discovery behavior when the issuer itself contains &lt;code&gt;/spiffe-oidc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The OIDC endpoint is not incidental web plumbing. It is part of the credential chain. If it disappears or publishes the wrong key, the JWT-SVID stops being a credential Entra can validate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The federated identity credential binds one workload to the blueprint
&lt;/h2&gt;

&lt;p&gt;Once the public issuer works, the post-deployment configuration creates a federated identity credential on the &lt;em&gt;agent identity blueprint application&lt;/em&gt;. It does not create the credential on the child agent identity (&lt;a href="https://github.com/astaykov/agentid-spiffe/blob/main/scripts/configure-postdeploy.ps1" rel="noopener noreferrer"&gt;FIC configuration source&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The effective object is small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spire-jwt-svid-blueprint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;container-app-host&amp;gt;/spiffe-oidc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spiffe://poc.local/agent/identity-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"audiences"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"api://AzureADTokenExchange"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is a decoded JWT-SVID captured during one of the PoC runs. The signature is omitted, but the header and payload are unchanged:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "alg": "RS256",
  "kid": "KNd3DSGQAvCP1U2rHUd3rBzf9oX1y7BG",
  "typ": "JWT"
}.{
  "aud": [
    "api://AzureADTokenExchange"
  ],
  "exp": 1785157382,
  "iat": 1785156782,
  "iss": "https://spiffe.proudhill-d482409c.westeurope.azurecontainerapps.io/spiffe-oidc",
  "sub": "spiffe://poc.local/agent/identity-1"
}.[Signature]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The header says that SPIRE signed the assertion with RS256 and identifies the public verification key through &lt;code&gt;kid&lt;/code&gt;. The payload names Entra's token exchange as the only audience, carries a ten-minute validity window, exposes the public OIDC issuer, and uses the registered SPIFFE ID as its subject. Entra retrieves the issuer's JWKS, selects &lt;code&gt;KNd3DSGQAvCP1U2rHUd3rBzf9oX1y7BG&lt;/code&gt;, verifies the omitted signature, and then evaluates the three FIC claim matches (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation#how-it-works" rel="noopener noreferrer"&gt;workload identity federation mechanics&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Those three trust properties map directly to the incoming JWT-SVID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JWT-SVID iss  == FIC issuer
JWT-SVID sub  == FIC subject
JWT-SVID aud  == FIC audiences[0]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Microsoft Entra compares those values exactly and case-sensitively when evaluating a standard federated identity credential (&lt;a href="https://learn.microsoft.com/graph/api/resources/federatedidentitycredentials-overview?view=graph-rest-1.0#how-do-federated-identity-credentials-work" rel="noopener noreferrer"&gt;federated identity credential matching&lt;/a&gt;). The FIC therefore says something precise: a token from this public issuer, for this one SPIFFE workload, minted specifically for Azure AD token exchange, may authenticate this blueprint.&lt;/p&gt;

&lt;p&gt;It does not grant the workload Microsoft Graph permissions. It does not authorize the model to use an MCP tool. It only replaces the blueprint's stored client secret or certificate with a short-lived external assertion. Authorization remains attached to the child agent identity and the downstream resource, consistent with the &lt;a href="https://learn.microsoft.com/entra/agent-id/agent-identities" rel="noopener noreferrer"&gt;Agent ID identity and authorization model&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The application requests the JWT-SVID only when MSAL needs it
&lt;/h2&gt;

&lt;p&gt;The FastAPI application opens the local SPIRE Workload API through the SPIRE Agent's Unix socket. Its assertion provider asks for one JWT-SVID with &lt;code&gt;api://AzureADTokenExchange&lt;/code&gt; as the audience (&lt;a href="https://github.com/astaykov/agentid-spiffe/blob/main/src/backend/main.py" rel="noopener noreferrer"&gt;backend source&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SpiffeJwtProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;svid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_ensure_source&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;fetch_svid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;audience&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_audience&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;svid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That audience matters. The &lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md#72-audience" rel="noopener noreferrer"&gt;JWT-SVID specification recommends a single audience&lt;/a&gt; because a JWT-SVID is a bearer token and every listed audience becomes a potential recipient. This token exists for one purpose: presentation to Entra's workload identity federation endpoint.&lt;/p&gt;

&lt;p&gt;The provider is passed to MSAL as a callable client assertion rather than as a static token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;blueprint_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfidentialClientApplication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;BLUEPRINT_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;client_credential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_assertion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_jwt_svid_provider&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AUTHORITY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MSAL can invoke a callable assertion provider when it needs a fresh credential, which fits a short-lived SVID better than capturing one token at process startup (&lt;a href="https://learn.microsoft.com/python/api/msal/msal.application.confidentialclientapplication" rel="noopener noreferrer"&gt;MSAL Python confidential client application&lt;/a&gt;). SPIRE remains responsible for issuing the workload credential. MSAL remains responsible for Entra token acquisition and caching.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;fmi_path&lt;/code&gt; turns blueprint authentication into one agent identity
&lt;/h2&gt;

&lt;p&gt;The first Entra request authenticates the blueprint with the JWT-SVID and asks for an exchange token scoped to one child agent identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;blueprint_app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquire_token_for_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api://AzureADTokenExchange/.default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;fmi_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;agent_app_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;fmi_path&lt;/code&gt; value is the child agent identity's client ID. Microsoft documents that parameter as the instruction telling Entra which agent identity the blueprint is acting on behalf of (&lt;a href="https://learn.microsoft.com/entra/agent-id/autonomous-agent-authentication-authorization-flow#request-a-token-for-the-agent-identity-blueprint" rel="noopener noreferrer"&gt;request a token for the agent identity blueprint&lt;/a&gt;). The result is T1, an exchange token bound to the blueprint-to-child path.&lt;/p&gt;

&lt;p&gt;The child agent identity has no independent client secret, certificate, or FIC. The application creates a second MSAL confidential client for the child and supplies T1 through another assertion callback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assertion_cb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;blueprint_app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquire_token_for_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api://AzureADTokenExchange/.default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;fmi_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;agent_app_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;agent_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfidentialClientApplication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;agent_app_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;client_credential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_assertion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;assertion_cb&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AUTHORITY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, the flow can branch. In app-only mode, the child requests a resource token using its application permissions. In the PoC's user-context mode, it combines T1 with the incoming user's access token in an on-behalf-of exchange. Microsoft documents that the second request switches &lt;code&gt;client_id&lt;/code&gt; from the blueprint to the child agent identity and presents both the blueprint exchange token and user assertion (&lt;a href="https://learn.microsoft.com/entra/agent-id/agent-on-behalf-of-oauth-flow#protocol-steps" rel="noopener noreferrer"&gt;Agent ID OBO protocol&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The JWT-SVID has already done its job before that branch. It authenticated the blueprint. T1 then authenticated the child agent identity. The final access token is scoped to the downstream resource and represents either the autonomous agent or the agent acting in the user's context, depending on the selected flow (&lt;a href="https://learn.microsoft.com/entra/agent-id/autonomous-agent-authentication-authorization-flow" rel="noopener noreferrer"&gt;authenticate and acquire tokens for autonomous agents&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That separation is the reason this is more than generic workload federation. The external workload does not become an ordinary app registration. It enters a blueprint-controlled path and emerges as a governed child agent identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the PoC proves
&lt;/h2&gt;

&lt;p&gt;The sample proves four useful things.&lt;/p&gt;

&lt;p&gt;First, a JWT-SVID can serve as the real client assertion for an Entra Agent ID blueprint. Microsoft lists SPIFFE and SPIRE as a supported workload identity federation scenario, and Agent ID supports direct workload identity exchange for third-party agent platforms (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation#supported-scenarios" rel="noopener noreferrer"&gt;supported workload identity federation scenarios&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/entra/agent-id/configure-third-party-agents#integration-patterns-for-third-party-agents" rel="noopener noreferrer"&gt;third-party Agent ID integration patterns&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Second, the SPIFFE ID maps cleanly into the FIC subject. The workload identity &lt;code&gt;spiffe://poc.local/agent/identity-1&lt;/code&gt; remains visible at the trust boundary instead of being translated into an unrelated shared secret.&lt;/p&gt;

&lt;p&gt;Third, the public issuer surface is as important as the token. Federation succeeds only when the JWT &lt;code&gt;iss&lt;/code&gt;, FIC issuer, discovery document &lt;code&gt;issuer&lt;/code&gt;, discovery URL, JWKS location, active &lt;code&gt;kid&lt;/code&gt;, and RS256 signature all describe the same cryptographic authority. Microsoft Entra retrieves the external keys through that OIDC issuer contract before accepting the assertion (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation#how-it-works" rel="noopener noreferrer"&gt;workload identity federation mechanics&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Fourth, the identity that calls the downstream API is still the child agent identity. SPIRE authenticates the workload to the blueprint. It does not bypass Agent ID's parent-child token flow, consent, or downstream authorization model (&lt;a href="https://learn.microsoft.com/entra/agent-id/agent-identities" rel="noopener noreferrer"&gt;agent identities&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  What the PoC deliberately does not prove
&lt;/h2&gt;

&lt;p&gt;The repository is unusually clear about its shortcuts, and that clarity matters.&lt;/p&gt;

&lt;p&gt;SPIRE state is ephemeral. Every Container App start deletes the server and agent data, regenerating the CA material, JWT signing key, and JWT &lt;code&gt;kid&lt;/code&gt;. Microsoft Entra can retain previously retrieved issuer metadata and keys, so exchanges can fail after a restart until its view refreshes (&lt;a href="https://github.com/astaykov/agentid-spiffe#spiffe-jwt-svid-to-entra-agent-id-poc" rel="noopener noreferrer"&gt;repository warning&lt;/a&gt;). This is the loudest reason not to mistake the sample for a production SPIRE topology.&lt;/p&gt;

&lt;p&gt;The agent uses a join token and &lt;code&gt;insecure_bootstrap = true&lt;/code&gt;, while the server and agent are co-located with the workload. Those choices remove infrastructure that would distract from the exchange, but they are explicitly marked as PoC-only in the &lt;a href="https://github.com/astaykov/agentid-spiffe/blob/main/src/spire/agent.conf" rel="noopener noreferrer"&gt;agent configuration&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The backend also logs the full JWT-SVID to simplify troubleshooting while the FIC is being wired up (&lt;a href="https://github.com/astaykov/agentid-spiffe/blob/main/src/backend/main.py" rel="noopener noreferrer"&gt;logging code&lt;/a&gt;). Do not carry that behavior into production. A JWT-SVID is a bearer credential susceptible to replay during its validity window, so it must not enter application logs, traces, prompts, or tool output (&lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md#71-replay-protection" rel="noopener noreferrer"&gt;JWT-SVID replay considerations&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;None of those shortcuts invalidate the demonstrated identity exchange. They define its boundary. The sample proves that a correctly issued JWT-SVID, signed with RS256 and backed by public OIDC discovery, can authenticate an Agent ID blueprint. It does not prove that one ephemeral container is how an organization should operate SPIRE.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trust contract is now executable
&lt;/h2&gt;

&lt;p&gt;The previous article ended with two control planes and one narrow trust contract. The PoC turns that sentence into running code.&lt;/p&gt;

&lt;p&gt;SPIRE attests the FastAPI process and issues &lt;code&gt;spiffe://poc.local/agent/identity-1&lt;/code&gt; a short-lived JWT-SVID. The OIDC discovery endpoint makes the active RSA public key available at a predictable public URL. The federated identity credential authorizes one exact issuer, SPIFFE ID, and audience to authenticate the blueprint. &lt;code&gt;fmi_path&lt;/code&gt; narrows the resulting T1 token to one child agent identity. That child obtains the final resource token under its own authorization boundary.&lt;/p&gt;

&lt;p&gt;No shared client secret crosses the container boundary. No X.509 certificate is copied into the agent. No custom token validator is added to Entra. The bridge uses the SPIFFE JWT identity format, standard OIDC discovery, Entra workload identity federation, and Agent ID's documented blueprint exchange (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation" rel="noopener noreferrer"&gt;workload identity federation&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/entra/agent-id/autonomous-agent-authentication-authorization-flow" rel="noopener noreferrer"&gt;Agent ID token acquisition&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The issuer configuration is the hinge. It must be exact, public, standards-shaped, and cryptographically aligned with the JWT in flight. Get that right, and the SPIFFE workload can enter Entra as a governed agent identity without ever owning a long-lived Entra credential.&lt;/p&gt;

&lt;p&gt;That is the result this PoC was built to isolate.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/astaykov/agentid-spiffe" rel="noopener noreferrer"&gt;SPIFFE JWT-SVID to Entra Agent ID PoC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/astaykov/your-spiffe-workload-can-authenticate-as-an-entra-agent-id-but-mind-the-trust-gap-3969"&gt;Your SPIFFE workload can authenticate as an Entra Agent ID, but mind the trust gap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/astaykov/your-agentic-platform-doesnt-need-a-single-secret-managed-identities-workload-identity-3602"&gt;Your agentic platform doesn't need a single secret&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spiffe.io/docs/latest/spiffe-about/spiffe-concepts/" rel="noopener noreferrer"&gt;SPIFFE concepts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spiffe.io/docs/latest/spire-about/spire-concepts/" rel="noopener noreferrer"&gt;SPIRE concepts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spiffe.io/docs/latest/deploying/registering/" rel="noopener noreferrer"&gt;Registering workloads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md" rel="noopener noreferrer"&gt;JWT-SVID specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openid.net/specs/openid-connect-discovery-1_0.html" rel="noopener noreferrer"&gt;OpenID Connect Discovery 1.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation" rel="noopener noreferrer"&gt;Workload identity federation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/graph/api/resources/federatedidentitycredentials-overview?view=graph-rest-1.0" rel="noopener noreferrer"&gt;Overview of federated identity credentials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation-create-trust" rel="noopener noreferrer"&gt;Configure an application to trust an external identity provider&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation-considerations" rel="noopener noreferrer"&gt;Important considerations for federated identity credentials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/agent-id/configure-third-party-agents" rel="noopener noreferrer"&gt;Integrate third-party agents with Microsoft Entra Agent ID&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/agent-id/autonomous-agent-authentication-authorization-flow" rel="noopener noreferrer"&gt;Authenticate and acquire tokens for autonomous agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/agent-id/agent-on-behalf-of-oauth-flow" rel="noopener noreferrer"&gt;Agent OAuth on-behalf-of flow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/agent-id/agent-identities" rel="noopener noreferrer"&gt;Agent identities in Microsoft Entra Agent ID&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity-platform/v2-oauth2-client-creds-grant-flow#third-case-access-token-request-with-a-federated-credential" rel="noopener noreferrer"&gt;OAuth 2.0 client credentials flow with a federated credential&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/python/api/msal/msal.application.confidentialclientapplication" rel="noopener noreferrer"&gt;MSAL Python ConfidentialClientApplication&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>entraagentid</category>
      <category>spiffe</category>
      <category>workloadidentity</category>
      <category>identitysecurity</category>
    </item>
    <item>
      <title>Your SPIFFE workload can authenticate as an Entra Agent ID, but mind the trust gap</title>
      <dc:creator>Anton Staykov</dc:creator>
      <pubDate>Tue, 04 Aug 2026 09:13:00 +0000</pubDate>
      <link>https://dev.to/astaykov/your-spiffe-workload-can-authenticate-as-an-entra-agent-id-but-mind-the-trust-gap-3969</link>
      <guid>https://dev.to/astaykov/your-spiffe-workload-can-authenticate-as-an-entra-agent-id-but-mind-the-trust-gap-3969</guid>
      <description>&lt;p&gt;SPIFFE with Microsoft Entra Agent ID sounds almost too tidy.&lt;/p&gt;

&lt;p&gt;Let SPIRE attest an AI agent running in Kubernetes, on-premises, or in another cloud. Give that workload a short-lived SPIFFE identity. Present the resulting token to Microsoft Entra. Receive an access token for a governed Entra Agent ID. No client secret, no certificate copied into a container, no cloud-specific identity shim.&lt;/p&gt;

&lt;p&gt;The architecture is viable. Microsoft explicitly lists &lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation#supported-scenarios" rel="noopener noreferrer"&gt;SPIFFE and SPIRE among its supported workload identity federation scenarios&lt;/a&gt;, and Entra Agent ID supports &lt;a href="https://learn.microsoft.com/entra/agent-id/configure-third-party-agents#use-workload-identity-federation-direct-identity-exchange" rel="noopener noreferrer"&gt;third-party agents through direct workload identity federation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But the clean diagram hides a trust gap.&lt;/p&gt;

&lt;p&gt;SPIFFE proves &lt;em&gt;which workload is running&lt;/em&gt;. Microsoft Entra decides &lt;em&gt;which agent identity that workload may act as&lt;/em&gt;. A federated identity credential connects those two statements. If that mapping, its issuer surface, or its audience design is wrong, a short-lived token merely helps the wrong workload become the wrong agent faster.&lt;/p&gt;

&lt;p&gt;That is the pattern worth understanding. Not "SPIFFE replaces Entra" and not "Entra understands SPIFFE natively." The real design has two identity control planes joined by one exact, case-sensitive trust contract.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SPIFFE workload plane                     Microsoft Entra control plane

+---------------------------+            +-----------------------------+
| AI agent workload         |            | Agent identity blueprint    |
|                           |            |                             |
| spiffe://example.com/     |            | FIC matches:                |
| agent/invoice-reconciler  |            |   iss + sub + aud           |
+-------------+-------------+            +--------------+--------------+
              |                                         |
              | 1. Attestation                          |
              v                                         |
+-------------+-------------+                           |
| SPIRE Server + Agent      |                           |
|                           |                           |
| Issues short-lived        |                           |
| JWT-SVID                  |                           |
+-------------+-------------+                           |
              |                                         |
              | 2. JWT-SVID + fmi_path                  |
              +----------------------------------------&amp;gt;|
                                                        |
                                          3. Returns T1 |
                                                        v
                                         +--------------+--------------+
                                         | Child agent identity        |
                                         |                             |
                                         | T1 exchanged for a          |
                                         | resource-scoped token       |
                                         +--------------+--------------+
                                                        |
              4. Entra access token                     |
              &amp;lt;-----------------------------------------+

The SPIFFE workload authenticates the blueprint.
The child agent identity is the principal authorized on downstream APIs.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  SPIFFE and Entra answer different questions
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://spiffe.io/docs/latest/spiffe-about/overview/" rel="noopener noreferrer"&gt;SPIFFE&lt;/a&gt; defines a platform-neutral identity framework for software workloads. Its central identifier is the SPIFFE ID, a URI such as &lt;code&gt;spiffe://example.com/agent/invoice-reconciler&lt;/code&gt;. A SPIFFE implementation such as SPIRE attests a workload using runtime evidence, then makes short-lived identity documents available through the &lt;a href="https://spiffe.io/docs/latest/deploying/svids/" rel="noopener noreferrer"&gt;SPIFFE Workload API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Microsoft Entra Agent ID starts one layer higher. An &lt;a href="https://learn.microsoft.com/entra/agent-id/agent-identities" rel="noopener noreferrer"&gt;agent identity is a specialized service principal&lt;/a&gt; used to request tokens, receive tokens, and act either autonomously or in a user's context. The agent identity does not hold its own credential. Its &lt;a href="https://learn.microsoft.com/entra/agent-id/agent-identities#authorizing-agent-identities" rel="noopener noreferrer"&gt;agent identity blueprint holds credentials and acquires tokens on behalf of child agent identities&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That division is useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SPIRE answers: &lt;em&gt;is this the workload that my runtime policy registered?&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Entra answers: &lt;em&gt;which enterprise agent identity may this authenticated workload use, and what may that identity access?&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The distinction matters because workload authentication is not downstream authorization. A valid SPIFFE identity does not grant Microsoft Graph permissions, an Azure role, or consent to an API scope. It only supplies evidence that Entra can accept as a credential for the blueprint. The resulting agent identity remains the principal that needs authorization on the target resource, a separation Microsoft also makes explicit for &lt;a href="https://learn.microsoft.com/entra/agent-id/agent-identities" rel="noopener noreferrer"&gt;agent identity token acquisition&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Think of SPIRE as the passport office and Entra as the border authority. One establishes that the document belongs to a particular workload. The other decides which governed identity crosses the border and what that identity may do after entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Only one of the two SVID forms fits the exchange
&lt;/h2&gt;

&lt;p&gt;SPIFFE defines two common forms of SPIFFE Verifiable Identity Document, and they are not interchangeable.&lt;/p&gt;

&lt;p&gt;An &lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md" rel="noopener noreferrer"&gt;X.509-SVID&lt;/a&gt; is an X.509 certificate in which the workload's SPIFFE ID appears as the single URI Subject Alternative Name. It is a strong fit for workload-to-workload mutual TLS. The workload proves possession of the corresponding private key while both sides validate certificate chains against SPIFFE trust bundles.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md" rel="noopener noreferrer"&gt;JWT-SVID&lt;/a&gt; is a signed bearer JWT. Its &lt;code&gt;sub&lt;/code&gt; claim must be the workload's SPIFFE ID, and it must contain both &lt;code&gt;aud&lt;/code&gt; and &lt;code&gt;exp&lt;/code&gt;. That shape fits Microsoft Entra workload identity federation because Entra's client credentials flow accepts a JWT from an external identity provider as the &lt;code&gt;client_assertion&lt;/code&gt; (&lt;a href="https://learn.microsoft.com/entra/identity-platform/v2-oauth2-client-creds-grant-flow#third-case-access-token-request-with-a-federated-credential" rel="noopener noreferrer"&gt;federated client credential flow&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This is the first catch: &lt;em&gt;an X.509-SVID is not a federated identity assertion for this flow&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;You cannot hand Entra an X.509-SVID and expect it to extract the URI SAN, validate the SPIFFE certificate chain, and perform the workload identity exchange. Microsoft documents the external credential as a JWT assertion with an issuer, subject, and audience that match a configured federated identity credential (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation#how-it-works" rel="noopener noreferrer"&gt;workload identity federation mechanics&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Keep X.509-SVIDs for mTLS inside the workload plane. Mint a dedicated JWT-SVID for the Entra token exchange. Trying to make one credential format do both jobs weakens the design rather than simplifying it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bridge is an exact three-claim contract
&lt;/h2&gt;

&lt;p&gt;A federated identity credential, or FIC, tells Entra which external JWT may authenticate a workload identity. Three values form the contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;issuer&lt;/code&gt; must match the incoming JWT's &lt;code&gt;iss&lt;/code&gt; claim.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;subject&lt;/code&gt; must match the incoming JWT's &lt;code&gt;sub&lt;/code&gt; claim.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;audiences&lt;/code&gt; must contain the audience Entra expects in the incoming JWT, normally &lt;code&gt;api://AzureADTokenExchange&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Microsoft performs this match &lt;a href="https://learn.microsoft.com/graph/api/resources/federatedidentitycredentials-overview?view=graph-rest-1.0#how-do-federated-identity-credentials-work" rel="noopener noreferrer"&gt;exactly and case-sensitively&lt;/a&gt;. Wildcards are not supported in standard FIC property values, and an application or agent identity blueprint can have at most 20 FICs (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation-considerations#general-federated-identity-credential-considerations" rel="noopener noreferrer"&gt;FIC considerations&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For one production agent workload, the incoming JWT-SVID could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://oidc.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spiffe://example.com/agent/invoice-reconciler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"api://AzureADTokenExchange"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1784304000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The corresponding FIC on the agent identity blueprint's application object is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spiffe-production-invoice-agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://oidc.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spiffe://example.com/agent/invoice-reconciler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"audiences"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"api://AzureADTokenExchange"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Incoming JWT-SVID                         Federated identity credential

iss: https://oidc.example.com       ===&amp;gt; issuer: https://oidc.example.com

sub: spiffe://example.com/          ===&amp;gt; subject: spiffe://example.com/
  agent/invoice-reconciler                    agent/invoice-reconciler

aud: api://AzureADTokenExchange     ===&amp;gt; audiences:
                 api://AzureADTokenExchange

      EXACT, CASE-SENSITIVE MATCH

This mapping authenticates the blueprint. It does not grant downstream
permissions or carry a user's delegated authorization context.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;subject&lt;/code&gt; is where SPIFFE fits naturally. The &lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md#31-subject" rel="noopener noreferrer"&gt;JWT-SVID specification requires &lt;code&gt;sub&lt;/code&gt; to equal the workload's SPIFFE ID&lt;/a&gt;, so the FIC binds one precise SPIFFE workload identity to one blueprint credential path.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;audience&lt;/code&gt; is where architecture becomes policy. SPIFFE allows site-specific audiences and recommends scoping them to the intended recipient (&lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md#32-audience" rel="noopener noreferrer"&gt;JWT-SVID audience requirements&lt;/a&gt;). For this token, the intended recipient is Entra's token exchange. The workload should therefore request a JWT-SVID with the single audience &lt;code&gt;api://AzureADTokenExchange&lt;/code&gt;, matching the FIC.&lt;/p&gt;

&lt;p&gt;Do not reuse a JWT-SVID minted for an internal API. Do not add Entra as a second audience to a token already used elsewhere. A recipient of a multi-audience bearer token can replay that token to another listed audience, which is why the SPIFFE specification &lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md#72-audience" rel="noopener noreferrer"&gt;strongly recommends single-audience JWT-SVIDs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The token flow, without the hand-waving
&lt;/h2&gt;

&lt;p&gt;The autonomous flow has seven steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SPIRE attests the agent workload according to its registration and runtime selectors.&lt;/li&gt;
&lt;li&gt;The workload requests a JWT-SVID from the local Workload API with &lt;code&gt;api://AzureADTokenExchange&lt;/code&gt; as its sole audience.&lt;/li&gt;
&lt;li&gt;SPIRE issues a short-lived JWT-SVID whose &lt;code&gt;sub&lt;/code&gt; is the workload's SPIFFE ID.&lt;/li&gt;
&lt;li&gt;The workload sends that JWT to the Microsoft identity platform token endpoint as &lt;code&gt;client_assertion&lt;/code&gt;, requests &lt;code&gt;api://AzureADTokenExchange/.default&lt;/code&gt;, and supplies the child agent identity's client ID in &lt;code&gt;fmi_path&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Entra discovers the external issuer's signing keys, validates the JWT, compares &lt;code&gt;iss&lt;/code&gt;, &lt;code&gt;sub&lt;/code&gt;, and &lt;code&gt;aud&lt;/code&gt; against the FIC on the blueprint, and returns the blueprint exchange token &lt;code&gt;T1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The workload sends &lt;code&gt;T1&lt;/code&gt; as the &lt;code&gt;client_assertion&lt;/code&gt; in a second request whose &lt;code&gt;client_id&lt;/code&gt; is the child agent identity and whose scope names the downstream resource.&lt;/li&gt;
&lt;li&gt;Entra validates the blueprint-to-child relationship and issues a resource-scoped access token in which the agent identity, not the SPIFFE workload, is the enterprise principal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first request uses the JWT-SVID to authenticate the blueprint. The &lt;code&gt;fmi_path&lt;/code&gt; parameter selects the child agent identity for which the blueprint is requesting an exchange token, exactly as documented in the &lt;a href="https://learn.microsoft.com/entra/agent-id/autonomous-agent-authentication-authorization-flow#request-a-token-for-the-agent-identity-blueprint" rel="noopener noreferrer"&gt;autonomous agent authentication flow&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id={agent-identity-blueprint-client-id}
&amp;amp;scope=api%3A%2F%2FAzureADTokenExchange%2F.default
&amp;amp;fmi_path={agent-identity-client-id}
&amp;amp;client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&amp;amp;client_assertion={jwt-svid}
&amp;amp;grant_type=client_credentials
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That request returns &lt;code&gt;T1&lt;/code&gt;, not the Microsoft Graph token. The second request switches &lt;code&gt;client_id&lt;/code&gt; from the blueprint to the child agent identity and uses &lt;code&gt;T1&lt;/code&gt; as its assertion (&lt;a href="https://learn.microsoft.com/entra/agent-id/autonomous-agent-authentication-authorization-flow#request-an-agent-identity-token" rel="noopener noreferrer"&gt;request an agent identity token&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id={agent-identity-client-id}
&amp;amp;scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
&amp;amp;client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&amp;amp;client_assertion={agent-blueprint-token-T1}
&amp;amp;grant_type=client_credentials
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The blueprint authenticates with the external assertion, but the child agent identity becomes the principal in the final resource token. That distinction is not administrative trivia. If the downstream API authorizes the blueprint rather than the agent identity, the architecture has collapsed the attribution and least-privilege boundary it adopted Agent ID to create.&lt;/p&gt;

&lt;h2&gt;
  
  
  A SPIFFE bundle endpoint is not OIDC discovery
&lt;/h2&gt;

&lt;p&gt;This is the catch most diagrams omit.&lt;/p&gt;

&lt;p&gt;SPIFFE defines how trust bundles carry X.509 authorities and JWT signing keys. Its &lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Federation.md" rel="noopener noreferrer"&gt;federation specification&lt;/a&gt; defines bundle endpoints through which one SPIFFE trust domain can retrieve another domain's current bundle. JWT-SVID signing keys appear in a SPIFFE bundle as JWK entries whose &lt;code&gt;use&lt;/code&gt; value is &lt;code&gt;jwt-svid&lt;/code&gt; (&lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md#6-representation-in-the-spiffe-bundle" rel="noopener noreferrer"&gt;JWT-SVID key publication&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Microsoft Entra expects something different. The configured FIC issuer must be a URL compatible with OIDC discovery, and Entra uses that issuer URL to retrieve the keys required to validate the external token (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation-create-trust#other-identity-providers" rel="noopener noreferrer"&gt;external issuer configuration&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;A SPIFFE bundle endpoint and an OIDC discovery endpoint both publish trust material over HTTPS, but they are not the same protocol surface. Similar nouns do not create interoperability.&lt;/p&gt;

&lt;p&gt;The production architecture therefore needs an issuer service that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exposes &lt;code&gt;/.well-known/openid-configuration&lt;/code&gt; for the configured HTTPS issuer;&lt;/li&gt;
&lt;li&gt;exposes a JWKS endpoint referenced by that discovery document;&lt;/li&gt;
&lt;li&gt;publishes the active SPIRE JWT signing keys in an Entra-compatible JWKS;&lt;/li&gt;
&lt;li&gt;keeps the JWT-SVID &lt;code&gt;iss&lt;/code&gt; claim exactly aligned with the configured issuer URL;&lt;/li&gt;
&lt;li&gt;rotates published keys without creating a window in which valid JWT-SVIDs cannot be verified.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That service is part of your identity control plane. Treating it as a static JSON file on an incidental web server is the identity equivalent of putting a production certificate chain in a shared folder and calling it PKI.&lt;/p&gt;

&lt;p&gt;Microsoft also documents that the identity platform stores only the first 100 signing keys downloaded from an external OIDC endpoint (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation#how-it-works" rel="noopener noreferrer"&gt;workload identity federation mechanics&lt;/a&gt;). That limit should be easy to respect, but it makes stale-key accumulation an operational concern rather than harmless clutter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Short-lived bearer tokens can still be replayed
&lt;/h2&gt;

&lt;p&gt;JWT-SVIDs reduce credential lifetime. They do not eliminate bearer-token behavior.&lt;/p&gt;

&lt;p&gt;The SPIFFE specification is direct: &lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md#71-replay-protection" rel="noopener noreferrer"&gt;JWT-SVIDs are susceptible to replay&lt;/a&gt;. A short &lt;code&gt;exp&lt;/code&gt; narrows the window. A &lt;code&gt;jti&lt;/code&gt; can support replay detection, but JWT-SVID validators are not required to track &lt;code&gt;jti&lt;/code&gt; uniqueness. Confidential transport remains mandatory because possession of the token is sufficient to present it during its valid lifetime (&lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md#73-transport-security" rel="noopener noreferrer"&gt;JWT-SVID transport security&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For this exchange, that produces a clear baseline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request the JWT-SVID only when an Entra token is needed;&lt;/li&gt;
&lt;li&gt;use one audience, &lt;code&gt;api://AzureADTokenExchange&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;keep JWT-SVID lifetime as short as the exchange path tolerates;&lt;/li&gt;
&lt;li&gt;never log the assertion or place it in agent prompts, tool output, or distributed traces;&lt;/li&gt;
&lt;li&gt;send it directly to Entra over TLS;&lt;/li&gt;
&lt;li&gt;cache the resulting resource token according to its lifetime, not the source assertion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Short-lived credentials change the incident from "rotate a secret that may have escaped months ago" to "contain a replay window measured in minutes." That is a major improvement. It is not immunity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rotation now crosses an organizational boundary
&lt;/h2&gt;

&lt;p&gt;SPIRE owns workload attestation and JWT signing-key rotation. The team operating the OIDC facade owns discovery availability and JWKS publication. Microsoft Entra owns external-token validation and FIC matching. The Agent ID team owns blueprint and child identity boundaries.&lt;/p&gt;

&lt;p&gt;That is four operational responsibilities, even if one platform team implements all of them.&lt;/p&gt;

&lt;p&gt;Microsoft currently documents RS256 as the supported signing algorithm for workload identity federation token exchange, while noting that other algorithms may work but are not tested (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation-considerations#supported-signing-algorithms-and-issuers" rel="noopener noreferrer"&gt;supported signing algorithms&lt;/a&gt;). An architect should therefore require the SPIRE JWT signing profile exposed to Entra to use RS256 rather than treating JOSE algorithm choice as an implementation detail.&lt;/p&gt;

&lt;p&gt;FIC changes also take time to propagate. Microsoft warns that token requests made shortly after a FIC change can fail with &lt;code&gt;AADSTS70021&lt;/code&gt; until regional caches update, and recommends retry logic (&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation-considerations#time-for-federated-credential-changes-to-propagate" rel="noopener noreferrer"&gt;FIC propagation&lt;/a&gt;). Key rollover and FIC rollout need staged tests, overlap, monitoring, and a rollback path. "The tokens are short-lived" does not make control-plane changes atomic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 20-credential limit is an architecture signal
&lt;/h2&gt;

&lt;p&gt;Standard FICs bind exact issuer and subject values, and each application or blueprint is limited to 20 credentials (&lt;a href="https://learn.microsoft.com/graph/api/resources/federatedidentitycredentials-overview?view=graph-rest-1.0#design-considerations" rel="noopener noreferrer"&gt;FIC design considerations&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That limit should stop an enterprise from attaching every SPIFFE workload in a fleet to one blueprint. It reinforces the boundary model from [[Enterprise-architect-decision-framework-for-Entra-Agent-ID-blueprints-on-non-Microsoft-platforms]]: one blueprint should represent a deliberate trust and blast-radius boundary, not become a universal token vending machine.&lt;/p&gt;

&lt;p&gt;Microsoft has introduced &lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identities-flexible-federated-identity-credentials" rel="noopener noreferrer"&gt;flexible federated identity credentials&lt;/a&gt; that can match subject patterns and selected custom claims, but the feature is currently preview. Pattern matching also changes the risk model. An exact SPIFFE ID binds one registered workload to the blueprint. A claims expression binds a class of workloads. That can reduce object count, but it increases the importance of SPIRE registration policy and namespace governance.&lt;/p&gt;

&lt;p&gt;Scale is not permission to blur identity boundaries. If 20 exact bindings are insufficient, first ask whether the blueprint boundary is too broad. Only then consider a flexible expression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workload federation authenticates the client in both autonomous and OBO flows
&lt;/h2&gt;

&lt;p&gt;The source assertion proves the workload's identity. That makes the JWT-SVID a &lt;em&gt;client credential&lt;/em&gt;, not a statement about whether the resulting flow is autonomous or delegated.&lt;/p&gt;

&lt;p&gt;Microsoft separates autonomous agent authorization from on-behalf-of authorization, but both flows still require the client to authenticate. In the autonomous flow, the SPIFFE-backed FIC authenticates the blueprint before the child agent identity requests a resource token with its own application permissions. In an OBO flow, the same client-authentication path can be combined with an incoming user assertion so that both agent and user context participate (&lt;a href="https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow" rel="noopener noreferrer"&gt;Microsoft identity platform OBO flow&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/entra/agent-id/agent-on-behalf-of-oauth-flow" rel="noopener noreferrer"&gt;Agent ID OBO flow&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For Agent ID, this remains a two-stage exchange. First, the blueprint presents the JWT-SVID as its &lt;code&gt;client_assertion&lt;/code&gt;, includes the child agent identity in &lt;code&gt;fmi_path&lt;/code&gt;, and receives &lt;code&gt;T1&lt;/code&gt;. The child agent identity then performs the OBO exchange by presenting both &lt;code&gt;T1&lt;/code&gt; as its client credential and the incoming user access token as the user assertion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id={agent-identity-client-id}
&amp;amp;scope=https%3A%2F%2Fresource.example.com%2Fscope1
&amp;amp;client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&amp;amp;client_assertion={agent-blueprint-token-T1}
&amp;amp;grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
&amp;amp;assertion={incoming-user-access-token}
&amp;amp;requested_token_use=on_behalf_of
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The distinction between &lt;code&gt;client_assertion&lt;/code&gt; and &lt;code&gt;assertion&lt;/code&gt; is the architecture. &lt;code&gt;client_assertion&lt;/code&gt; proves which client is making the exchange. &lt;code&gt;assertion&lt;/code&gt; carries the signed-in user's delegated context. Workload identity federation replaces the client's stored secret; it does not replace the user token, user consent, delegated permissions, or OBO protocol.&lt;/p&gt;

&lt;p&gt;This also sharpens the connection to [[The-industry-does-not-have-an-agent-identity-propagation-standard-yet]]. SPIFFE can authenticate the workload at a cross-platform boundary, and Entra can combine that client authentication with an Entra-issued user assertion. SPIFFE still does not independently preserve a human subject across identity provider domains, capture consent, or define who is accountable for translating user context.&lt;/p&gt;

&lt;p&gt;Do not put a user's identifier into a private JWT-SVID claim and call the problem solved. The JWT-SVID authenticates the client. The incoming access token and OBO exchange carry delegated authorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  The production decision
&lt;/h2&gt;

&lt;p&gt;Use this pattern when all of the following are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the organization already operates SPIRE as a workload identity authority;&lt;/li&gt;
&lt;li&gt;the agent runs outside Azure or across heterogeneous infrastructure;&lt;/li&gt;
&lt;li&gt;the agent needs a governed Microsoft Entra identity for autonomous or OBO access;&lt;/li&gt;
&lt;li&gt;eliminating blueprint secrets is worth operating an OIDC discovery and JWKS publication surface;&lt;/li&gt;
&lt;li&gt;SPIFFE registration policy and Entra blueprint ownership have accountable operators;&lt;/li&gt;
&lt;li&gt;exact workload-to-blueprint mappings fit the intended blast-radius model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not use it as a shortcut around delegated consent or OBO. Use the JWT-SVID to authenticate the client, then supply the incoming user token through the documented &lt;code&gt;assertion&lt;/code&gt; parameter. Reject the pattern when your only available credential is an X.509-SVID or when nobody owns the issuer surface and key-rotation path.&lt;/p&gt;

&lt;p&gt;The final architecture is not "SPIFFE authenticates to Entra." That phrase hides too much.&lt;/p&gt;

&lt;p&gt;SPIRE attests a workload. A dedicated JWT-SVID presents that workload identity to a deliberately operated OIDC issuer surface. An exact FIC authorizes that issuer, SPIFFE ID, and audience to authenticate an agent identity blueprint. Entra then issues a resource token for the governed agent identity, whose permissions remain independently controlled.&lt;/p&gt;

&lt;p&gt;Two control planes. One narrow trust contract. No shared secret.&lt;/p&gt;

&lt;p&gt;That is a strong architecture, provided you operate all three parts of the sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://spiffe.io/docs/latest/spiffe-about/overview/" rel="noopener noreferrer"&gt;SPIFFE overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spiffe.io/docs/latest/deploying/svids/" rel="noopener noreferrer"&gt;Working with SVIDs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md" rel="noopener noreferrer"&gt;JWT-SVID specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md" rel="noopener noreferrer"&gt;X.509-SVID specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Federation.md" rel="noopener noreferrer"&gt;SPIFFE federation specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation" rel="noopener noreferrer"&gt;Microsoft Entra workload identity federation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation-considerations" rel="noopener noreferrer"&gt;Important considerations for federated identity credentials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identity-federation-create-trust" rel="noopener noreferrer"&gt;Configure an app to trust an external identity provider&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/agent-id/configure-third-party-agents" rel="noopener noreferrer"&gt;Integrate third-party agents with Microsoft Entra Agent ID&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/agent-id/agent-identities" rel="noopener noreferrer"&gt;Agent identities in Microsoft Entra Agent ID&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/agent-id/autonomous-agent-authentication-authorization-flow" rel="noopener noreferrer"&gt;Authenticate and acquire tokens for autonomous agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow" rel="noopener noreferrer"&gt;Microsoft identity platform and OAuth 2.0 On-Behalf-Of flow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/agent-id/agent-on-behalf-of-oauth-flow" rel="noopener noreferrer"&gt;Agent OAuth flows: On-behalf-of flow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/graph/api/resources/federatedidentitycredentials-overview?view=graph-rest-1.0" rel="noopener noreferrer"&gt;Federated identity credentials overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity-platform/v2-oauth2-client-creds-grant-flow#third-case-access-token-request-with-a-federated-credential" rel="noopener noreferrer"&gt;OAuth 2.0 client credentials flow with a federated credential&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/workload-id/workload-identities-flexible-federated-identity-credentials" rel="noopener noreferrer"&gt;Flexible federated identity credentials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc7523" rel="noopener noreferrer"&gt;RFC 7523: JWT profile for OAuth 2.0 client authentication and authorization grants&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>entraagentid</category>
      <category>spiffe</category>
      <category>workloadidentity</category>
      <category>identitysecurity</category>
    </item>
    <item>
      <title>Microsoft Graph least privilege has two control planes</title>
      <dc:creator>Anton Staykov</dc:creator>
      <pubDate>Tue, 28 Jul 2026 09:37:00 +0000</pubDate>
      <link>https://dev.to/astaykov/microsoft-graph-least-privilege-has-two-control-planes-4hnf</link>
      <guid>https://dev.to/astaykov/microsoft-graph-least-privilege-has-two-control-planes-4hnf</guid>
      <description>&lt;p&gt;The Microsoft Graph least-privilege story has improved a lot. That is not the problem.&lt;/p&gt;

&lt;p&gt;The problem is that many teams are still looking at only half of it.&lt;/p&gt;

&lt;p&gt;Granular permissions such as &lt;code&gt;User.Create&lt;/code&gt;, &lt;code&gt;User.EnableDisableAccount.All&lt;/code&gt;, &lt;code&gt;User.RevokeSessions.All&lt;/code&gt;, and &lt;code&gt;Application.ReadWrite.OwnedBy&lt;/code&gt; are a better design surface than the old habit of reaching for &lt;code&gt;Directory.ReadWrite.All&lt;/code&gt; whenever an app needed to do anything useful in the directory. Microsoft Graph permissions intentionally follow a &lt;code&gt;{resource}.{operation}.{constraint}&lt;/code&gt; naming pattern, which lets engineers express the operation an app is allowed to perform instead of throwing the app into a tenant-wide bucket (&lt;a href="https://learn.microsoft.com/graph/permissions-overview#permissions-naming-pattern" rel="noopener noreferrer"&gt;Microsoft Graph permissions overview&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That shift matters. But it does not finish the authorization model.&lt;/p&gt;

&lt;p&gt;The less understood truth is this: Microsoft Graph least privilege has &lt;em&gt;two control planes&lt;/em&gt;. The first is the Graph permission plane: what the client application is allowed to request from Microsoft Graph. The second is the Microsoft Entra control plane: who or what is allowed to control the application object, the service principal, and the directory objects the app will act on.&lt;/p&gt;

&lt;p&gt;If you design only the first plane, your architecture can still be wildly overprivileged.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first plane: Graph permissions authorize the client
&lt;/h2&gt;

&lt;p&gt;Start with the piece engineers usually know.&lt;/p&gt;

&lt;p&gt;Microsoft Graph supports delegated access and app-only access. In delegated access, the app calls Microsoft Graph on behalf of a signed-in user. In app-only access, the app calls Microsoft Graph as itself, without a signed-in user (&lt;a href="https://learn.microsoft.com/graph/permissions-overview#permission-types" rel="noopener noreferrer"&gt;Microsoft Graph permissions overview&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Delegated permissions are the easiest place to see the intersection model. Microsoft states it plainly: in a delegated scenario, the privileges an app has are determined by the Microsoft Graph permissions granted to the app &lt;em&gt;and&lt;/em&gt; the user's own permissions (&lt;a href="https://learn.microsoft.com/graph/permissions-overview#delegated-permissions" rel="noopener noreferrer"&gt;Microsoft Graph delegated permissions&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That sentence should be printed on every onboarding app design review.&lt;/p&gt;

&lt;p&gt;If your custom HR onboarding app has been granted the &lt;em&gt;delegated&lt;/em&gt; permission &lt;code&gt;User.Create&lt;/code&gt;, the app has permission to call the create-user operation. But the signed-in user still needs the Entra authority to create users. The Graph permission authorizes the client. The Entra role authorizes the human. Delegated Microsoft Graph access needs both.&lt;/p&gt;

&lt;p&gt;Flip it around and the same lesson holds. If the signed-in user is a User Administrator, but the app has not been granted the right delegated Microsoft Graph permission, the user's admin role does not magically donate authority to the client. A user can perform an operation in the Entra admin center and still fail through a custom app because the app itself is not authorized for the Graph call.&lt;/p&gt;

&lt;p&gt;This is the first correction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In a delegated scenario, admin role assignment is not a substitute for Graph consent, and Graph consent is not a substitute for admin role assignment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The second plane: Entra controls who can control the thing doing the calling
&lt;/h2&gt;

&lt;p&gt;Now comes the part that gets missed.&lt;/p&gt;

&lt;p&gt;Microsoft Entra applications are represented by two related objects: the application object and the service principal object. The application object is the global template for the app. The service principal is the local representation of that app in a specific tenant, and it defines what the app can actually do in that tenant, who can access it, and what resources it can access (&lt;a href="https://learn.microsoft.com/entra/identity-platform/app-objects-and-service-principals" rel="noopener noreferrer"&gt;Application and service principal objects in Microsoft Entra ID&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That distinction is not directory trivia. It is a control-plane boundary.&lt;/p&gt;

&lt;p&gt;The application object controls things like redirect URIs, credentials, &lt;em&gt;requested&lt;/em&gt; permissions, exposed APIs, and ownership. The service principal controls the enterprise application instance in the tenant: assignments, local policies, &lt;em&gt;consented&lt;/em&gt; permissions, provisioning configuration, sign-in behavior, and other tenant-specific settings (&lt;a href="https://learn.microsoft.com/entra/identity-platform/app-objects-and-service-principals#service-principal-object" rel="noopener noreferrer"&gt;Application and service principal objects&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;So when you ask, "is this Graph app least privileged?" the permission list is not enough. You also need to ask &lt;em&gt;who can mutate&lt;/em&gt; the app registration, &lt;em&gt;who can mutate the&lt;/em&gt; enterprise application, who can add credentials, who can add owners, who can change user assignment, and who can change the permissions the app requests.&lt;/p&gt;

&lt;p&gt;That is the second plane.&lt;/p&gt;

&lt;h2&gt;
  
  
  Owner is not a label, owner is control
&lt;/h2&gt;

&lt;p&gt;Application and enterprise application (service principal) ownership is one of the most underestimated privilege paths in Microsoft Entra.&lt;/p&gt;

&lt;p&gt;Microsoft documents that an owner of an enterprise application can manage the organization-specific configuration of the application, including single sign-on, provisioning, and user assignment. The owner can also add or remove other owners. More importantly, Microsoft states that owners have the same permissions as application administrators, scoped to an individual application (&lt;a href="https://learn.microsoft.com/entra/identity/enterprise-apps/overview-assign-app-owners" rel="noopener noreferrer"&gt;Enterprise application ownership&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Read that again slowly: &lt;em&gt;application administrator, scoped to one application&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That is not harmless metadata. &lt;strong&gt;That is delegated control over a highly sensitive object&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The default user permissions documentation is even more concrete:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Owners of application registrations can update application authentication (reply urls), &lt;strong&gt;credentials&lt;/strong&gt;, owners, permissions, policies, and basic properties. Owners of enterprise applications can update service principal &lt;strong&gt;credentials&lt;/strong&gt;, owners, permissions, &lt;strong&gt;app role assignments&lt;/strong&gt;, provisioning settings, and related policies (&lt;a href="https://learn.microsoft.com/entra/fundamentals/users-default-permissions#ownership-permissions" rel="noopener noreferrer"&gt;Default user permissions and ownership permissions&lt;/a&gt;).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Microsoft also calls out the dangerous consequence directly: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;... the application might have more permissions than the owner, which can become elevation of privilege because an owner can create or update objects while impersonating the application (&lt;a href="https://learn.microsoft.com/entra/identity/enterprise-apps/overview-assign-app-owners" rel="noopener noreferrer"&gt;Enterprise application ownership note&lt;/a&gt;).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the part many app teams miss. The owner does not need to have the same directory role as the app. If the app has powerful application permissions, the person who can add a credential or change ownership can potentially create a path to act as the app.&lt;/p&gt;

&lt;p&gt;Static secrets get most of the attention here, but ownership is the quieter root. If I can become owner, add myself as another owner, add or rotate credentials, adjust the app configuration, or influence the service principal's local settings, I am no longer merely adjacent to the app. I am in the control plane of the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bad pattern: broad Graph permission plus casual ownership
&lt;/h2&gt;

&lt;p&gt;Here is the pattern I still see too often.&lt;/p&gt;

&lt;p&gt;An engineering team builds a Graph automation app. They correctly avoid &lt;code&gt;Directory.ReadWrite.All&lt;/code&gt; and use a narrower Graph permission. Good. Then they leave three developers as owners of the app registration, two operations engineers as owners of the enterprise application, and a shared service account with broad rights because "someone needs to maintain it."&lt;/p&gt;

&lt;p&gt;On paper, the app permission set looks defensible. In reality, the control plane around the app is loose.&lt;/p&gt;

&lt;p&gt;That is not least privilege. That is narrow API permission sitting inside broad object control.&lt;/p&gt;

&lt;p&gt;The better question is not only "what Graph permissions does the app have?" The better question is: "who can change the identity that has those permissions?"&lt;/p&gt;

&lt;p&gt;If the answer is a long list of permanent owners, the app is overprivileged even if the Graph permission list looks elegant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scoped Entra roles are the missing design tool
&lt;/h2&gt;

&lt;p&gt;This is where Microsoft Entra role scoping becomes much more interesting than most app teams realize.&lt;/p&gt;

&lt;p&gt;Microsoft Entra roles are usually assigned at tenant scope, but Microsoft also supports assigning roles at narrower scopes, including application registrations and administrative units. Microsoft describes the resource a role assignment applies to as the scope, and supports restricted scope for built-in and custom roles (&lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/manage-roles-portal" rel="noopener noreferrer"&gt;Assign Microsoft Entra roles&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For app registrations, the model is direct. Instead of granting someone tenant-wide Application Administrator, you can assign a relevant built-in or even custom role scoped to a single app registration. Microsoft describes this as a way to let a principal update credentials and basic properties of a single app without granting tenant-wide application administration (&lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/manage-roles-portal#assign-roles-with-app-registration-scope" rel="noopener noreferrer"&gt;Assign roles with app registration scope&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The Graph shape is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments
Content-Type: application/json

{
  "@odata.type": "#microsoft.graph.unifiedRoleAssignment",
  "principalId": "&amp;lt;principal-object-id&amp;gt;",
  "roleDefinitionId": "&amp;lt;role-definition-id&amp;gt;",
  "directoryScopeId": "/&amp;lt;application-object-id&amp;gt;"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;directoryScopeId&lt;/code&gt; value matters. For app registration scope, &lt;code&gt;/&amp;lt;id&amp;gt;&lt;/code&gt; means the principal can manage that Microsoft Entra object. Microsoft explicitly contrasts that with &lt;code&gt;/administrativeUnits/&amp;lt;id&amp;gt;&lt;/code&gt;, which means the principal can manage the members of the administrative unit based on the assigned role (&lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/manage-roles-portal#assign-roles-with-app-registration-scope" rel="noopener noreferrer"&gt;Assign roles with app registration scope&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That distinction is the whole architecture in miniature. Same role-assignment resource. Different scope semantics. Different blast radius.&lt;/p&gt;

&lt;h2&gt;
  
  
  Administrative units are not just for humans
&lt;/h2&gt;

&lt;p&gt;Administrative units are usually explained as a delegation feature for helpdesk and regional administration. That is true, but incomplete.&lt;/p&gt;

&lt;p&gt;Microsoft documents that Entra roles can be assigned with administrative-unit scope so the role permissions apply only when managing members of that administrative unit, not tenant-wide settings or unrelated directory objects (&lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/manage-roles-portal#assign-roles-with-administrative-unit-scope" rel="noopener noreferrer"&gt;Assign roles with administrative unit scope&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For example, a User Administrator scoped to the "Germany Employees" administrative unit can manage users in that administrative unit without becoming User Administrator for the whole tenant, subject to the role's supported administrative-unit behavior and restrictions (&lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/manage-roles-portal#assign-roles-with-administrative-unit-scope" rel="noopener noreferrer"&gt;Administrative-unit role scope&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That already matters for humans. It matters even more for automation.&lt;/p&gt;

&lt;p&gt;If you are building an onboarding app for five subsidiaries, why should the app's runtime identity be able to manage every user in the tenant? If the work is limited to five administrative units, the role assignment should be limited to those administrative units. &lt;/p&gt;

&lt;p&gt;This is the balance shift: Graph permissions are always tenant-wide. Entra role assignments can be scoped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The widely unknown pattern: service principal plus administrative-unit scope
&lt;/h2&gt;

&lt;p&gt;Now for the part that is still too obscure.&lt;/p&gt;

&lt;p&gt;You can assign a Microsoft Entra role to a service principal at administrative-unit scope.&lt;/p&gt;

&lt;p&gt;This is not how most portal walkthroughs teach the feature. Most examples show a user as the principal. Some examples mention groups. The architecture pattern for app identities is still widely unknown. But the Graph contract supports the pieces.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;unifiedRoleAssignment&lt;/code&gt; resource represents a role definition assigned to a principal at a scope, and Microsoft lists supported principals as users, role-assignable groups, and service principals (&lt;a href="https://learn.microsoft.com/graph/api/resources/unifiedroleassignment?view=graph-rest-1.0" rel="noopener noreferrer"&gt;unifiedRoleAssignment resource type&lt;/a&gt;). The create-role-assignment API documents &lt;code&gt;directoryScopeId&lt;/code&gt; formats for the directory provider, including &lt;code&gt;/&lt;/code&gt; for tenant-wide scope, &lt;code&gt;/administrativeUnits/{administrativeunit-id}&lt;/code&gt; for administrative-unit scope, and &lt;code&gt;/{application-objectId}&lt;/code&gt; for application scope (&lt;a href="https://learn.microsoft.com/graph/api/rbacapplication-post-roleassignments?view=graph-rest-1.0" rel="noopener noreferrer"&gt;Create unifiedRoleAssignment&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Put those two facts together and the payload becomes obvious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments
Content-Type: application/json

{
  "@odata.type": "#microsoft.graph.unifiedRoleAssignment",
  "principalId": "&amp;lt;service-principal-object-id&amp;gt;",
  "roleDefinitionId": "&amp;lt;role-definition-id&amp;gt;",
  "directoryScopeId": "/administrativeUnits/&amp;lt;administrative-unit-id&amp;gt;"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That assignment says: this service principal has this Entra role, but only for the members of this administrative unit, subject to the role's administrative-unit support.&lt;/p&gt;

&lt;p&gt;In practice, there is one more wrinkle engineers need to know. Microsoft documents that service principals and guest users do not receive directory read permissions by default, and service principals using an administrative-unit-scoped role assignment need corresponding &lt;em&gt;read&lt;/em&gt; permissions, commonly Directory Readers at tenant scope or another role that includes the necessary read permissions (&lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/manage-roles-portal#assign-roles-with-administrative-unit-scope" rel="noopener noreferrer"&gt;Service principals and administrative-unit scoped role assignments&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That tenant-scope read dependency annoys people at first. It should not. Read capability and write authority are different risks. A service principal might need enough tenant-level read to resolve directory objects while its actual administrative authority remains scoped to one administrative unit. That is still a materially better design than granting a tenant-wide write role because the app needs to manage one slice of the directory.&lt;/p&gt;

&lt;p&gt;The pattern is not theoretical. It is validated, battle-tested, and still underused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this changes Graph automation design
&lt;/h2&gt;

&lt;p&gt;Once you see the two planes, the design review changes.&lt;/p&gt;

&lt;p&gt;For a delegated app, you ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What Microsoft Graph delegated permission does the client need?&lt;/li&gt;
&lt;li&gt;Which Entra role must the signed-in user hold?&lt;/li&gt;
&lt;li&gt;Can that role be scoped to an administrative unit or application registration instead of the tenant?&lt;/li&gt;
&lt;li&gt;Who owns the app registration and enterprise application?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an app-only automation identity, you ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What Microsoft Graph application permission does the service principal need?&lt;/li&gt;
&lt;li&gt;Can the operation be represented by an Entra role assignment instead of a broad Graph application permission?&lt;/li&gt;
&lt;li&gt;Can that Entra role be scoped to an administrative unit or specific (application) object?&lt;/li&gt;
&lt;li&gt;Does the service principal have only the directory read permissions needed to make that scoped role usable?&lt;/li&gt;
&lt;li&gt;Who can add credentials or owners to the app and service principal?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last question is not an afterthought. It is often the difference between least privilege and theater.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical model for app teams
&lt;/h2&gt;

&lt;p&gt;Here is the model I would use with app and platform engineers.&lt;/p&gt;

&lt;p&gt;First, choose the narrowest Microsoft Graph permission that matches the API operation. Use &lt;code&gt;User.Create&lt;/code&gt; if the app creates users. Use &lt;code&gt;Application.ReadWrite.OwnedBy&lt;/code&gt; if the app only needs to manage applications it owns. Avoid directory-wide permissions unless the operation truly has no narrower resource-specific permission (&lt;a href="https://learn.microsoft.com/graph/permissions-reference" rel="noopener noreferrer"&gt;Microsoft Graph permissions reference&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Second, decide whether the app needs delegated or app-only access. In delegated mode, remember that Graph evaluates the client permission &lt;strong&gt;and&lt;/strong&gt; the user's own authority. In app-only mode, remember that the app is the actor, so application permissions &lt;strong&gt;or role assignments&lt;/strong&gt; must be treated as production privilege (&lt;a href="https://learn.microsoft.com/graph/permissions-overview#permission-types" rel="noopener noreferrer"&gt;Microsoft Graph permission types&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Third, scope Entra roles as aggressively as the business process allows. If the operator manages one app, use app-registration scope. If the automation manages one population, use administrative-unit scope. If the app really needs tenant-wide scope, make that an explicit exception, not the default (&lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/manage-roles-portal" rel="noopener noreferrer"&gt;Assign Microsoft Entra roles at different scopes&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Fourth, treat owners as privileged. Application owners and enterprise application owners should be reviewed like role assignments, because they can control the object that holds the app's permissions and credentials (&lt;a href="https://learn.microsoft.com/entra/fundamentals/users-default-permissions#ownership-permissions" rel="noopener noreferrer"&gt;Default user permissions and ownership permissions&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Finally, keep the object model visible. The app registration and the service principal are not interchangeable. The app registration is the template. The service principal is the tenant-local instance that controls what the app can actually do in that tenant (&lt;a href="https://learn.microsoft.com/entra/identity-platform/app-objects-and-service-principals#relationship-between-application-objects-and-service-principals" rel="noopener noreferrer"&gt;Application and service principal relationship&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The sentence to bring to your next design review
&lt;/h2&gt;

&lt;p&gt;Least privilege for Microsoft Graph is not "we picked a smaller Graph permission."&lt;/p&gt;

&lt;p&gt;That is a good start. It is not the finish line.&lt;/p&gt;

&lt;p&gt;Least privilege for Microsoft Graph means the client has the narrowest Graph permission, the actor has the narrowest Entra authority, the role assignment has the narrowest scope, and the app object has the narrowest ownership model.&lt;/p&gt;

&lt;p&gt;Miss any one of those, and your beautiful permission list can still hide a control-plane problem.&lt;/p&gt;

&lt;p&gt;The industry is finally getting better Microsoft Graph permission granularity. Now app teams need to get equally serious about the Entra control plane around those permissions.&lt;/p&gt;

&lt;p&gt;Because the app that can touch your directory is only as least-privileged as the people and identities that can control the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/graph/permissions-overview" rel="noopener noreferrer"&gt;Overview of Microsoft Graph permissions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/graph/permissions-reference" rel="noopener noreferrer"&gt;Microsoft Graph permissions reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity-platform/app-objects-and-service-principals" rel="noopener noreferrer"&gt;Application and service principal objects in Microsoft Entra ID&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity/enterprise-apps/overview-assign-app-owners" rel="noopener noreferrer"&gt;Overview of enterprise application ownership in Microsoft Entra ID&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/fundamentals/users-default-permissions#object-ownership" rel="noopener noreferrer"&gt;What are the default user permissions in Microsoft Entra ID?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/manage-roles-portal" rel="noopener noreferrer"&gt;Assign Microsoft Entra roles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/graph/api/resources/unifiedroleassignment?view=graph-rest-1.0" rel="noopener noreferrer"&gt;unifiedRoleAssignment resource type&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/graph/api/rbacapplication-post-roleassignments?view=graph-rest-1.0" rel="noopener noreferrer"&gt;Create unifiedRoleAssignment&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>microsoftgraph</category>
      <category>microsoftentra</category>
      <category>leastprivilege</category>
      <category>appsecurity</category>
    </item>
    <item>
      <title>Enterprise architects: your overdue Entra decision is an agent CSA schema</title>
      <dc:creator>Anton Staykov</dc:creator>
      <pubDate>Thu, 23 Jul 2026 09:23:00 +0000</pubDate>
      <link>https://dev.to/astaykov/enterprise-architects-your-overdue-entra-decision-is-an-agent-csa-schema-2j5n</link>
      <guid>https://dev.to/astaykov/enterprise-architects-your-overdue-entra-decision-is-an-agent-csa-schema-2j5n</guid>
      <description>&lt;p&gt;If you are an enterprise architect working on Microsoft Entra and AI agents, your first overdue job is not another policy wizard, another dashboard, or another governance steering committee. It is schema design.&lt;/p&gt;

&lt;p&gt;Specifically, it is deciding how you classify non-human identities with &lt;a href="https://learn.microsoft.com/entra/fundamentals/custom-security-attributes-overview" rel="noopener noreferrer"&gt;custom security attributes in Microsoft Entra&lt;/a&gt;. Not eventually. Up front.&lt;/p&gt;

&lt;p&gt;I keep seeing the same pattern across customers of every size: teams move quickly on agent experimentation, they onboard identities, they test controls, and then they realize they have no consistent attribute language for policy scope. At that point, every policy becomes a naming convention problem in disguise.&lt;/p&gt;

&lt;p&gt;That is backwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  The control plane starts with classification
&lt;/h2&gt;

&lt;p&gt;Custom security attributes are not decorative metadata. They are tenant-scoped key-value classifications you can assign to users, enterprise applications (service principals), and agent identities that are modeled as a service principal subtype, with dedicated role and permission boundaries for who can define and assign them (&lt;a href="https://learn.microsoft.com/entra/fundamentals/custom-security-attributes-overview" rel="noopener noreferrer"&gt;overview&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/graph/api/resources/custom-security-attributes-overview?view=graph-rest-1.0" rel="noopener noreferrer"&gt;Graph model&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/entra/agent-id/agent-service-principals" rel="noopener noreferrer"&gt;agent identity service principal model&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That alone should change how architects think about them. This is not "nice to have taxonomy." This is policy input.&lt;/p&gt;

&lt;p&gt;Microsoft Entra Conditional Access for agents supports attribute-driven targeting with custom security attributes, and policy evaluation happens during token issuance and refresh, not just at policy authoring time (&lt;a href="https://learn.microsoft.com/en-us/entra/identity/conditional-access/agent-id#attribute-driven-conditional-access" rel="noopener noreferrer"&gt;Conditional Access for agents&lt;/a&gt;). In other words: if your classification is sloppy, your runtime decisions are sloppy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why agents raise the stakes
&lt;/h2&gt;

&lt;p&gt;You can say "an agent identity is still a service principal" and be technically correct. Microsoft Entra Agent ID is built on service principal infrastructure (&lt;a href="https://learn.microsoft.com/entra/agent-id/agent-service-principals" rel="noopener noreferrer"&gt;agent identities, service principals, and applications&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;You can also miss the point.&lt;/p&gt;

&lt;p&gt;Agent identity introduces a blueprint-centered model where one blueprint can represent many agents, where blueprint-level policy decisions can affect all derived identities, and where operational accountability (owners and sponsors) is part of the model (&lt;a href="https://learn.microsoft.com/entra/agent-id/key-concepts" rel="noopener noreferrer"&gt;key concepts&lt;/a&gt;). This is not how most architects historically modeled "traditional" workload identities.&lt;/p&gt;

&lt;p&gt;So yes, same substrate. Different operating semantics.&lt;/p&gt;

&lt;p&gt;That difference is exactly why CSA strategy for agents should be deliberate, not inherited by accident from old workload identity templates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reuse your existing workload schema or create a dedicated agent schema?
&lt;/h2&gt;

&lt;p&gt;This is the question every architecture board eventually reaches.&lt;/p&gt;

&lt;p&gt;The honest answer is conditional, not ideological.&lt;/p&gt;

&lt;p&gt;Reusing your existing workload schema has real advantages. It accelerates rollout, avoids duplicate governance machinery, and can be enough if your workload identity estate is already cleanly governed and your agent population is still limited.&lt;/p&gt;

&lt;p&gt;But here is the trade-off many teams underestimate: semantic collision.&lt;/p&gt;

&lt;p&gt;When the same attribute family has to describe static daemon services, integration apps, and AI agents with interactive and autonomous behavior patterns, naming starts to drift. Policy intent gets fuzzy. People read "criticality" or "environment" and assume shared meaning where meaning has already diverged. Then policy scopes become misleading even when the syntax is valid.&lt;/p&gt;

&lt;p&gt;That is the failure mode I care about most. Not policy syntax errors. Policy &lt;em&gt;intent&lt;/em&gt; errors.&lt;/p&gt;

&lt;p&gt;For regulated enterprises, or any organization expecting meaningful agent scale, my default recommendation is to establish a dedicated agent CSA schema and keep crosswalk mappings to legacy workload attributes where needed. You can still align governance. You just stop pretending the populations are identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architect's mistake: tactical granularity too early
&lt;/h2&gt;

&lt;p&gt;Most teams that finally commit to an agent schema make the same second mistake: they overengineer it.&lt;/p&gt;

&lt;p&gt;They try to encode every nuanced runtime trait into attributes on day one. They turn schema design into a cataloging exercise. They create dozens of values nobody will govern consistently.&lt;/p&gt;

&lt;p&gt;Do not do that.&lt;/p&gt;

&lt;p&gt;Strategic schema design means defining &lt;em&gt;long-lived control boundaries&lt;/em&gt;, not short-lived implementation detail. Microsoft documents clear limits and lifecycle constraints on custom security attributes, including tenant-level definition caps and immutable naming characteristics, which is exactly why restraint is a design virtue (&lt;a href="https://learn.microsoft.com/entra/fundamentals/custom-security-attributes-overview#limits-and-constraints" rel="noopener noreferrer"&gt;limits and constraints&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/graph/api/resources/custom-security-attributes-overview?view=graph-rest-1.0" rel="noopener noreferrer"&gt;Graph overview&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Start with coarse boundaries that support high-impact policy outcomes you know you will still care about two years from now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two coarse boundaries worth defining first
&lt;/h2&gt;

&lt;p&gt;First boundary: approved workload state.&lt;/p&gt;

&lt;p&gt;If your operating model says "no AI agent should sign in until explicitly approved," then encode exactly that as a first-class classification and enforce from there. This is the kind of architectural control objective that survives platform changes.&lt;/p&gt;

&lt;p&gt;Second boundary: department trust segmentation.&lt;/p&gt;

&lt;p&gt;If your operating model says "financial department agents should only authenticate within finance-defined trust boundaries," then encode that boundary at classification level before you layer finer controls. Entra policy and filtering surfaces are strongest when your classes reflect organizational trust zones, not implementation trivia (&lt;a href="https://learn.microsoft.com/en-us/entra/identity/conditional-access/agent-id#attribute-driven-conditional-access" rel="noopener noreferrer"&gt;attribute-driven Conditional Access for agents&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/entra/identity/conditional-access/agent-id/policy-autonomous-agents#allow-only-specific-agents-to-access-resources" rel="noopener noreferrer"&gt;autonomous agent policy walkthrough&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;These are strategic guardrails. The fine-grained boundaries should come from additional controls and deeper policy patterns, which we can cover in follow-up pieces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Governance is part of schema, not a separate phase
&lt;/h2&gt;

&lt;p&gt;Custom security attributes are security-sensitive by design, and Entra uses dedicated attribute roles for definition and assignment. Even Global Administrator is not automatically enough for these operations by default (&lt;a href="https://learn.microsoft.com/entra/fundamentals/custom-security-attributes-overview#custom-security-attribute-roles" rel="noopener noreferrer"&gt;custom security attribute roles&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/permissions-reference#attribute-assignment-administrator" rel="noopener noreferrer"&gt;permissions reference&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Treat that as a feature, not friction.&lt;/p&gt;

&lt;p&gt;Your schema decision should include explicit ownership for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;who defines agent attribute sets and values,&lt;/li&gt;
&lt;li&gt;who is allowed to assign and update values on agent identities,&lt;/li&gt;
&lt;li&gt;how schema changes are reviewed when agent programs evolve.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those answers are not defined with the schema, your taxonomy will drift faster than your policies can keep up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical position
&lt;/h2&gt;

&lt;p&gt;Could you reuse your current workload identity schema for AI agents? Yes.&lt;/p&gt;

&lt;p&gt;Should you, by default, in a large regulated enterprise? Usually no.&lt;/p&gt;

&lt;p&gt;The better default is a dedicated agent schema with a small number of coarse, policy-driving attributes. Make the boundaries obvious. Keep the vocabulary stable. Optimize for long-term policy clarity, not short-term convenience.&lt;/p&gt;

&lt;p&gt;This is architecture work in the most literal sense: defining the shape of control before control logic gets complicated.&lt;/p&gt;

&lt;p&gt;We already did this in this series when discussing dynamic consent accumulation and agent governance pressure in &lt;a href="https://dev.to/astaykov/the-overlooked-gem-in-microsoft-entra-that-gives-your-ai-agents-super-powers-3mde"&gt;The Overlooked Gem in Microsoft Entra&lt;/a&gt; and &lt;a href="https://dev.to/astaykov/your-agent-is-becoming-the-crown-jewel-soc-reviews-and-governance-for-the-dynamic-consent-era-23l3"&gt;Your Agent Is Becoming the Crown Jewel&lt;/a&gt;. This article just names the next prerequisite: the classification layer that keeps your controls honest.&lt;/p&gt;

&lt;p&gt;If you delay this decision, every downstream guardrail becomes harder to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/fundamentals/custom-security-attributes-overview" rel="noopener noreferrer"&gt;What are custom security attributes in Microsoft Entra ID?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/graph/api/resources/custom-security-attributes-overview?view=graph-rest-1.0" rel="noopener noreferrer"&gt;Overview of custom security attributes using Microsoft Graph API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity/enterprise-apps/custom-security-attributes-apps" rel="noopener noreferrer"&gt;Manage custom security attributes for an application&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/identity/conditional-access/agent-id#attribute-driven-conditional-access" rel="noopener noreferrer"&gt;Conditional Access for agents (attribute-driven Conditional Access)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/entra/identity/conditional-access/agent-id/policy-autonomous-agents#allow-only-specific-agents-to-access-resources" rel="noopener noreferrer"&gt;Conditional Access for autonomous agents (allow only specific agents to access resources)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/agent-id/key-concepts" rel="noopener noreferrer"&gt;Microsoft Entra Agent ID key concepts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/agent-id/agent-service-principals" rel="noopener noreferrer"&gt;Agent identities, service principals, and applications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity/role-based-access-control/permissions-reference#attribute-assignment-administrator" rel="noopener noreferrer"&gt;Role permissions reference: Attribute Assignment Administrator&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>entraagentid</category>
      <category>customsecurityattributes</category>
      <category>aiagents</category>
      <category>zerotrust</category>
    </item>
  </channel>
</rss>
