Getting the Right Graph Token Audience with az account get-access-token
Get the correct Microsoft Graph token audience from az account get-access-token for ChannelMessage.Send: use --resource or --scope, decode the JWT aud, fix InvalidAuthenticationToken.
- #microsoft-teams
- #microsoft-graph
- #azure
- #troubleshooting
Fixing errors like this? Get 500 free DevOps AI prompts
500 copy-paste AI prompts for the stack you actually run — one PDF, free.
Exact Error Message
{
"error": {
"code": "InvalidAuthenticationToken",
"message": "Access token validation failure. Invalid audience.",
"innerError": {
"date": "2026-07-15T09:40:11",
"request-id": "9f8e7d6c-..."
}
}
}
You get this 401 Unauthorized when calling POST /teams/{team-id}/channels/{channel-id}/messages (or any Graph endpoint) with a token from az account get-access-token. The token is real and unexpired — it is simply aimed at the wrong service.
What the Error Means
Every access token has an audience (aud) claim naming the API it is valid for. Microsoft Graph only accepts tokens whose audience is Graph itself. By default, az account get-access-token mints a token for Azure Resource Manager (https://management.azure.com), because that is what most az commands talk to. Hand that ARM token to Graph and Graph says: this token was not issued for me — InvalidAuthenticationToken: Invalid audience.
This is an authentication failure (401), distinct from a permission failure (403 Authorization_RequestDenied). Fixing the audience is about which token you request, not what it is allowed to do.
Common Causes
- Calling
az account get-access-tokenwith no--resource/--scope, so you get an ARM-audience token. - Passing a resource that is close but wrong, e.g.
https://management.core.windows.net/or a Graph resource with a trailing typo. - Using the legacy Azure AD Graph audience (
https://graph.windows.net) instead of Microsoft Graph (https://graph.microsoft.com). They are different APIs. - Reusing a cached token from an earlier command that targeted a different resource.
Two Correct Ways to Request a Graph Token
Using —resource (audience form)
az account get-access-token \
--resource https://graph.microsoft.com \
--query accessToken -o tsv
Here --resource is the audience URI. The resulting token has aud: https://graph.microsoft.com. Note: the resource has no trailing /.default — that suffix belongs to the scope form, not the resource form.
Using —scope (v2 form)
az account get-access-token \
--scope https://graph.microsoft.com/.default \
--query accessToken -o tsv
--scope uses the v2.0 endpoint convention, where /.default means “all statically consented scopes for this resource.” Both approaches yield a token Graph accepts; pick one and be consistent. Do not combine --resource and --scope in the same call.
How to Reproduce the Error
Request a token with no resource (defaults to ARM) and call Graph:
TOKEN=$(az account get-access-token --query accessToken -o tsv)
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://graph.microsoft.com/v1.0/teams/{team-id}/channels/{channel-id}/messages" \
-d '{"body":{"content":"test"}}'
{ "error": { "code": "InvalidAuthenticationToken", "message": "Access token validation failure. Invalid audience." } }
Diagnostic Commands: Decode the JWT
The fastest way to diagnose an audience problem is to decode the token’s payload. A JWT is three base64url segments separated by dots; the middle one holds the claims:
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq '{aud, scp, roles, appid, upn}'
A correct Graph delegated token shows:
{
"aud": "https://graph.microsoft.com",
"scp": "ChannelMessage.Send User.Read ...",
"upn": "you@contoso.com"
}
A wrong (ARM) token shows something like:
{
"aud": "https://management.azure.com/",
"scp": "user_impersonation"
}
If aud is not exactly https://graph.microsoft.com, that is your bug. Note that base64url may need padding; if base64 -d errors, this variant is more forgiving:
python3 -c "import sys,base64,json; p=sys.argv[1].split('.')[1]; p+='='*(-len(p)%4); print(json.dumps(json.loads(base64.urlsafe_b64decode(p)),indent=2))" "$TOKEN"
Step-by-Step Resolution
- Re-request the token with the Graph audience explicitly set:
TOKEN=$(az account get-access-token \
--resource https://graph.microsoft.com \
--query accessToken -o tsv)
- Verify the
audclaim before doing anything else:
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq -r '.aud'
# expect: https://graph.microsoft.com
-
Confirm you are on Microsoft Graph, not Azure AD Graph. If
audishttps://graph.windows.net, you targeted the deprecated API — switch the resource tohttps://graph.microsoft.com. -
Retry the Graph call. With the audience correct, a valid delegated call returns
201 Created:
curl -s -o /dev/null -w "%{http_code}\n" -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://graph.microsoft.com/v1.0/teams/{team-id}/channels/{channel-id}/messages" \
-d '{"body":{"content":"audience fixed"}}'
- If you now get
403instead of401, the audience is right and you have moved on to a permissions problem — check that the token is delegated (hasscp, notroles) and thatChannelMessage.Sendis consented.
Prevention and Best Practices
- Never call
az account get-access-tokenfor Graph without--resource https://graph.microsoft.comor--scope https://graph.microsoft.com/.default. - Add a one-line
audassertion to scripts so a wrong-audience token fails loudly instead of returning a confusing401. - Remember the distinction:
401 InvalidAuthenticationToken= wrong/expired token or audience;403 Authorization_RequestDenied= right token, insufficient permission. - Do not mix Microsoft Graph and the deprecated Azure AD Graph resources; only
https://graph.microsoft.comworks for Teams messaging. - For scripting these token checks and Graph calls, the DevOps AI prompt library includes ready-to-use Azure CLI and Graph prompts.
Related Errors
403 Authorization_RequestDenied— correct audience but the identity lacks permission (often an app-only token used forChannelMessage.Send).CompactToken parsing failed— the token is truncated or malformed, not merely the wrong audience.401 / Lifetime validation failed— the token has expired; request a fresh one.AADSTS500011— the resource principal was not found in the tenant, usually a wrong resource URI.
Frequently Asked Questions
Why does my token work for az but not for Graph? By default az account get-access-token targets Azure Resource Manager, so its aud is https://management.azure.com. Graph only accepts tokens with aud: https://graph.microsoft.com, so you must pass --resource or --scope.
What is the difference between —resource and —scope? --resource https://graph.microsoft.com sets the audience directly (v1.0 style). --scope https://graph.microsoft.com/.default uses the v2.0 scope convention. Both yield a Graph-audience token; use one, not both.
How do I check the audience of my token? Base64-decode the second segment of the JWT and read the aud claim, for example echo "$TOKEN" | cut -d. -f2 | base64 -d | jq .aud. It must be https://graph.microsoft.com.
Does the resource need a trailing /.default? Only with --scope. With --resource you pass the bare audience https://graph.microsoft.com and no /.default suffix.
I fixed the audience and now get 403 instead of 401 — why? That is progress: authentication now succeeds, and you have a permissions issue. Ensure the token is delegated and that ChannelMessage.Send is consented. For more, see the Microsoft Teams guides.
Get 500 Battle-Tested DevOps AI Prompts — Free
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.