From Blueprint to Token: How Entra Agent Identity Inheritance Really Works
For ‘lazy’ people: TLDR at the end of this blog :-)
My previous blog described the four-object model* of the Entra agent identity platform.
Microsoft Entra Agent ID introduces four new object types: the agent identity blueprint, the agent identity blueprint principal, the agent identity, and the agent user (impersonates ‘real’ user).
For simplicity we will only use two objects; the agent blueprint and the agent identity. The agent identity inherits capabilities from the agent blueprint that creates the agent identity.
What exactly is inherited? That’s what we will dive into in this blog.
Security for agents is enforced at the blueprint level, while execution happens at the agent identity level.
The agent blueprint is the:
1️⃣ IT-container which defines authentication (credentials; FIC, certificate or client secret) and governance, managed by the Owner role (IT-admin or Power Platform Admin).
2️⃣ Template for Inheritance from agent blueprint to agent identity created from, and connected to the blueprint
Let’s dive deeper in the Inheritance options of the agent blueprint.
Inheritance
This part describes the inheritance from the agent blueprint to the agent identity (and also what is not inherited).
1️⃣Protocol properties (always inherited)
2️⃣Delegated permissions (conditionally inherited)
3️⃣What is NOT inherited (direct-only)
1. Protocol properties (always inherited)
Agent identities inherit protocol properties from the parent agent blueprint through the parent-child relationship (agentIdentityBlueprintId).
Unlike classic (service principals) agents that operate independently, modern agent identities require their parent agent identity blueprint for impersonation and token exchange operations.
The token authentication flow works as follows: the blueprint authenticates using its credential (FIC, certificate, or secret), gets an exchange token T1, then uses T1 to impersonate the agent identity and produce a access (resource) token T2 the agent identity can actually use for resource access.
This means the blueprint defines the OAuth 2.0 app configuration (Identifier URIs, supported grant types, scope definitions) that all child identities share as their base.
The Identifier URI
The identifier URI is the globally unique name for the agent as a resource (e.g. what other agents use to request tokens to talk to the agent, not when calling out from the agent). The identifier URI and scope for agents are created when the agent blueprint is created, see my previous blog.
Usage example: when Agent B wants to call Agent A, it needs to know the Agent’s URI to put in the scope field of its token request. The URI lives on the blueprint, and all child agent identities share it as their API surface.
PATCH https://graph.microsoft.com/beta/applications/<blueprint-appId>
{
"identifierUris": ["api://<blueprint-appId>"]
}Scope definitions (OAuth2PermissionScopes)
Scope definitions are the permissions the agent exposes to calling agents. It defines what capabilities calling agents (or users) can be granted when they call into the agent.
The scope is defined on the blueprint (access_agent) with an admin consent description, display name, admin or user consent, and a value string (what is visible in the token).
{
"adminConsentDescription": "Allow the app to access the agent on behalf of the signed-in user.",
"adminConsentDisplayName": "Access agent",
"id": "<guid>",
"isEnabled": true,
"type": "User",
"value": "access_agent"
}Supported grant types
Agent identity blueprints support client_credentials enabling secure token acquisition for impersonation scenarios. The jwt-bearer grant type facilitates token exchanges in OBO (On-Behalf-Of) scenarios, allowing for delegation patterns. refresh_token grants enable background operations with user context, supporting long-running processes that maintain user authorization.
Critically, agents do not support interactive /authorize flows and redirect URIs; authentication is performed via programmatic token exchanges (e.g. client credentials and JWT-bearer flows).
These constraints are set at the blueprint level and apply to all child identities.
2. Delegated permissions (conditionally inherited)
Agent identities can inherit delegated permissions from the parent agent identity blueprint.
Without inheritance, every time you create a new agent identity, you would need to go through an admin consent flow for that specific identity to grant permissions (e.g. Mail.Read, User.Read).
The key thing to understand is that delegated permission inheritance is not controlled by a single toggle, but by a combination of configurations:
1️⃣ The blueprint application object (inheritablePermissions)
This defines which scopes, from which APIs, are eligible to flow down to child identities.
2️⃣ The blueprint service principal (OAuth2PermissionGrants)
This provides the required admin consent for those scopes against the target resource (e.g. Microsoft Graph).
When both are configured, Entra applies the delegated permissions dynamically during token issuance when the agent identity operates through the blueprint.
⚠️ Important
The term InheritDelegatedPermissions is used in documentation as a conceptual description of this behavior, but it is not an actual property on the agent identity or service principal that can be configured or patched.
3. What is NOT inherited (direct-only)
Direct permission assignment: Permissions can be assigned directly to an agent identity for instance-specific access requirements.
Role assignment: Agent identities can be assigned Azure RBAC roles and directory roles.
Agent identity blueprints cannot be assigned Azure RBAC roles.
Owner and Sponsor accountability are set separately, the Owner is the technical administrator responsible for operational management, and the Sponsor is the business representative accountable for the agent’s purpose and lifecycle.
An agent blueprint often has an Owner; an agent identity often has a Sponsor.
If you know you do, if you understand you teach
Theory is one, but configuring yourself and see the output makes it the best learning experience.
Scenario: We have a Copilot Studio HR agent blueprint. We want all agent identities created from the blueprint to automatically inherit
Mail.ReadandCalendars.Read(Microsoft Graph) without each identity needing its own consent.
Prerequisites
# Install beta module (required - this feature is in preview)
Install-Module Microsoft.Graph.Beta.Applications -Scope CurrentUser -Force
# Connect with all needed scopes
Connect-MgGraph `
-Scopes "AgentIdentityBlueprint.Create",
"AgentIdentityBlueprint.ReadWrite.All",
"AgentIdentityBlueprint.AddRemoveCreds.All",
"AgentIdentityBlueprintPrincipal.Create",
"DelegatedPermissionGrant.ReadWrite.All",
"Application.ReadWrite.All",
"User.Read" `
-TenantId "<your-tenant-id>"1️⃣Create the blueprint
$currentUser = Get-MgContext | Select-Object -ExpandProperty Account
$user = Get-MgUser -UserId $currentUser$blueprintBody = @{
"@odata.type" = "Microsoft.Graph.AgentIdentityBlueprint"
"displayName" = "HR Agent Blueprint"
"sponsors@odata.bind" = @("https://graph.microsoft.com/v1.0/users/$($user.Id)")
"owners@odata.bind" = @("https://graph.microsoft.com/v1.0/users/$($user.Id)")
} | ConvertTo-Json -Depth 5
$blueprint = Invoke-MgGraphRequest `
-Method POST `
-Uri "https://graph.microsoft.com/beta/applications/graph.agentIdentityBlueprint" `
-Headers @{ "OData-Version" = "4.0"; "Content-Type" = "application/json" } `
-Body $blueprintBody `
-OutputType PSObject
$blueprintAppId = $blueprint.appId # e.g. "bc057821-f236-49d6-9f2c-1ebf43e9437a"
$blueprintObjId = $blueprint.id
Write-Host "Blueprint appId: $blueprintAppId"2️⃣Create the blueprint principal and grant consent
# 2a. Create the blueprint service principal (required before any identity can be created)
$bpPrincipalBody = @{ appId = $blueprintAppId } | ConvertTo-Json
Invoke-MgGraphRequest `
-Method POST `
-Uri "https://graph.microsoft.com/beta/serviceprincipals/graph.agentIdentityBlueprintPrincipal" `
-Headers @{ "OData-Version" = "4.0"; "Content-Type" = "application/json" } `
-Body $bpPrincipalBody# 2b. Retrieve the blueprint service principal's object ID
$bpSP = Invoke-MgGraphRequest `
-Method GET `
-Uri "https://graph.microsoft.com/beta/servicePrincipals?`$filter=appId eq '$blueprintAppId'" `
-Headers @{ "OData-Version" = "4.0" } `
-OutputType PSObject
$bpSPObjectId = $bpSP.value[0].id
Write-Host "Blueprint SP Object ID: $bpSPObjectId"
# 2c. Get Microsoft Graph service principal resource ID
$graphSP = Get-MgServicePrincipal -Filter "displayName eq 'Microsoft Graph'"
$graphSPId = $graphSP.Id
# Microsoft Graph App ID (constant across all tenants)
# 00000003-0000-0000-c000-000000000000
# 2d. Grant delegated permissions (Mail.Read + Calendars.Read) to the blueprint SP
# This is the OAuth2PermissionGrant that makes scopes eligible for inheritance
$grantBody = @{
clientId = $bpSPObjectId # blueprint service principal
consentType = "AllPrincipals" # admin consent for all users
resourceId = $graphSPId # Microsoft Graph service principal
scope = "Mail.Read Calendars.Read User.Read"
} | ConvertTo-Json
Invoke-MgGraphRequest `
-Method POST `
-Uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants" `
-Headers @{ "Content-Type" = "application/json" } `
-Body $grantBody
Write-Host "OAuth2PermissionGrant created for blueprint SP"3️⃣ Declare inheritable permissions on the blueprint app object
This step uses the inheritablePermissions navigation property on the agentIdentityBlueprint application resource. The resourceAppId must be a valid GUID , invalid GUIDs cause a 400 error. The enumerated pattern is recommended to start with.
# Microsoft Graph App ID (constant): 00000003-0000-0000-c000-000000000000
$graphAppId = "00000003-0000-0000-c000-000000000000"$inheritBody = @{
"resourceAppId" = $graphAppId
"inheritableScopes" = @{
"@odata.type" = "microsoft.graph.enumeratedScopes"
"scopes" = @("User.Read", "Mail.Read", "Calendars.Read")
}
} | ConvertTo-Json -Depth 5
Invoke-MgGraphRequest `
-Method POST `
-Uri "https://graph.microsoft.com/beta/applications/microsoft.graph.agentIdentityBlueprint/$blueprintAppId/inheritablePermissions" `
-Headers @{ "OData-Version" = "4.0"; "Content-Type" = "application/json" } `
-Body $inheritBody
Write-Host "inheritablePermissions configured on blueprint"To add more scopes later, use PATCH instead of POST (POST would return a 409 conflict):
$patchBody = @{
"inheritableScopes" = @{
"@odata.type" = "microsoft.graph.enumeratedScopes"
"scopes" = @("User.Read", "Mail.Read", "Calendars.Read", "User.ReadBasic.All")
}
} | ConvertTo-Json -Depth 5Invoke-MgGraphRequest `
-Method PATCH `
-Uri "https://graph.microsoft.com/beta/applications/microsoft.graph.agentIdentityBlueprint/$blueprintAppId/inheritablePermissions/$graphAppId" `
-Headers @{ "OData-Version" = "4.0"; "Content-Type" = "application/json" } `
-Body $patchBody4️⃣ Create an agent identity
$agentBody = @{
"@odata.type" = "Microsoft.Graph.AgentIdentity"
"displayName" = "HR Agent - UK Region"
"agentIdentityBlueprintId" = $blueprintAppId
"sponsors@odata.bind" = @("https://graph.microsoft.com/v1.0/users/$($user.Id)")
} | ConvertTo-Json -Depth 5$agentIdentity = Invoke-MgGraphRequest `
-Method POST `
-Uri "https://graph.microsoft.com/beta/serviceprincipals/Microsoft.Graph.AgentIdentity" `
-Headers @{ "OData-Version" = "4.0"; "Content-Type" = "application/json" } `
-Body $agentBody `
-OutputType PSObject
$agentSPId = $agentIdentity.id
Write-Host "Agent Identity SP Object ID: $agentSPId"5️⃣ No opt-in required (how inheritance actually works)
There is no explicit step to enable delegated permission inheritance on the agent identity.
The Microsoft documentation uses InheritDelegatedPermissions as a conceptual name for this behavior, but it is not a literal property or flag that can be configured or patched on the agent identity service principal.
This is a documentation gap in the current preview state of Entra Agent ID.
Instead, inheritance is fully automatic and driven by two configurations on the blueprint:
1️⃣ inheritablePermissions on the blueprint application object
Defines which scopes are allowed to flow down to child identities
2️⃣ OAuth2PermissionGrant on the blueprint service principal
Provides admin consent for those scopes against the target resource
When both are in place and the FIC-based impersonation flow runs, Entra dynamically merges the delegated scopes into the issued token for the agent identity.
⚠️ Important
The agent identity itself does not store these permissions , they are applied at runtime during token issuance.
6️⃣Verify the configuration
The blueprint service principal (e04fcab1-...) holds a tenant-wide admin consent grant (AllPrincipals) for Mail.Read Calendars.Read User.Read against the Microsoft Graph resource (ad75500a-...). This is the prerequisite that makes those scopes eligible to flow down.
# Check the blueprint's declared inheritable permissions
$check = Invoke-MgGraphRequest `
-Method GET `
-Uri "https://graph.microsoft.com/beta/applications/microsoft.graph.agentIdentityBlueprint/$blueprintAppId/inheritablePermissions" `
-Headers @{ "OData-Version" = "4.0" } `
-OutputType PSObject
$check.value | ConvertTo-Json -Depth 5The blueprint app object has User.Read, Mail.Read, and Calendars.Read declared as enumerated inheritable scopes for Microsoft Graph (00000003-0000-0000-c000-000000000000).
The reason people get confused here is they go to the agent identity and look for permissions, find nothing, and assume something is broken. This is by design , agent identities inherit scopes from their parent blueprint during token issuance, not ahead of time.
Only permissions set directly on the agent identity are visible here
7️⃣The ‘seeing is believing’ part
To see if everything works, the agent actually needs to have run an OBO token exchange (at least once).
First we need a client secret on the blueprint for this test.
# Add a test secret to the blueprint (dev/test only — not for production)
$secretBody = @{
passwordCredential = @{ displayName = "HR-Agent-Test-Secret" }
} | ConvertTo-Json$secret = Invoke-MgGraphRequest `
-Method POST `
-Uri "https://graph.microsoft.com/beta/applications/$blueprintObjId/addPassword" `
-Headers @{ "Content-Type" = "application/json" } `
-Body $secretBody `
-OutputType PSObject
$clientSecret = $secret.secretText # Save this - shown only once
Write-Host "Secret: $clientSecret"Now perform the two-token exchange manually. First, get the agent identity’s appId , this is different from the blueprint's appId and must be retrieved separately:
# Get the agent identity's appId (NOT the same as the blueprint appId)
$agentSP = Invoke-MgGraphRequest `
-Method GET `
-Uri "https://graph.microsoft.com/beta/servicePrincipals/$agentSPId`?`$select=id,appId,displayName,agentIdentityBlueprintId" `
-Headers @{ "OData-Version" = "4.0" } `
-OutputType PSObject$agentAppId = $agentSP.appId
Write-Host "Agent appId (fmi_path target) : $agentAppId"
Write-Host "Blueprint appId (client_id) : $blueprintAppId"
# These must be two different GUIDs - if they match, something went wrongGet T1 (blueprint authenticates, requests an exchange token for the agent identity)
$t1Body = "client_id=$blueprintAppId" +
"&client_secret=$([System.Web.HttpUtility]::UrlEncode($clientSecret))" +
"&scope=api://AzureADTokenExchange/.default" +
"&grant_type=client_credentials" +
"&fmi_path=$agentAppId"$t1Response = Invoke-RestMethod `
-Method POST `
-Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" `
-ContentType "application/x-www-form-urlencoded" `
-Body $t1Body
$T1 = $t1Response.access_token
Write-Host "T1 acquired. Expires in: $($t1Response.expires_in)s"
$t1Claims = Show-TokenClaims $T1
Write-Host "T1 aud : $($t1Claims.aud)" # should equal $agentAppId - this is the impersonation proof
Write-Host "T1 sub : $($t1Claims.sub)" # should be the blueprint SP object IDThe T1 token is not used to call any API , its only purpose is to prove to Entra that the blueprint is authorised to speak on behalf of the specific agent identity. The aud pointing at $agentAppId is what makes that proof valid.
Get TR (agent identity uses T1 as its credential, gets a Graph resource token)
$trBody = "client_id=$agentAppId" +
"&scope=https://graph.microsoft.com/.default" +
"&grant_type=client_credentials" +
"&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" +
"&client_assertion=$T1"$trResponse = Invoke-RestMethod `
-Method POST `
-Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" `
-ContentType "application/x-www-form-urlencoded" `
-Body $trBody
$TR = $trResponse.access_token
Write-Host "TR acquired for agent identity"
$trClaims = Show-TokenClaims $TR
Write-Host "TR aud : $($trClaims.aud)" # https://graph.microsoft.com
Write-Host "TR appid (agent) : $($trClaims.appid)" # agent identity appId
Write-Host "TR xms_par_app_azp : $($trClaims.xms_par_app_azp)" # blueprint appId - confirms parent-child relationship
Write-Host "TR roles : $($trClaims.roles)" # app-only permissions if anyNote:
scpis empty here , this is the autonomous (app-only) flow. Thescpclaim with inherited delegated scopes only appears in the OBO flow where a real user token is involved. The key claim to look for here isxms_par_app_azp, which should match your$blueprintAppId. That confirms Entra recognised the parent-child relationship.
Call Microsoft Graph using TR
$headers = @{ Authorization = "Bearer $TR" }$users = Invoke-RestMethod `
-Method GET `
-Uri "https://graph.microsoft.com/v1.0/users?`$top=1&`$select=displayName,userPrincipalName" `
-Headers $headers
Write-Host "Graph call succeeded"
Write-Host "User: $($users.value[0].displayName) ($($users.value[0].userPrincipalName))"ERROR ⚠️
Authorization_RequestDeniedwithInsufficient privilegesmeans the token is valid and the agent identity authenticated correctly , Graph accepted it, but the agent identity hasn't been granted theUser.Read.All(orDirectory.Read.All) application permission needed to list other users in the tenant.
Lessons learned: there are two completely separate permission layers:
- Delegated permissions (
scp) — what the agent can do on behalf of a user (what you configured withinheritablePermissions) - Application permissions (
roles) — what the agent can do autonomously with no user context (what you need for/v1.0/users)
Grant an application permission to the agent identity
# Get the User.Read.All app role ID from Microsoft Graph
$graphSP = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"$appRole = $graphSP.AppRoles | Where-Object {
$_.Value -eq "User.Read.All" -and $_.AllowedMemberTypes -contains "Application"
}
Write-Host "App role ID for User.Read.All: $($appRole.Id)"
# Assign it directly to the agent identity service principal
$assignBody = @{
principalId = $agentSPId # agent identity SP object ID
resourceId = $graphSP.Id # Microsoft Graph SP object ID
appRoleId = $appRole.Id
} | ConvertTo-Json
Invoke-MgGraphRequest `
-Method POST `
-Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$agentSPId/appRoleAssignments" `
-Headers @{ "Content-Type" = "application/json" } `
-Body $assignBody
Write-Host "User.Read.All granted to agent identity"Wait 30–60 seconds for the permission to propagate, then request a fresh TR token (the old one won’t have the new role , tokens are issued with permissions baked in at issuance time).
# Re-request T1 and TR after granting the app role
$t1Response = Invoke-RestMethod `
-Method POST `
-Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" `
-ContentType "application/x-www-form-urlencoded" `
-Body $t1Body
$T1 = $t1Response.access_token
$trResponse = Invoke-RestMethod `
-Method POST `
-Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" `
-ContentType "application/x-www-form-urlencoded" `
-Body $trBody
$TR = $trResponse.access_token
# Confirm roles now appear in the token
$trClaims = Show-TokenClaims $TR
Write-Host "TR roles : $($trClaims.roles)" # should now show User.Read.All
# Retry the Graph call
$headers = @{ Authorization = "Bearer $TR" }
$users = Invoke-RestMethod `
-Method GET `
-Uri "https://graph.microsoft.com/v1.0/users?`$top=1&`$select=displayName,userPrincipalName" `
-Headers $headers
Write-Host "Graph call succeeded"
Write-Host "User: $($users.value[0].displayName) ($($users.value[0].userPrincipalName))"A successful response here confirms the entire chain is working: blueprint credential → T1 exchange token → agent identity impersonation → TR resource token → Graph API call.
Verify the Token
Let’s verify the Access Token $TR via JWT.io
The $TR token confirms the parent-child impersonation flow is working and can validate direct application permissions via the roles claim.
Check the xms_par_app_azp claim: this confirms the token was issued via the Blueprint inheritance relationship.
To prove delegated permission inheritance from the blueprint, inspect a user-context token and verify the inherited scopes in the
scpclaim.
Check the sign-in logs
Wait 1–2 minutes for propagation, then go to:
Entra Admin Center → Agent ID → Sign-in logs → Service principal sign-ins
Filter by agent blueprint or agent identity. You will see two entries per exchange:
- T1 entry — shows the blueprint as the signing principal, resource =
api://AzureADTokenExchange
- TR entry — shows the agent identity as the signing principal, resource = Microsoft Graph
Click the TR entry and open Basic info and Additional details.
At the time of writing the Additional details are not visible, I don’t know if it’s a bug or not yet available due to Preview feature. Post will be upgraded in the future when I find the answer.
TLDR
Thanks again for reading and I truly hope it helps people (including myself) to better understand this complex topic.
