Microsoft Teams Error Guide: 'ErrorAccessDenied' Graph API Access Denied on Teams Resource
Fix the Microsoft Graph ErrorAccessDenied error on Teams chats, channels, and messages: diagnose missing permissions, consent gaps, and token scope issues.
- #microsoft-teams
- #troubleshooting
- #errors
- #graph-api
Overview
ErrorAccessDenied is the Microsoft Graph API error returned when a caller attempts to read or write a Teams resource — a chat, channel, message, member list, or tab — that it does not have permission to access. The caller may be missing a delegated or application permission, the permission may exist in the app registration but not yet consented to by a tenant admin, or the access token’s scope simply does not include the required claim.
The Graph API returns HTTP 403 with a body like:
{
"error": {
"code": "ErrorAccessDenied",
"message": "Access is denied. Check credentials and try again.",
"innerError": {
"date": "2026-06-23T14:02:11",
"request-id": "a1b2c3d4-1111-2222-3333-444455556666",
"client-request-id": "f0e1d2c3-4455-6677-8899-aabbccddeeff"
}
}
}
This error surfaces on any Teams endpoint under /v1.0/chats, /v1.0/teams, /v1.0/users/{id}/chats, /v1.0/teams/{id}/channels/{id}/messages, and related resources. It can also appear on beta endpoints such as /beta/chats/{id}/messages. It is distinct from AuthenticationError (invalid token) — the token is valid but the granted permissions are insufficient.
Symptoms
- All requests to a Teams chat or channel message endpoint return HTTP 403.
- The
error.codefield is exactlyErrorAccessDenied. - The token decodes correctly but is missing the required scope claim.
- Consent was granted for one permission (e.g.,
Chat.Read) but the endpoint requires a higher permission (e.g.,Chat.ReadWriteorChannelMessage.Read.All). - Listing teams (
/v1.0/me/joinedTeams) succeeds but reading messages inside them fails.
curl -s -X GET \
"https://graph.microsoft.com/v1.0/chats/19:abc123def456@thread.v2/messages" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq .
{
"error": {
"code": "ErrorAccessDenied",
"message": "Access is denied. Check credentials and try again.",
"innerError": {
"date": "2026-06-23T14:05:33",
"request-id": "b2c3d4e5-2233-4455-6677-889900aabbcc"
}
}
}
Common Root Causes
1. Missing required API permission in the app registration
The Azure AD app registration does not list the permission the endpoint requires. Reading chat messages requires Chat.Read or Chat.ReadWrite (delegated) or Chat.Read.All (application). If only User.Read is registered, every Teams call returns 403.
az ad app permission list \
--id $APP_ID \
--query "[].{resourceAppId:resourceAppId, scope:scope, type:permissionType}" \
-o table
ResourceAppId Scope Type
----------------------------------- ------------------ -----------
00000003-0000-0000-c000-000000000000 User.Read Delegated
Chat.Read is absent — any call to /v1.0/chats/... returns ErrorAccessDenied.
2. Permission granted but admin consent not completed
The permission appears in the app registration but the tenant admin has not yet consented. Without consent, the token issuer omits the scope even if the app requests it.
az ad app permission list-grants \
--id $APP_ID \
--query "[].{scope:scope, consentType:consentType, principalId:principalId}" \
-o table
Scope ConsentType PrincipalId
-------------------------------------- ------------ -----------
User.Read AllPrincipals null
Chat.Read.All does not appear in grants even though it was added to the manifest — admin consent is pending.
3. Access token scope does not contain the required claim
The token was issued before the new permission was consented to, or the client is requesting the wrong scope string in its token acquisition call. Decoding the JWT scp or roles claim reveals the gap.
# Decode the access token payload (base64 the middle segment)
echo $ACCESS_TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | jq '{scp,roles,oid,tid}'
{
"scp": "User.Read openid profile",
"roles": null,
"oid": "c3d4e5f6-3344-5566-7788-99aabbccddee",
"tid": "d4e5f6a7-4455-6677-8899-aabbccddeeff"
}
Chat.Read is absent from scp. The app must request https://graph.microsoft.com/Chat.Read in its token acquisition scope parameter.
4. Application permission used where delegated (or vice versa) is required
Some Teams endpoints only accept delegated flows (signed-in user context) and reject application tokens, or require application permissions that a delegated token cannot carry. Sending an app-only token to /v1.0/me/chats always fails because /me requires a delegated context.
curl -s -X GET \
"https://graph.microsoft.com/v1.0/me/chats" \
-H "Authorization: Bearer $APP_ONLY_TOKEN" | jq .error.code
"ErrorAccessDenied"
Switch to a delegated token with a signed-in user, or use the user-specific endpoint /v1.0/users/{userId}/chats with an application token that has Chat.Read.All.
5. Restricted Teams resource requiring protected API access
Some Graph endpoints (e.g., reading all messages across all chats via Chat.Read.All, or accessing meeting recordings) are “protected APIs” that require explicit Microsoft approval beyond normal admin consent. Without approval, the 403 persists even after granting the permission.
curl -s -X GET \
"https://graph.microsoft.com/v1.0/users/$USER_ID/chats/getAllMessages" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq .error
{
"code": "ErrorAccessDenied",
"message": "The application does not have protected API access. Please apply for access via https://aka.ms/teamsgraph/requestaccess"
}
The message field explicitly references the protected API request form when this is the cause.
6. Conditional Access or Information Barrier policy blocking access
Azure AD Conditional Access or Teams Information Barrier policies can block specific API calls for compliant reasons even when permissions look correct. The 403 is returned without a descriptive inner error, and the Access Review or Sign-in logs show a policy match.
az ad signin-logs list \
--filter "appId eq '$APP_ID' and status/errorCode eq 53003" \
--query "[].{time:createdDateTime, error:status/failureReason}" \
-o table
Time Error
------------------------- -----------------------------------------------
2026-06-23T14:10:45.000Z Blocked by Conditional Access policy: require compliant device
Error code 53003 in Azure AD sign-in logs indicates a Conditional Access block.
Diagnostic Workflow
Step 1: Confirm the error code and capture the request-id
curl -s -X GET \
"https://graph.microsoft.com/v1.0/chats/$CHAT_ID/messages" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '{code: .error.code, message: .error.message, requestId: .error.innerError["request-id"]}'
Confirm code is ErrorAccessDenied and note the request-id for Microsoft support if needed.
Step 2: Decode the access token and verify scopes
echo $ACCESS_TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | jq '{scp, roles, aud}'
Check that scp (delegated) or roles (application) contains the permission the endpoint requires. Refer to the Graph permissions reference for the exact permission name.
Step 3: List app permissions and consent grants
az ad app permission list --id $APP_ID -o table
az ad app permission list-grants --id $APP_ID -o table
Verify the required permission appears in both the registration and the grants list. A permission in the registration but absent from grants means admin consent is outstanding.
Step 4: Check for protected API requirements
curl -s -X GET \
"https://graph.microsoft.com/v1.0/chats/$CHAT_ID/messages" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq .error.message
If the message references https://aka.ms/teamsgraph/requestaccess, submit a protected API access request through that URL. Standard consent cannot unblock it.
Step 5: Review Azure AD sign-in and audit logs
az ad signin-logs list \
--filter "appId eq '$APP_ID'" \
--query "[0:10].{time:createdDateTime, status:status.errorCode, reason:status.failureReason}" \
-o table
Error codes 53003 (Conditional Access) or 50076 (MFA required) in the sign-in logs point to policy-level blocks rather than permission configuration issues.
Example Root Cause Analysis
A CI pipeline calls GET /v1.0/teams/{teamId}/channels/{channelId}/messages using a service principal token to archive channel messages nightly. It worked in the staging tenant but returns ErrorAccessDenied in production.
Decoding the production token:
echo $PROD_TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | jq '{scp, roles}'
{
"scp": null,
"roles": ["User.Read.All", "Team.ReadBasic.All"]
}
ChannelMessage.Read.All is not in roles. Checking the app registration in the production tenant:
az ad app permission list --id $PROD_APP_ID -o table
ResourceAppId Scope Type
------------------------------------ ------------------------- -----------
00000003-0000-0000-c000-000000000000 User.Read.All Application
00000003-0000-0000-c000-000000000000 Team.ReadBasic.All Application
ChannelMessage.Read.All was added to the staging app registration but never replicated to production during the tenant-specific deployment step. The fix:
az ad app permission add \
--id $PROD_APP_ID \
--api 00000003-0000-0000-c000-000000000000 \
--api-permissions 7b2449af-6ccd-4f98-a5ac-d6425208e17e=Role
az ad app permission admin-consent --id $PROD_APP_ID
After admin consent propagates (allow 60–90 seconds), re-issue the token and retry the channel message request — HTTP 200 is returned.
Prevention Best Practices
- Declare all required Graph API permissions in infrastructure-as-code (Bicep, Terraform
azuread_application_api_permission) so staging and production app registrations stay in sync — permission drift between tenants is the most common cause ofErrorAccessDeniedin multi-tenant pipelines. - Build a pre-deployment smoke test that decodes the service principal token and asserts required
rolesclaims are present before the pipeline proceeds. - Track consent status in your deployment runbook: after adding an application permission, a Global Administrator must complete admin consent; automate with
az ad app permission admin-consentin your pipeline where the deploying identity has sufficient privilege. - Always handle 403 /
ErrorAccessDeniedwith an automatic token refresh and single retry before alerting — expired tokens occasionally produce this code before thescpis re-evaluated. - For protected APIs (bulk chat export, recording access), open the Microsoft access request early in the project lifecycle; approval takes days to weeks and cannot be expedited.
- Pair Graph API calls with the incident assistant to correlate 403 responses with sign-in log entries and surface the exact policy block.
Quick Command Reference
# Decode token scopes (delegated = scp, application = roles)
echo $ACCESS_TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | jq '{scp, roles, aud, tid}'
# List app permissions in the registration
az ad app permission list --id $APP_ID -o table
# List consent grants
az ad app permission list-grants --id $APP_ID -o table
# Add a missing application permission (ChannelMessage.Read.All)
az ad app permission add \
--id $APP_ID \
--api 00000003-0000-0000-c000-000000000000 \
--api-permissions 7b2449af-6ccd-4f98-a5ac-d6425208e17e=Role
# Grant admin consent
az ad app permission admin-consent --id $APP_ID
# Test a chat message endpoint
curl -s -X GET \
"https://graph.microsoft.com/v1.0/chats/$CHAT_ID/messages" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '{code: .error.code}'
# Check sign-in logs for Conditional Access blocks (error 53003)
az ad signin-logs list \
--filter "appId eq '$APP_ID' and status/errorCode eq 53003" \
--query "[].{time:createdDateTime, reason:status.failureReason}" -o table
Conclusion
ErrorAccessDenied on a Microsoft Graph Teams resource means the access token does not carry the permission the endpoint requires. The six root causes in priority order:
- The required permission (
Chat.Read.All,ChannelMessage.Read.All, etc.) is not registered in the Azure AD app registration. - The permission is registered but admin consent has not been completed in the target tenant.
- The access token was issued with the wrong scope string — the granted permission is absent from
scporroles. - An application-only token was used against a delegated-only endpoint (e.g.,
/me/chats), or vice versa. - The endpoint is a protected API requiring an explicit Microsoft approval beyond standard admin consent.
- A Conditional Access or Information Barrier policy is blocking the request at the Azure AD layer.
Always decode the JWT and compare scp/roles against the required permission before diving into the app registration — the token tells you exactly what was granted at issuance. See more troubleshooting patterns 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.