Skip to content
Application identity

Authenticate Entra Agent ID agents

Let an agent that runs as a Microsoft Entra Agent ID agent identity authenticate to Keycard with its own Entra token. No secret in the agent or in Keycard.

Microsoft Entra Agent ID gives each agent its own agent identity, created from an agent identity blueprint. An agent identity has no credential of its own: the blueprint holds the credential and requests tokens on the agent identity’s behalf. The token the agent identity ends up with is an ordinary Entra app-only token whose subject is the agent identity.

Keycard accepts that token the same way it accepts any deployed workload’s token. You register your tenant as a Provider and give each agent identity an Application with a Workload Identity credential. Neither the agent nor Keycard holds a secret.

flowchart LR
    A[Managed identity] -->|"Token for the blueprint's federated credential"| B[Blueprint]
    B -->|"Token with fmi_path = agent identity ID"| C[Agent identity]
    C -->|"App-only token, sub = agent identity ID"| D[Keycard]
    D -->|"Access token for the Resource"| E[Resource]
  1. The agent runs on an Azure service with a user-assigned managed identity. The blueprint has a federated identity credential that trusts that managed identity, so the managed identity’s token stands in for the blueprint’s credential.
  2. The agent requests the blueprint’s token with fmi_path set to the agent identity’s ID. fmi_path names the agent identity the blueprint is acting for.
  3. The agent presents the blueprint’s token as the agent identity’s client assertion and gets the agent identity’s own token for Azure Resource Manager. That token’s iss is your tenant’s v1 issuer, https://sts.windows.net/<tenant-id>/, and its sub is the agent identity’s ID.
  4. The agent sends that token to Keycard as its client assertion. Keycard matches the issuer and subject to a Workload Identity credential and authenticates the request as the agent identity’s Application.

Keycard matches on the subject, and the subject is the agent identity, so each agent identity is its own Application. The blueprint appears in the token only as the xms_par_app_azp claim. Microsoft recommends logging that claim and not authorizing on it, because a decision on the blueprint applies to every agent created from it.

  • Manager access to the Zone (see Roles & Permissions).
  • The Azure CLI, signed in to your tenant with az login --tenant <tenant-id>. When you sign in as a Global Administrator, the Azure CLI’s Microsoft Graph token can create every Entra object in this guide.
  • With a narrower Entra role, the Microsoft Graph permissions that Create an agent identity blueprint lists for each request.
  • An Azure service to run the agent on, such as Container Apps, App Service, or a virtual machine, with a user-assigned managed identity attached.
  • The Resource the agent will call, registered in your Zone.

If your Zone already has a Provider for your tenant’s v1 issuer, skip this section. Service principals and managed identities that authenticate to Keycard with tokens for Azure Resource Manager use the same issuer, so one Provider serves them and your agent identities.

  1. Add the Provider

    In Keycard Console, navigate to Providers → Add Provider:

    Field Value
    Name A label, for example Entra <tenant-name>
    Issuer URL https://sts.windows.net/<tenant-id>/, including the trailing slash

    Leave Client ID and Client Secret empty. Keycard discovers the tenant’s signing keys from the issuer’s OpenID Connect discovery document.

You can also create the Provider with the Keycard Terraform provider, using keycard_provider with oauth2.issuer set to the issuer above.

Create the blueprint and an agent identity

Section titled “Create the blueprint and an agent identity”

These requests go to Microsoft Graph through az rest, and every one needs the OData-Version: 4.0 header. Your user ID, for the sponsor and owner fields, is the output of az ad signed-in-user show --query id -o tsv.

  1. Create the blueprint

    A blueprint needs a sponsor, the person accountable for its agents. This request makes you its sponsor and owner:

    Terminal window
    az rest --method post --url https://graph.microsoft.com/v1.0/applications/ \
    --headers OData-Version=4.0 Content-Type=application/json \
    --body '{"@odata.type": "Microsoft.Graph.AgentIdentityBlueprint", "displayName": "<blueprint-name>",
    "sponsors@odata.bind": ["https://graph.microsoft.com/v1.0/users/<your-user-id>"],
    "owners@odata.bind": ["https://graph.microsoft.com/v1.0/users/<your-user-id>"]}'

    Note the appId in the response.

  2. Create the blueprint principal

    Terminal window
    az rest --method post --url https://graph.microsoft.com/v1.0/serviceprincipals/microsoft.graph.agentIdentityBlueprintPrincipal \
    --headers OData-Version=4.0 Content-Type=application/json \
    --body '{"appId": "<blueprint-app-id>"}'
  3. Trust the agent’s managed identity

    A federated identity credential on the blueprint lets the managed identity’s token stand in for the blueprint’s credential. The managed identity must be in the same tenant as the blueprint. Its subject is the managed identity’s principal ID:

    Terminal window
    az identity show --resource-group <resource-group> --name <managed-identity-name> --query principalId -o tsv
    az rest --method post --url https://graph.microsoft.com/v1.0/applications/<blueprint-app-id>/federatedIdentityCredentials \
    --headers OData-Version=4.0 Content-Type=application/json \
    --body '{"name": "<credential-name>", "issuer": "https://login.microsoftonline.com/<tenant-id>/v2.0",
    "subject": "<managed-identity-principal-id>", "audiences": ["api://AzureADTokenExchange"]}'
  4. Create the agent identity

    Terminal window
    az rest --method post --url https://graph.microsoft.com/beta/serviceprincipals/Microsoft.Graph.AgentIdentity \
    --headers OData-Version=4.0 Content-Type=application/json \
    --body '{"displayName": "<agent-name>", "agentIdentityBlueprintId": "<blueprint-app-id>",
    "sponsors@odata.bind": ["https://graph.microsoft.com/v1.0/users/<your-user-id>"]}'

    Note the id in the response. It is the agent identity’s ID: the sub of its tokens and the subject of its credential in Keycard. Repeat this step for each agent you create from the blueprint.

  1. Create an Application for the agent identity

    In Keycard Console, navigate to Applications → Add Application and name it after the agent identity. Don’t generate a client ID and secret for it.

  2. Add a Workload Identity credential

    On the Application’s Application Credentials tab, click Add credential and choose Workload Identity:

    Field Value
    Provider The Provider for your tenant
    Subject The agent identity’s ID

    Console marks Subject optional. Set it anyway, because a credential with no subject matches no token.

Then give the Application access the same way as any agent that acts as itself. For MCP tools behind a Gateway, that is an Access Policy that names the Application’s identifier, as Autonomous Agent Access describes.

The agent requests three tokens in Azure, then one from Keycard. This Python example uses msal for the blueprint’s request, which takes fmi_path, and azure-identity for the managed identity and the agent identity:

Terminal window
pip install "msal>=1.39" azure-identity httpx
import os
import httpx
import msal
from azure.identity import ClientAssertionCredential, ManagedIdentityCredential
TENANT_ID = os.environ["ENTRA_TENANT_ID"]
AGENT_IDENTITY_ID = os.environ["AGENT_IDENTITY_ID"]
TOKEN_EXCHANGE_SCOPE = "api://AzureADTokenExchange/.default"
managed_identity = ManagedIdentityCredential(client_id=os.environ["MANAGED_IDENTITY_CLIENT_ID"])
blueprint = msal.ConfidentialClientApplication(
client_id=os.environ["BLUEPRINT_APP_ID"],
client_credential={"client_assertion": lambda: managed_identity.get_token(TOKEN_EXCHANGE_SCOPE).token},
authority=f"https://login.microsoftonline.com/{TENANT_ID}",
)
def blueprint_token() -> str:
# fmi_path names the agent identity the blueprint is acting for.
result = blueprint.acquire_token_for_client([TOKEN_EXCHANGE_SCOPE], fmi_path=AGENT_IDENTITY_ID)
if "access_token" not in result:
raise RuntimeError(f"{result.get('error')}: {result.get('error_description')}")
return result["access_token"]
agent_identity = ClientAssertionCredential(
tenant_id=TENANT_ID,
client_id=AGENT_IDENTITY_ID,
func=blueprint_token,
)
entra_token = agent_identity.get_token("https://management.azure.com/.default").token
response = httpx.post(
f"{os.environ['KEYCARD_ISSUER'].rstrip('/')}/oauth/2/token",
data={
"grant_type": "client_credentials",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": entra_token,
"resource": os.environ["KEYCARD_RESOURCE_URL"],
},
headers={"User-Agent": "<agent-name>"},
)
response.raise_for_status()
access_token = response.json()["access_token"]
Variable Value
ENTRA_TENANT_ID Your tenant ID
MANAGED_IDENTITY_CLIENT_ID The managed identity’s client ID, not its principal ID
BLUEPRINT_APP_ID The blueprint’s appId
AGENT_IDENTITY_ID The agent identity’s id
KEYCARD_ISSUER Your Zone’s Issuer URL, from Settings → Connection
KEYCARD_RESOURCE_URL The Resource the agent needs a token for

The agent identity’s token is for Azure Resource Manager (https://management.azure.com/.default). That resource exists in every tenant, the agent identity needs no Azure role to get a token for it, and its tokens carry the v1 issuer your Provider trusts. The Keycard request has no client ID, because Keycard identifies the Application from the token. Send the access token to the Resource as a bearer token.

A blueprint usually has many agent identities, and each needs its own Application and credential. This configuration lists a blueprint’s agent identities from Microsoft Graph with the msgraph provider and registers each one with the Keycard Terraform provider:

terraform {
required_providers {
keycard = {
source = "keycardai/keycard"
version = "~> 0.7"
}
msgraph = {
source = "microsoft/msgraph"
version = "~> 0.6"
}
}
}
variable "zone_id" { type = string }
variable "tenant_id" { type = string }
variable "blueprint_app_id" { type = string }
variable "identifier_prefix" { type = string } # For example urn:<org>:agent
# The Provider for your tenant's v1 issuer, not the /v2.0 one Entra sign-in uses
data "keycard_provider" "entra" {
zone_id = var.zone_id
identifier = "https://sts.windows.net/${var.tenant_id}/"
}
data "msgraph_resource" "agent_identities" {
url = "servicePrincipals/microsoft.graph.agentIdentity"
api_version = "v1.0"
headers = {
"OData-Version" = "4.0"
}
query_parameters = {
"$select" = ["id", "displayName", "agentIdentityBlueprintId"]
}
response_export_values = {
agent_identities = "value[].{id: id, display_name: displayName, blueprint_app_id: agentIdentityBlueprintId}"
}
}
locals {
agent_identities = {
for agent in coalesce(data.msgraph_resource.agent_identities.output.agent_identities, []) :
agent.id => agent.display_name
if agent.blueprint_app_id == var.blueprint_app_id
}
}
resource "keycard_application" "agent" {
for_each = local.agent_identities
zone_id = var.zone_id
name = each.value
identifier = "${var.identifier_prefix}:${each.key}"
}
resource "keycard_application_workload_identity" "agent" {
for_each = local.agent_identities
zone_id = var.zone_id
application_id = keycard_application.agent[each.key].id
provider_id = data.keycard_provider.entra.id
subject = each.key
}

The keycard_provider lookup finds the Provider by its identifier, which Console and the Keycard provider set to the Issuer URL unless you give the Provider a different one. identifier_prefix starts each Application’s identifier, so with urn:<org>:agent an agent identity’s Application gets the identifier urn:<org>:agent:<agent-identity-id>.

The msgraph provider can use your Azure CLI sign-in, and the identity it runs as must be able to read agent identities in your tenant. The Keycard provider takes a Service Account’s client ID and secret, for example from the KEYCARD_CLIENT_ID and KEYCARD_CLIENT_SECRET environment variables. Each apply brings the Zone in line with the blueprint: an agent identity created since the last apply gets an Application and credential, and one deleted in Entra loses them. Access still comes from your Access Policies, so grant each new Application access the same way as the first.

Run the agent, then open its Application in Keycard Console and select the Activity tab. Each credential Keycard issued to the agent identity is listed there. A request that a policy denied shows in the Audit Log with the agent identity’s Application as the actor.

invalid_client: No token application credential configured for provider ‘…’ and subject ‘…’

Section titled “invalid_client: No token application credential configured for provider ‘…’ and subject ‘…’”

Keycard has no Workload Identity credential for the subject in the token. If the subject is the agent identity’s ID, add the credential as in Register the agent identity in Keycard, or apply the Terraform above.

Send Keycard the agent identity’s token, from the request with client_id set to the agent identity. The blueprint’s token has the v2 issuer, https://login.microsoftonline.com/<tenant-id>/v2.0, and a sub that isn’t the agent identity’s ID, so no credential matches it.

Entra refuses the blueprint’s token request

Section titled “Entra refuses the blueprint’s token request”

The federated identity credential’s subject must be the managed identity’s principal ID, and the agent must pass the managed identity’s client ID. They are different values, and each place takes a different one.

The managed identity must also be in the blueprint’s tenant. Microsoft doesn’t let an application trust a managed identity from another tenant (see Configure an application to trust a managed identity). An agent that runs in another tenant needs a managed identity in that tenant.