Setup guide
Everything you must provision before this skill can run โ the exact credentials, permissions, and where to click. Links go to the official consoles and docs, which stay current.
A tenant administrator must create (or authorise you to create) an Entra app registration, grant tenant-wide admin consent to the Microsoft Graph permissions it declares, and hand over the client ID plus a certificate or client secret.
You need to already have
- A Microsoft Entra ID tenant and a work or school account in it (personal Microsoft accounts cannot hold these permissions)
- Application Developer role to create the app registration, or default member permissions if the tenant has not restricted app creation
- Cloud Application Administrator or Application Administrator to grant tenant-wide admin consent
- Privileged Role Administrator (least privileged supported) or Global Administrator to consent to Microsoft Graph application permissions specifically
- If the tenant enables the admin consent workflow, a reviewer must approve the request before any permission takes effect
- Python 3.9+ with requests, cryptography and PyJWT, or Azure CLI 2.60+ for the CLI path
How the pieces connect
Credentials this skill needs
Set these as environment variables. Never paste secrets into a chat or commit them.
| Variable | What it is | Where to get it |
|---|---|---|
AZURE_TENANT_ID | Directory (tenant) ID format: GUID, or a verified domain such as contoso.onmicrosoft.com | Entra admin center > Identity > Applications > App registrations > your app > Overview > Directory (tenant) ID |
AZURE_CLIENT_ID | Application (client) ID format: GUID (this is appId, NOT the object id shown next to it) | Entra admin center > Identity > Applications > App registrations > your app > Overview > Application (client) ID |
AZURE_CLIENT_SECRETsecretoptional | Client secret value (use this OR a certificate, not both) format: Opaque string, 16-64 characters; copy the Value column, never the Secret ID | Entra admin center > Identity > Applications > App registrations > your app > Certificates & secrets > Client secrets > New client secret, then copy the Value immediately - it is masked once you navigate away and cannot be retrieved |
AZURE_CLIENT_CERTIFICATE_PATHoptional | Path to PEM file containing the private key and certificate (preferred over a secret) format: Filesystem path to an unencrypted PEM holding both the private key and the X.509 certificate; store it mode 0600 | Generate locally with openssl, then upload only the public cert (.cer/.pem/.crt) at Entra admin center > Identity > Applications > App registrations > your app > Certificates & secrets > Certificates > Upload certificate |
AZURE_CLIENT_CERTIFICATE_PASSWORDsecretoptional | Passphrase protecting the PEM private key, if it has one format: Plain string; omit entirely when the PEM is unencrypted | Chosen by whoever generated the key pair; not stored anywhere in Entra |
AZURE_AUTHORITY_HOSToptional | Authority host for sovereign clouds format: URL, defaults to https://login.microsoftonline.com; use https://login.microsoftonline.us (US Gov) or https://login.chinacloudapi.cn (21Vianet) | Determined by which national cloud the tenant lives in; see the Microsoft Graph national cloud deployments doc |
Permissions to grant
Application.ReadWrite.AllApplicationadmin consent requiredLets the bootstrap identity create the application object, PATCH requiredResourceAccess and keyCredentials, and call addPassword/addKey.
โณ Use AppRegistration.Create if the skill only ever creates new apps, or Application.ReadWrite.OwnedBy to restrict writes to apps this identity owns.
AppRoleAssignment.ReadWrite.AllApplicationadmin consent requiredRequired to POST appRoleAssignedTo, which is how admin consent for Microsoft Graph application permissions is actually granted.
โณ No narrower Graph permission exists; this is inherently high privilege because it can escalate any principal, so scope it to a dedicated bootstrap app and audit its use.
DelegatedPermissionGrant.ReadWrite.AllApplicationadmin consent requiredRequired to POST or PATCH oauth2PermissionGrants, which grants delegated Graph permissions tenant-wide with consentType AllPrincipals.
โณ Directory.ReadWrite.All also works but is far broader; prefer this narrower permission.
Application.Read.AllApplicationadmin consent requiredReads the Microsoft Graph service principal to translate permission names such as User.Read.All into the appRole and oauth2PermissionScope GUIDs the grant APIs demand.
User.Read.AllApplicationadmin consent requiredExample target permission provisioned onto the new app, and the permission used by the verification call GET /v1.0/users?$top=1.
โณ Use User.ReadBasic.All when only id, displayName, mail and userPrincipalName are needed.
User.ReadDelegatedBaseline delegated scope proving an interactive flow works; GET /v1.0/me returns the signed-in user without any admin consent.
Privileged Role AdministratorIAM roleEntra directory role held by the human operator; least privileged built-in role Microsoft supports for consenting to Microsoft Graph application permissions.
โณ Cloud Application Administrator suffices for consenting to non-Graph APIs and for all other app management, so use it where Graph app roles are not involved.
Application DeveloperIAM roleEntra directory role that lets the human operator create app registrations even when the tenant restricts default member permissions.
Step by step
- 1
Confirm you actually need a registration
If the workload runs on App Service, Functions, Container Apps, AKS or a VM, use a managed identity and stop here. If it runs in CI, use workload identity federation: App registrations > your app > Certificates & secrets > Federated credentials > Add credential. Both remove the stored secret entirely.
Open in the console / docs โ - 2
Register the application
Go to the Microsoft Entra admin center, then Identity > Applications > App registrations > New registration. Enter a Name, choose 'Accounts in this organizational directory only' for Supported account types, leave Redirect URI empty for a daemon app, and select Register. On the Overview blade copy both Application (client) ID and Directory (tenant) ID.
Open in the console / docs โ - 3
Decide delegated versus application permissions
On your app select API permissions > Add a permission > Microsoft Graph. Choose 'Delegated permissions' when a user is signed in (effective access is the intersection of the scope and that user's rights) or 'Application permissions' for unattended daemons (tenant-wide, unscoped). Search the exact permission name, tick it, then select Add permissions.
Open in the console / docs โ - 4
Look up the exact permission strings
Confirm each permission in the Microsoft Graph permissions reference and prefer the row marked 'Least privileged'. Note that the delegated and application variants of the same name behave very differently - application Mail.Read reads every mailbox in the tenant.
Open in the console / docs โ - 5
Grant admin consent
On the API permissions blade select 'Grant admin consent for <tenant>' and confirm; the Status column must change to 'Granted for <tenant>'. Equivalent CLI: az ad app permission admin-consent --id <appId>. Programmatically, application permissions go through POST /servicePrincipals/{graph-sp-id}/appRoleAssignedTo and delegated permissions through POST /oauth2PermissionGrants.
Open in the console / docs โ - 6
Add a certificate credential (preferred)
Generate locally: openssl req -x509 -newkey rsa:3072 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=svc-app". Then on your app select Certificates & secrets > Certificates > Upload certificate, pick cert.pem (.cer, .pem and .crt are accepted), and select Add. Never upload the private key.
Open in the console / docs โ - 7
Or add a client secret, and record its expiry
On your app select Certificates & secrets > Client secrets > New client secret, set a Description and an Expires value of 12 months or less (24 months is the hard maximum), then Add. Copy the Value column immediately - not the Secret ID - because it is masked as soon as you leave the blade and cannot be recovered.
Open in the console / docs โ - 8
Acquire a token and inspect its claims
POST to https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token with grant_type=client_credentials, client_id, scope=https://graph.microsoft.com/.default and either client_secret or client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer plus client_assertion. Decode the JWT payload: application permissions appear in 'roles', delegated in 'scp'. A missing claim means consent did not land.
Open in the console / docs โ - 9
Constrain over-broad application permissions
For application Mail.Read, Mail.Send, Calendars.Read or Contacts.Read, scope the app to a mail-enabled security group with New-ApplicationAccessPolicy in Exchange Online PowerShell. For SharePoint use Sites.Selected plus a per-site permission grant instead of Sites.Read.All.
Open in the console / docs โ - 10
Assign an owner and schedule rotation
On your app select Owners > Add owners so the registration is not orphaned, then record the credential expiry from Certificates & secrets and schedule replacement at 75% of its lifetime. Expiring credentials fail closed with AADSTS7000222 and no warning.
Open in the console / docs โ
Check it worked
TOKEN=$(curl -s -X POST "https://login.microsoftonline.com/$AZURE_TENANT_ID/oauth2/v2.0/token" -d 'grant_type=client_credentials' -d "client_id=$AZURE_CLIENT_ID" --data-urlencode "client_secret=$AZURE_CLIENT_SECRET" -d 'scope=https://graph.microsoft.com/.default' | python -c 'import sys,json;print(json.load(sys.stdin)["access_token"])') && curl -s -o /dev/null -w 'HTTP %{http_code}\n' -H "Authorization: Bearer $TOKEN" 'https://graph.microsoft.com/v1.0/users?$top=1&$select=id,displayName' # expect HTTP 200If you hit an error
AADSTS65001: The user or administrator has not consented to use the application with ID '<appId>'. Send an interactive authorization request for this user and resource.Cause: The permission is declared in requiredResourceAccess but nobody has consented. Declaring is not granting.
Fix: Have an admin select 'Grant admin consent for <tenant>' on the API permissions blade, run az ad app permission admin-consent --id <appId>, or POST the appRoleAssignedTo / oauth2PermissionGrants record yourself.
AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app '<appId>'.Cause: The Secret ID (a GUID) was copied instead of the Value column, or the secret was truncated or URL-encoded incorrectly.
Fix: Create a new client secret and copy the Value immediately. If it contains characters like '+' or '/', send it with --data-urlencode rather than -d.
AADSTS7000222: The provided client secret keys for app '<appId>' are expired.Cause: The client secret passed its endDateTime. Entra sends no runtime warning and the failure is total.
Fix: POST /applications/{id}/addPassword to mint a replacement, deploy it, then POST /applications/{id}/removePassword for the old keyId. Move to a certificate to make this rarer.
AADSTS700016: Application with identifier '<appId>' was not found in the directory '<tenant>'.Cause: The object ID was sent instead of the appId, the app lives in a different tenant, or no service principal was ever created in this tenant.
Fix: Use the Application (client) ID from the Overview blade, confirm AZURE_TENANT_ID matches, and POST /servicePrincipals with {"appId": "<appId>"} if the enterprise application is missing.
Authorization_RequestDenied: Insufficient privileges to complete the operation.Cause: The calling identity lacks the Graph permission or the Entra directory role for the write. Granting Microsoft Graph app roles in particular needs Privileged Role Administrator.
Fix: Add the missing permission to the bootstrap app and consent to it, or assign the human operator Cloud Application Administrator plus Privileged Role Administrator. Do not retry the call unchanged.
AADSTS500011: The resource principal named <resource> was not found in the tenant named <tenant>.Cause: The scope names a resource that has no service principal in this tenant, or the scope string is misspelled - for example 'https://graph.microsoft.com' without the required '/.default'.
Fix: Use scope=https://graph.microsoft.com/.default for client credentials, and create the resource service principal in the tenant if it is genuinely absent.
InvalidAuthenticationToken: Access token validation failure. Invalid audience.Cause: A token minted for a different resource (commonly https://management.azure.com/.default) was sent to Microsoft Graph.
Fix: Request a fresh token with scope=https://graph.microsoft.com/.default. One token cannot address two resources.
AADSTS50011: The redirect URI '<uri>' specified in the request does not match the redirect URIs configured for the application '<appId>'.Cause: Delegated flow only: the redirect URI is missing, registered under the wrong platform type, or differs by scheme, trailing slash or case.
Fix: On Authentication > Platform configurations add the exact URI under the correct platform (Web, SPA or Mobile and desktop). SPAs must use the SPA platform or token redemption fails with AADSTS9002326.
AADSTS700027: Client assertion failed signature validation.Cause: The client assertion was signed by a key whose certificate is not registered on the app, or the x5t#S256 header does not match the signing certificate.
Fix: Confirm the uploaded certificate's SHA-256 thumbprint equals the base64url x5t#S256 header value, sign with PS256, and set aud to https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token.
AADSTS90002: Tenant '<value>' not found. Check to make sure you have the correct tenant ID.Cause: AZURE_TENANT_ID holds a display name, an email domain that is not a verified domain, or a value from a different national cloud.
Fix: Use the Directory (tenant) ID GUID from the Overview blade, or a verified domain, and set AZURE_AUTHORITY_HOST when the tenant is in a US Gov or 21Vianet cloud.
Official documentation: https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app โ
API operations this skill uses (20)
Every call the skill makes, linked to its official reference page.
| Operation | Call | Permission | Ref |
|---|---|---|---|
| Resolve tenant identity Confirms which tenant the credentials actually point at before any write, so an app is never created in the wrong directory. | GET /v1.0/organization?$select=id,displayName,verifiedDomains | Organization.Read.All (Application or Delegated) | docs โ |
| Look up the Microsoft Graph service principal and permission catalog Translates permission names such as User.Read.All into the appRole and oauth2PermissionScope GUIDs that every grant API requires, and validates that a requested permission actually exists. | GET /v1.0/servicePrincipals?$filter=appId eq '00000003-0000-0000-c000-000000000000'&$select=id,appRoles,oauth2PermissionScopes | Application.Read.All (Application or Delegated) | docs โ |
| Create the app registration Creates the application object with displayName, signInAudience and the requiredResourceAccess permission request. Returns both id (object ID) and appId (client ID). | POST /v1.0/applications | AppRegistration.Create, or Application.ReadWrite.All | docs โ |
| Read an existing app registration by client ID Makes the flow idempotent and surfaces credential expiry dates, so the skill can detect an already-provisioned app instead of creating a duplicate. | GET /v1.0/applications(appId='{appId}')?$select=id,appId,displayName,requiredResourceAccess,passwordCredentials,keyCredentials | Application.Read.All (Application or Delegated) | docs โ |
| Update declared permissions, redirect URIs, or the first certificate Adds permissions to requiredResourceAccess, sets web.redirectUris for delegated flows, and is the only way to install the very first keyCredential since addKey cannot bootstrap. | PATCH /v1.0/applications/{id} | Application.ReadWrite.All, or Application.ReadWrite.OwnedBy | docs โ |
| Create the service principal Creates the tenant-local identity (the enterprise application) that receives consent grants. Without it, every consent call fails or silently does nothing. | POST /v1.0/servicePrincipals | Application.ReadWrite.All (Application or Delegated) | docs โ |
| Grant admin consent for an application permission The actual grant for application permissions: one assignment per permission with principalId (client SP), resourceId (Graph SP) and appRoleId. | POST /v1.0/servicePrincipals/{graph-sp-id}/appRoleAssignedTo | AppRoleAssignment.ReadWrite.All and Application.Read.All | docs โ |
| Audit granted application permissions Proves consent actually landed and lists every principal holding a Graph app role, which is the core check for an over-permissioned tenant. | GET /v1.0/servicePrincipals/{graph-sp-id}/appRoleAssignedTo | Application.Read.All (Application or Delegated) | docs โ |
| Revoke an application permission Rolls back an over-broad grant, and is the cleanup path when the skill provisioned more than the task turned out to need. | DELETE /v1.0/servicePrincipals/{graph-sp-id}/appRoleAssignedTo/{appRoleAssignment-id} | AppRoleAssignment.ReadWrite.All | docs โ |
| Grant admin consent for delegated permissions tenant-wide The actual grant for delegated permissions: a single record with consentType AllPrincipals and every scope in one space-separated scope string. | POST /v1.0/oauth2PermissionGrants | DelegatedPermissionGrant.ReadWrite.All | docs โ |
| Inspect existing delegated grants Finds the existing grant so new scopes are PATCHed onto it instead of creating duplicate rows. Filtering by clientId also minimises replication delay. | GET /v1.0/oauth2PermissionGrants?$filter=clientId eq '{client-sp-id}' | Directory.Read.All, or DelegatedPermissionGrant.ReadWrite.All | docs โ |
| Add a scope to an existing delegated grant Extends consent by rewriting the whole space-separated scope string, which is the only safe way to add a delegated permission after initial consent. | PATCH /v1.0/oauth2PermissionGrants/{oAuth2PermissionGrant-id} | DelegatedPermissionGrant.ReadWrite.All | docs โ |
| Mint a client secret Generates a secret with an explicit endDateTime. The response secretText is returned exactly once and must go straight to a secret store. | POST /v1.0/applications/{id}/addPassword | Application.ReadWrite.OwnedBy, or Application.ReadWrite.All | docs โ |
| Retire a client secret after rotation Deletes the superseded credential by keyId once the replacement is deployed, closing the overlap window. | POST /v1.0/applications/{id}/removePassword | Application.ReadWrite.OwnedBy, or Application.ReadWrite.All | docs โ |
| Roll a certificate credential Adds a replacement certificate using a proof JWT signed by the current key. Only valid when the app already holds a valid certificate; otherwise PATCH keyCredentials. | POST /v1.0/applications/{id}/addKey | Application.ReadWrite.OwnedBy, or none when the app rolls its own key | docs โ |
| Acquire an app-only access token Produces the bearer token for every Graph call, and its roles/scp claims are the ground truth for whether consent succeeded. | POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token (grant_type=client_credentials, scope=https://graph.microsoft.com/.default, client_secret or client_assertion) | None beyond the app credential itself | docs โ |
| Verify app-only access end to end Smallest real call that proves the token, the consent and the network path all work. A 200 ends the provisioning loop. | GET /v1.0/users?$top=1&$select=id,displayName | User.ReadBasic.All, or User.Read.All (Application) | docs โ |
| Verify delegated access end to end Confirms a delegated token carries a user context. This endpoint returns 400 for app-only tokens, so it also distinguishes the two token types. | GET /v1.0/me?$select=id,displayName,userPrincipalName | User.Read (Delegated) | docs โ |
| Assign an owner to the registration Prevents the app becoming orphaned and unrotatable when its creator leaves the organisation. | POST /v1.0/applications/{id}/owners/$ref | Application.ReadWrite.All and Directory.Read.All | docs โ |
| Grant admin consent interactively Fallback when the bootstrap identity lacks AppRoleAssignment.ReadWrite.All: consents to every declared permission in one command using the operator's own privileges. | CLI az ad app permission admin-consent --id <appId> | Signed-in user must hold Privileged Role Administrator or Global Administrator | docs โ |