Microsoft Teams Error Guide: 'Authorization_RequestDenied' App Permission Not Granted
Fix the Microsoft Graph API 403 Authorization_RequestDenied error in Teams: diagnose missing app permissions, absent admin consent, and wrong permission types.
- #microsoft-teams
- #troubleshooting
- #errors
- #graph-api
Overview
A 403 Forbidden with code Authorization_RequestDenied means Microsoft Graph validated the bearer token successfully but determined the calling principal — app or user — does not hold the required permission for the requested resource or action. The permission may be absent from the app registration, present but not consented to by a tenant admin, or the wrong type (delegated vs. application).
The literal JSON error body from Graph:
{
"error": {
"code": "Authorization_RequestDenied",
"message": "Insufficient privileges to complete the operation.",
"innerError": {
"date": "2026-06-23T14:15:22",
"request-id": "c3d4e5f6-3333-4444-5555-666677778888",
"client-request-id": "c3d4e5f6-3333-4444-5555-666677778888"
}
}
}
For operations that require admin consent on application permissions, Graph sometimes returns a more specific message:
{
"error": {
"code": "Authorization_RequestDenied",
"message": "Application does not have privileges to create a team or channel.",
"innerError": {
"date": "2026-06-23T14:18:04",
"request-id": "d4e5f6a7-4444-5555-6666-777788889999",
"client-request-id": "d4e5f6a7-4444-5555-6666-777788889999"
}
}
}
This error appears on any Graph endpoint: reading Teams messages, listing channels, managing memberships, creating meetings, or accessing user mailboxes from an unattended daemon. Unlike 401, the token is valid — the account or app simply lacks authorization.
Symptoms
- API returns
403immediately after a successful token acquisition. - Teams bot can authenticate but cannot read channel messages or post to channels.
- Service principal can call some Graph endpoints but not others (permissions are per-scope, not all-or-nothing).
- Admin-only operations such as
PATCH /teams/{id}orDELETE /channels/{id}return403for non-admin users.
curl -s -X GET \
"https://graph.microsoft.com/v1.0/teams/aaaabbbb-1111-2222-3333-ccccddddeeee/channels" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json"
{
"error": {
"code": "Authorization_RequestDenied",
"message": "Insufficient privileges to complete the operation.",
"innerError": {
"date": "2026-06-23T14:15:22",
"request-id": "c3d4e5f6-3333-4444-5555-666677778888",
"client-request-id": "c3d4e5f6-3333-4444-5555-666677778888"
}
}
}
Common Root Causes
1. Required API permission not added to the app registration
The application must declare every Microsoft Graph permission it needs under API permissions in the Azure portal (or via az ad app permission add). If the permission is missing entirely, Graph never considers granting it — even if an admin wants to consent.
# List permissions currently declared on the app registration
az ad app permission list \
--id "<APP_CLIENT_ID>" \
--query "[].{api:resourceAppId, permissions:resourceAccess[].{id:id,type:type}}" \
-o json
[
{
"api": "00000003-0000-0000-c000-000000000000",
"permissions": [
{ "id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d", "type": "Scope" }
]
}
]
The GUID e1fe6dd8-... maps to User.Read — a basic delegated scope. If the operation needs ChannelMessage.Read.All and it is absent from this list, 403 is guaranteed.
2. Admin consent not granted for application permissions
Application permissions (type Role, used by daemons running without a signed-in user) require explicit tenant-level admin consent. Adding the permission to the app registration is not enough — a Global Administrator must consent via the Azure portal or the /adminconsent endpoint. Until consent is granted the permission shows in the portal but Graph will not honour it.
# Check granted OAuth2 permissions for the service principal
az ad sp show --id "<APP_CLIENT_ID>" --query "appId" -o tsv
# List granted app role assignments (application permissions that have been consented)
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/<SP_OBJECT_ID>/appRoleAssignments" \
--headers "Authorization=Bearer $TOKEN" \
| jq '.value[] | {appRoleId, principalDisplayName}'
{
"appRoleId": "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
"principalDisplayName": "my-teams-bot"
}
If the required permission GUID (e.g., 7b2449af-6ccd-4f98-a5bd-4ced04a5440a for ChannelMessage.Read.All) does not appear in the appRoleAssignments list, admin consent is missing.
3. Delegated permission used in a daemon (no signed-in user)
Delegated permissions (Scope type) require a signed-in user context. A daemon or background service that acquires tokens via client credentials flow (grant_type=client_credentials) will never satisfy a delegated permission — Graph sees no user and refuses. The daemon must request the corresponding application permission instead.
# Inspect the token to determine if it represents a user or an app
echo "$TOKEN" | cut -d'.' -f2 | base64 -d 2>/dev/null \
| jq '{idtyp, oid, upn, scp, roles}'
{
"idtyp": "app",
"oid": "aabb1122-3344-5566-7788-99aabbccddee",
"upn": null,
"scp": null,
"roles": []
}
idtyp: "app" with an empty roles array and no scp means the token is application-scoped but holds no application permissions. Switch the permission type to Role and re-acquire.
4. User lacks the required Teams role or license
For delegated flows, even if the app permission is correct, Graph enforces the signed-in user’s Teams role on the underlying resource. A user who is not a member of a private channel cannot read its messages regardless of app permissions. A user without a Teams license cannot call many /teams/ endpoints at all.
# Check the user's Teams license assignment
curl -s -X GET \
"https://graph.microsoft.com/v1.0/users/<USER_ID>/licenseDetails" \
-H "Authorization: Bearer $TOKEN" \
| jq '.value[].servicePlans[] | select(.servicePlanName | startswith("TEAMS")) | {servicePlanName, provisioningStatus}'
{
"servicePlanName": "TEAMS1",
"provisioningStatus": "Disabled"
}
provisioningStatus: "Disabled" means the Teams service plan is assigned but not enabled for this user — Graph will return 403 on Teams-specific endpoints.
5. Protected API requiring additional Microsoft approval
Some Graph endpoints — particularly those accessing Teams messages at scale (/teams/{id}/channels/{id}/messages) — are classified as protected APIs. They require Microsoft to approve the app before Graph honours the permission, even after admin consent. Approval is requested through the Microsoft 365 protected API request form.
# Attempt to list channel messages (protected endpoint)
curl -s -X GET \
"https://graph.microsoft.com/v1.0/teams/aaaabbbb-1111-2222-3333-ccccddddeeee/channels/19%3Axxx%40thread.tacv2/messages" \
-H "Authorization: Bearer $TOKEN" | jq .error
{
"code": "Authorization_RequestDenied",
"message": "Application does not have required permissions to access this protected API.",
"innerError": {
"date": "2026-06-23T14:22:11",
"request-id": "e5f6a7b8-5555-6666-7777-888899990000"
}
}
6. Conditional Access policy blocking the application
Azure AD Conditional Access can restrict which apps, locations, or device states are allowed to call Graph. An app that passes authentication may still be blocked by a policy requiring compliant devices or MFA for the service principal’s access to Microsoft Graph.
# Check sign-in logs for the service principal for CA policy failures
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=appId eq '<APP_CLIENT_ID>' and status/errorCode ne 0&\$top=5" \
--headers "Authorization=Bearer $ADMIN_TOKEN" \
| jq '.value[] | {createdDateTime, status, conditionalAccessStatus, appliedConditionalAccessPolicies}'
{
"createdDateTime": "2026-06-23T14:20:00Z",
"status": { "errorCode": 53003, "failureReason": "Blocked by Conditional Access" },
"conditionalAccessStatus": "failure",
"appliedConditionalAccessPolicies": [
{ "displayName": "Require Compliant Device for Graph API", "result": "failure" }
]
}
Diagnostic Workflow
Step 1: Confirm the token is valid and inspect its permission claims
echo "$TOKEN" | cut -d'.' -f2 | base64 -d 2>/dev/null \
| jq '{idtyp, oid, upn, scp, roles, tid}'
Check scp for delegated scopes or roles for application permissions. If both are empty, the app holds no Graph permissions despite a valid token.
Step 2: Compare token permissions against the required Graph scope
Look up the required permission for the failing endpoint in the Microsoft Graph permissions reference. Then verify it appears in either scp (delegated) or roles (application) in the decoded token.
# Example: check if ChannelMessage.Read.All is in the app token's roles
echo "$TOKEN" | cut -d'.' -f2 | base64 -d 2>/dev/null \
| jq '.roles | map(select(. == "ChannelMessage.Read.All"))'
[]
An empty array confirms the permission is not present in the token.
Step 3: Verify the permission is declared and consented in Azure AD
# Declared permissions on the app registration
az ad app permission list --id "<APP_CLIENT_ID>" -o json | jq .
# Consented app role assignments on the service principal
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/<SP_OBJECT_ID>/appRoleAssignments" \
--headers "Authorization=Bearer $ADMIN_TOKEN" \
| jq '.value[].appRoleId'
If the permission GUID appears in az ad app permission list but not in appRoleAssignments, admin consent is pending.
Step 4: Grant admin consent
# Grant admin consent for all configured permissions via Azure CLI
az ad app permission admin-consent --id "<APP_CLIENT_ID>"
Or navigate in the Azure portal to Azure AD → App registrations → [your app] → API permissions → Grant admin consent for [tenant].
Step 5: Re-acquire a token and retry
After consent is granted, acquire a new token — existing tokens will not pick up the new permission grant.
TOKEN=$(az account get-access-token \
--tenant "<TENANT_ID>" \
--resource "https://graph.microsoft.com" \
| jq -r '.accessToken')
curl -s -X GET \
"https://graph.microsoft.com/v1.0/teams/aaaabbbb-1111-2222-3333-ccccddddeeee/channels" \
-H "Authorization: Bearer $TOKEN" | jq .
Example Root Cause Analysis
A Teams bot service principal that sends channel notifications begins returning 403 Authorization_RequestDenied after a tenant admin reviewed and tightened app permissions. The bot was previously working and the token is still valid.
Decoding the token shows roles: [] — no application permissions are present:
echo "$TOKEN" | cut -d'.' -f2 | base64 -d 2>/dev/null | jq '.roles'
[]
Checking appRoleAssignments on the service principal shows only User.Read.All was consented — not ChannelMessage.Send or Channel.ReadBasic.All, which the bot requires for its new channel-listing feature added in the most recent deployment:
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/<SP_OBJECT_ID>/appRoleAssignments" \
--headers "Authorization=Bearer $ADMIN_TOKEN" \
| jq '.value[].appRoleId'
"df021288-bdef-4463-88db-98f22de89214"
That GUID maps to User.Read.All only. The admin tightened consent scope during a quarterly review and removed previously granted permissions. Fix: add ChannelMessage.Send and Channel.ReadBasic.All to the app registration, then re-grant admin consent:
az ad app permission add \
--id "<APP_CLIENT_ID>" \
--api 00000003-0000-0000-c000-000000000000 \
--api-permissions "5922d31f-46c8-4404-9eaf-2117e390a8a4=Role" \
"59a6b24b-4225-4393-8165-ebaec5d55d0a=Role"
az ad app permission admin-consent --id "<APP_CLIENT_ID>"
After re-acquiring a token, roles contains the new permissions and the bot resumes posting.
Prevention Best Practices
- Document every Graph permission your application uses alongside the endpoint that requires it; include the permission type (delegated vs. application) so a future admin review doesn’t accidentally remove required permissions.
- Pin app permissions in infrastructure-as-code (Terraform
azuread_applicationresource or Bicep templates) so an Azure portal change triggers a drift alert rather than silently removing consent. - Use the principle of least privilege — request only the specific permissions each endpoint requires. Overly broad permissions attract admin scrutiny and increase the blast radius if credentials are compromised.
- For daemon services, always use application permissions (
Role) rather than delegated permissions (Scope) — delegated scopes will never work without a signed-in user context. - Check the Microsoft Graph permissions reference before requesting a protected API endpoint; protected APIs require a separate Microsoft approval process beyond admin consent.
- The incident assistant can correlate a
403error with the specific missing permission GUID and generate theaz ad app permission addcommand needed.
Quick Command Reference
# Decode token to check scp/roles
echo "$TOKEN" | cut -d'.' -f2 | base64 -d 2>/dev/null \
| jq '{idtyp, scp, roles, oid, tid}'
# List declared permissions on the app registration
az ad app permission list --id "<APP_CLIENT_ID>" -o json
# List consented application permissions (appRoleAssignments)
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/<SP_OBJECT_ID>/appRoleAssignments" \
--headers "Authorization=Bearer $ADMIN_TOKEN" \
| jq '.value[] | {appRoleId}'
# Add a Graph application permission to an app registration
az ad app permission add \
--id "<APP_CLIENT_ID>" \
--api 00000003-0000-0000-c000-000000000000 \
--api-permissions "<PERMISSION_GUID>=Role"
# Grant admin consent for all configured permissions
az ad app permission admin-consent --id "<APP_CLIENT_ID>"
# Check user Teams license status
curl -s -X GET \
"https://graph.microsoft.com/v1.0/users/<USER_ID>/licenseDetails" \
-H "Authorization: Bearer $TOKEN" \
| jq '.value[].servicePlans[] | select(.servicePlanName | startswith("TEAMS"))'
# Check sign-in logs for Conditional Access failures
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=appId eq '<APP_CLIENT_ID>' and status/errorCode ne 0&\$top=5" \
--headers "Authorization=Bearer $ADMIN_TOKEN" | jq '.value[].status'
Conclusion
A 403 Authorization_RequestDenied from Microsoft Graph always means the token is valid but the calling principal lacks authorization for the requested resource. The root causes in order of frequency:
- The required API permission is not declared on the app registration in Azure AD.
- The permission is declared but admin consent has not been granted (most common for application permissions).
- A delegated permission is being used in a daemon context — no signed-in user means no delegated scope can be satisfied.
- The signed-in user lacks the required Teams role, channel membership, or license for the target resource.
- The endpoint is a protected API requiring explicit Microsoft approval beyond standard admin consent.
- A Conditional Access policy is blocking the service principal from accessing Microsoft Graph.
Start by decoding the token’s roles and scp claims — if both are empty, the consent layer is the problem before any code change is needed. See more troubleshooting guides in Microsoft Teams guides.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.