OBO Blueprint Setup
To use OBO (on-behalf-of) mode — where an agent acts on behalf of a user — the Blueprint must be configured as an OAuth2 API so the user's token can target it as audience.
Configure the Blueprint as an API
Apply once via PATCH /applications/{blueprint_obj_id}:
import uuid
scope_id = str(uuid.uuid4())
patch = {
"identifierUris": [f"api://{blueprint_app_id}"],
"api": {
"requestedAccessTokenVersion": 2,
"oauth2PermissionScopes": [{
"id": scope_id,
"adminConsentDescription": "Allow the app to access the agent API on behalf of the user.",
"adminConsentDisplayName": "Access agent API",
"userConsentDescription": "Allow the app to access the agent API on your behalf.",
"userConsentDisplayName": "Access agent API",
"value": "access_as_user",
"type": "User",
"isEnabled": True,
}],
"preAuthorizedApplications": [{
"appId": client_app_id, # Your front-end or CLI app's appId
"permissionIds": [scope_id],
}],
},
"optionalClaims": {
"accessToken": [{
"name": "idtyp",
"source": None,
"essential": False,
"additionalProperties": ["include_user_token"],
}]
},
}
requests.patch(
f"{GRAPH}/applications/{blueprint_obj_id}",
headers=headers, json=patch,
).raise_for_status()All four pieces are required:
identifierUris— enablesapi://{appId}as audience.oauth2PermissionScopes— defines theaccess_as_userscope users can consent to.preAuthorizedApplications— authorizes your client app and skips the consent prompt.optionalClaims— emits theidtypclaim needed for token-type validation.
Grant Delegated Permissions to Each Agent Identity
Each Agent Identity needs oauth2PermissionGrants specifying which Graph delegated permissions it may exercise on behalf of users:
from datetime import datetime, timedelta, timezone
expiry = (datetime.now(timezone.utc) + timedelta(days=3650)).strftime("%Y-%m-%dT%H:%M:%SZ")
requests.post(
f"{GRAPH}/oauth2PermissionGrants",
headers=headers,
json={
"clientId": agent_sp_id, # Agent Identity SP object ID
"consentType": "AllPrincipals",
"resourceId": graph_sp_id, # Microsoft Graph SP object ID
"scope": "User.Read Tasks.ReadWrite",
"expiryTime": expiry,
},
).raise_for_status()Acquire the User Token — Targeting the Blueprint
from azure.identity import InteractiveBrowserCredential
credential = InteractiveBrowserCredential(
tenant_id=tenant_id,
client_id=client_app_id, # Your client app, NOT the Blueprint
redirect_uri="http://localhost:8400",
)
user_token = credential.get_token(f"api://{blueprint_app_id}/access_as_user")If the user token targets
https://graph.microsoft.cominstead of the Blueprint, the OBO exchange returnsAADSTS50013: Assertion failed signature validation.
Then feed user_token.token into exchange_obo(...) from runtime-token-exchange.md.
Per-Agent Scoping
Different agents can have different permission boundaries:
AGENT_SCOPES = {
"it-helpdesk": "User.Read Tasks.ReadWrite",
"comms-agent": "User.Read Mail.Send Calendars.ReadWrite",
"hr-onboarding": "User.Read User.ReadBasic.All",
}
for agent_name, scopes in AGENT_SCOPES.items():
grant_delegated(agent_identities[agent_name], scopes)Key Rules (OBO)
- User token audience = Blueprint (
api://{blueprint_app_id}/access_as_user), not Graph. - Use
/.defaultscope on the OBO exchange step. expiryTimeis required onoauth2PermissionGrantsfor Agent Identity grants.- Browser-based admin consent URLs do not work for Agent Identities — use
oauth2PermissionGrantsAPI for programmatic consent. Group.ReadWrite.Allcannot be granted as a delegated permission to Agent Identities.Tasks.ReadWritealone covers Planner task operations.- Not every Graph scope is allowed for Agent Identities — test each new scope.