DEV Community

Anton Staykov
Anton Staykov

Posted on

Agent-to-agent authorization with Entra Agent ID on AWS AgentCore

The first two versions of my Amazon Web Services (AWS) proof of value established cross-cloud Microsoft Entra Agent ID and removed the long-lived client secret. Version 3 answers the question customers now ask most often: what happens when one agent calls another?

It is tempting to treat Agent2Agent (A2A) as a new identity problem because the caller and resource are both agents. It is not. A2A changes the communication contract. It does not replace the authentication and authorization contract.

The sample is astaykov/agentid-agentcore. Release v3.0 adds that missing hop. Agent 1 delegates directory work to Agent 2 on behalf of a signed-in user. Agent 3 calls Agent 2 autonomously. Both use A2A, both present Microsoft Entra access tokens, and they receive deliberately different capabilities.

Version 3 makes agent-to-agent authorization the point

The first article proved that an AWS-hosted agent can use Entra Agent ID to reach Microsoft and AWS resources. Its purpose was cross-cloud identity portability.

The second article replaced the blueprint client secret with an AWS Security Token Service (STS) assertion and a federated identity credential (FIC). Its purpose was secretless workload authentication.

Version 3 begins where those two stop. It keeps the cross-cloud federation and introduces two more agents:

  • Agent 1 remains the user-facing orchestrator in AgentCore Runtime.
  • Agent 2 is an A2A specialist in a second AgentCore Runtime.
  • Agent 3 is an autonomous caller running in AWS Lambda and triggered by Amazon EventBridge Scheduler.

The v3 repository documentation makes the new authorization split explicit. Agent 1 calls Agent 2 with delegated user authority, while Agent 3 calls the same agent with application authority. Agent 2 exposes directory tools only to the delegated path.

That is what v3 adds. Blueprint 2 becomes a resource boundary where two kinds of principals arrive through one protocol and must not collapse into one permission set.

A2A standardizes the message, not the trust decision

The A2A specification defines a common model for agent discovery, messages, tasks, artifacts, and protocol bindings. An Agent Card advertises what an agent can do and how a client can reach it. A message/send operation carries work from one agent to another.

None of that turns an Agent Card into a permission grant.

AWS makes the separation visible in its AgentCore A2A deployment model and protocol contract. An A2A server exposes its Agent Card at /.well-known/agent-card.json, receives JSON-RPC requests at the root path, and can authenticate clients with OAuth 2.0 bearer tokens or AWS Signature Version 4. AgentCore provides the transport and an authentication gate. The agent application still owns the claim-level authorization decision.

The sample currently pins A2A protocol version 0.3.0, matching the implementation documented in the v3 setup guide. The current A2A specification lists 1.0.0 as the latest released version. That version difference matters for interoperability work, but it does not change the identity principle: the protocol relies on standard web security rather than inventing an agent-specific token.

flowchart LR
    User["User + browser"] -->|"Token for Blueprint 1"| A1["Agent 1<br/>orchestrator"]
    A1 -->|"A2A delegated token"| A2["Agent 2<br/>specialist"]
    Scheduler["EventBridge"] --> A3["Agent 3<br/>autonomous Lambda"]
    A3 -->|"A2A app-only token"| A2
    A2 -->|"Delegated token"| MCP["Microsoft MCP server"]
    R1["AWS role 1"] -.->|"FIC"| B1["Blueprint 1"]
    R2["AWS role 2"] -.->|"FIC"| B2["Blueprint 2"]
    R3["AWS role 3"] -.->|"FIC"| B3["Blueprint 3"]

The A2A request does not contain a magical statement that one agent trusts another. It contains a normal bearer token for Blueprint 2, and that resource validates normal Microsoft Entra claims. The task is agent-shaped. The trust contract is still OAuth.

Protocol interoperability does not imply authorization equivalence. Two callers can speak the same A2A dialect and still belong on opposite sides of a capability boundary.

Separate blueprints make three workload trust roots visible

All three agents belong to the same proof-of-value deployment, but they do not all use the same compute surface. Agent 1 and Agent 2 run in AgentCore Runtime, while Agent 3 runs in Lambda. They could still reference agent identities created from one shared blueprint because a blueprint is a reusable template for multiple agent identities.

I deliberately did not model them that way.

Each agent has its own blueprint, its own child agent identity, and its own AWS execution role. Each blueprint has one FIC whose subject is the matching AWS role Amazon Resource Name (ARN). Blueprint 1 trusts ExecutionRoleArn, Blueprint 2 trusts SpecialistRoleArn, and Blueprint 3 trusts AutonomousRoleArn, as recorded in the Entra setup guide.

Why spend three blueprints where one might function?

Because the sample is meant to expose the control planes. Microsoft documents that the blueprint holds the credential while the child agent identity has no credential of its own. Reusing one blueprint would therefore reuse one credential-bearing parent across three operational roles. That can be a legitimate production design. It is the wrong teaching design when the goal is to inspect three independent workload attestations.

The separate model makes a one-to-one relationship visible:

  • an AWS execution role identifies the running workload;
  • a FIC tells one blueprint which AWS role assertion it accepts;
  • the blueprint acquires a token for one child agent identity; and
  • that agent identity appears as the caller or actor in downstream authorization.

The cost is more Entra objects and more configuration. The benefit is a smaller conceptual and operational blast radius. Rotating, disabling, or misconfiguring Blueprint 3 does not change the credential trust for Agent 2.

A shared blueprint groups agents by kind or runtime. A separate blueprint isolates credential ownership and blast radius. The right production choice depends on whether those agents are interchangeable instances or independent security principals. For this demonstration, clarity wins.

Blueprint 2 is an API surface, not just Agent 2's credential holder

The most important setup step in v3 is easy to misread. Blueprint 2 authenticates Agent 2 through its FIC, but it also represents the resource API that Agent 1 and Agent 3 request tokens for. Microsoft documents that a blueprint receiving incoming requests needs an identifier URI and OAuth scope, just like any other protected web API.

The sample gives Blueprint 2:

  • the identifier URI api://{blueprint-2-client-id};
  • access-token version 2;
  • delegated scope user_impersonation;
  • user role Agent2.Tools.User; and
  • application role Agent2.Chat.Application.

The relevant shape is below. The actual setup uses generated globally unique identifiers for every scope and role.

{
  "identifierUris": ["api://{blueprint-2-client-id}"],
  "api": {
    "requestedAccessTokenVersion": 2,
    "oauth2PermissionScopes": [
      {
        "id": "{scope-guid}",
        "value": "user_impersonation",
        "type": "User",
        "isEnabled": true
      }
    ]
  },
  "appRoles": [
    {
      "id": "{user-role-guid}",
      "value": "Agent2.Tools.User",
      "allowedMemberTypes": ["User"],
      "isEnabled": true
    },
    {
      "id": "{application-role-guid}",
      "value": "Agent2.Chat.Application",
      "allowedMemberTypes": ["Application"],
      "isEnabled": true
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Why define both a scope and a user role for the delegated path?

They answer different questions. The scope says that the client agent has delegated consent to call Blueprint 2 on behalf of a user. The user role says that this user is assigned to the directory-tool capability exposed by Agent 2. Microsoft recommends that a protected API verify scopes for calls made on behalf of users and app roles for application callers. The same guidance allows a user-assigned app role to add a second claim-based boundary to delegated access.

The application role answers a third question: may this autonomous principal call Agent 2 without a user? Microsoft Entra emits assigned app-role values in the roles claim, and app roles can be assigned to users, groups, or another application's service principal.

Definitions are not grants. Creating user_impersonation does not consent Agent Identity 1 to it. Creating Agent2.Tools.User does not assign a user. Creating Agent2.Chat.Application does not authorize Agent Identity 3.

The setup therefore creates three separate grants:

  1. An oauth2PermissionGrant from the Agent Identity 1 to the Blueprint 2 resource service principal for user_impersonation.
  2. A user or group assignment to Agent2.Tools.User on the Blueprint 2 resource service principal.
  3. An app-role assignment from Agent Identity 3 to Agent2.Chat.Application on the Blueprint 2 resource service principal.

Microsoft Graph distinguishes application IDs from service-principal object IDs here. The oAuth2PermissionGrant resource expects the client and resource service-principal object IDs. The app-role assignment API likewise uses the principal, resource, and role IDs.

That distinction is not administrative trivia. It determines which principal receives the claim. A delegated grant on the blueprint principal is not a grant to Agent Identity 1. An application role assigned to Blueprint 3 is not automatically a role assigned to Agent Identity 3. Authorize the principal that will actually appear in the token exchange.

The delegated path composes client consent, user assignment, and caller identity

The user-delegated path begins before A2A. The browser obtains a token for Blueprint 1 and invokes Agent 1. Agent 1 then needs a new token whose audience is Blueprint 2. Forwarding the browser token would be wrong because an access token is valid only for its intended audience, and Microsoft explicitly warns against relaying middle-tier access tokens to another resource.

Agent 1 first authenticates through Blueprint 1. Its AWS role obtains a short-lived AWS assertion, Blueprint 1's FIC accepts that assertion, and the blueprint obtains the federated managed identity (FMI) token bound to Agent Identity 1. Agent Identity 1 then performs the Microsoft Entra agent on-behalf-of (OBO) flow using the user's Blueprint 1 token as the assertion and api://{blueprint-2-client-id}/user_impersonation as the requested downstream scope.

The resulting token for Agent 2 carries the user's identity, the delegated scope, the role assigned to that user on Blueprint 2, and the authorized-party identity of Agent Identity 1. Agent 2 validates all of them.

sequenceDiagram
    participant U as User
    participant A1 as Agent 1
    participant E as Microsoft Entra
    participant A2 as Agent 2
    participant M as MCP server

    U->>A1: Token for Blueprint 1
    A1->>E: AWS assertion + Agent 1 FMI/OBO exchange
    E-->>A1: Blueprint 2 token with scp + user role
    A1->>A2: A2A message/send + bearer token
    A2->>E: Agent 2 FMI/OBO exchange
    E-->>A2: Delegated MCP token
    A2->>M: Directory tool call

The v3 authorizer does not treat scp as sufficient. It requires:

  • aud to identify Blueprint 2;
  • scp to contain user_impersonation;
  • roles to contain Agent2.Tools.User;
  • azp to equal Agent Identity 1; and
  • the token not to identify itself as an app-only token.

This composition blocks several accidental privilege paths. A user assigned to the role cannot invent a client that lacks delegated consent. A consented client cannot use directory tools for a user who lacks the role. A different client cannot replay an otherwise valid Blueprint 2 token and pass the caller check.

After that first A2A authorization, Agent 2 still does not forward the Blueprint 2 token to the Microsoft MCP server. It performs another OBO exchange through Blueprint 2 and Agent Identity 2 for the MCP resource scopes. The OBO protocol exists precisely for this audience transition: each middle tier receives a token for itself, then requests a different token for the next resource.

There are two authorization boundaries in this chain. Blueprint 2 controls whether the user and Agent 1 may invoke Agent 2's directory skill. The MCP resource controls what Agent 2 may do with the user's delegated authority. A2A delegation does not erase downstream consent. It adds another resource to the chain.

The autonomous path receives an app role and no inherited tools

Agent 3 has no signed-in user. EventBridge Scheduler invokes its Lambda under AWS controls, then Agent 3 uses its own AWS role, Blueprint 3 FIC, and Agent Identity 3 to request an app-only token for Blueprint 2.

The Microsoft Entra autonomous agent flow still starts with the blueprint credential and an FMI token for the child agent identity. The second exchange uses client credentials and requests api://{blueprint-2-client-id}/.default. For client-credentials flows, .default resolves the application permissions already granted for that resource.

Agent Identity 3 has exactly one such assignment on Blueprint 2: Agent2.Chat.Application.

No scp claim is expected because there is no user delegation. Agent 2 instead requires:

  • roles to contain Agent2.Chat.Application;
  • azp to identify Agent Identity 3;
  • oid to identify Agent Identity 3; and
  • the requested skill to be chat.

The sample's authorization implementation reduces the distinction to an explicit branch:

if claims.get("scp") is not None:
    require_scope(claims, "user_impersonation")
    require_role(claims, "Agent2.Tools.User")
    require_caller(claims, agent1_id)
    mode = "delegated"
else:
    require_role(claims, "Agent2.Chat.Application")
    require_caller(claims, agent3_id)
    require_subject(claims, agent3_id)
    mode = "application"

if skill == "directory" and mode != "delegated":
    deny()
Enter fullscreen mode Exit fullscreen mode

The actual code validates the signature with Microsoft Entra signing keys, issuer, audience, expiry, issued-at time, tenant, and claim types before this authorization branch. That follows the protected-API rule that authentication alone is insufficient: the API must verify the expected permission claims.

Notice what is not in the autonomous path. There is no user role, no delegated scope, and no MCP token exchange. Agent 2 constructs a tool-free agent for chat; an app-only caller asking for directory is rejected before a model or tool is invoked. Prompt text cannot promote the caller into another authorization mode because the tool list is selected after claim validation, not by the model.

This is the useful A2A result. Agent 3 can collaborate with Agent 2 without inheriting the directory authority available to Agent 1's user-delegated path. Agent-to-agent connectivity is not transitive permission.

Authorization must fail before the model gets a vote

An Agent Card can advertise a skill. That does not mean every authenticated caller may execute it. The A2A specification deliberately aligns security with standard enterprise web practices, and AgentCore supports OAuth bearer authentication at the runtime boundary. The sample adds the missing resource-server checks inside Agent 2.

That ordering matters:

  1. AgentCore verifies the inbound JWT configuration before invoking the runtime.
  2. Agent 2 validates the token signature, issuer, audience, lifetime, and required identity claims.
  3. Agent 2 distinguishes delegated from application mode.
  4. Agent 2 checks the scope, role, expected caller, and requested skill.
  5. Only then does it construct a model with either directory tools or an empty tool list.

AgentCore currently forwards the A2A bearer token through an allowlisted runtime header so the application can perform those checks. AWS documents that custom headers can reach the runtime only when allowed by its configuration, and that the Authorization header requires a custom JWT authorizer. The sample accepts the AgentCore-compatible forwarded header and never puts the token into the A2A message, Agent Card, prompt, or logs.

The negative tests are as important as the successful calls. The v3 demonstration guide checks a user without the Blueprint 2 role, Agent 1 without delegated consent, Agent 3 without its application role, an app-only request for the directory skill, and tokens with the wrong issuer, audience, signature, or lifetime.

Those failures locate the trust boundary:

  • missing consent fails token acquisition;
  • missing assignment fails token issuance or resource authorization;
  • the wrong caller fails claim authorization;
  • the wrong skill fails capability authorization; and
  • an invalid token fails authentication.

There is no single "A2A authorized" bit. There is a chain of independently testable decisions, and every one happens before a model can interpret the request.

The first version of this proof of value showed that Entra Agent ID is not limited to Microsoft-hosted agents or Microsoft-hosted resources. The second showed that cross-cloud identity does not require a shared client secret. Version 3 closes the next gap: one governed agent can call another through an open protocol without abandoning scopes, roles, consent, workload attestation, or resource-specific enforcement.

A2A is not a shortcut around your authorization model. It is where that model becomes visible.

References

Top comments (0)