Microsoft Teams Error Guide: 'InvalidAuthenticationToken' Expired or Invalid OAuth Bearer Token
Fix the Microsoft Graph API 401 InvalidAuthenticationToken error in Teams: diagnose expired tokens, wrong audiences, missing scopes, and clock skew.
- #microsoft-teams
- #troubleshooting
- #errors
- #graph-api
Overview
A 401 Unauthorized with code InvalidAuthenticationToken means the Microsoft Graph API rejected the bearer token attached to the request. The token may be expired, signed for a different audience, issued for the wrong tenant, or simply malformed. Graph validates the JWT signature, audience claim (aud), expiry (exp), and issuer (iss) on every call — if any check fails the request is rejected before any authorization logic runs.
The literal JSON error body from Graph:
{
"error": {
"code": "InvalidAuthenticationToken",
"message": "Access token is empty.",
"innerError": {
"date": "2026-06-23T14:02:11",
"request-id": "a1b2c3d4-1111-2222-3333-444455556666",
"client-request-id": "a1b2c3d4-1111-2222-3333-444455556666"
}
}
}
Or, for an expired token:
{
"error": {
"code": "InvalidAuthenticationToken",
"message": "Access token has expired or is not yet valid.",
"innerError": {
"date": "2026-06-23T14:05:33",
"request-id": "b2c3d4e5-2222-3333-4444-555566667777",
"client-request-id": "b2c3d4e5-2222-3333-4444-555566667777"
}
}
}
It occurs on every Graph API call: sending Teams messages, reading channel data, managing memberships, or calling the /me profile endpoint. The HTTP status is always 401.
Symptoms
- Every API request to
https://graph.microsoft.com/v1.0/...returns401. - Application logs show
InvalidAuthenticationTokenorAccess token has expired. - Teams bot or integration stops posting messages or reading channel data.
- Refresh logic that re-uses a cached token silently fails when the token ages past 3600 seconds.
curl -s -X GET "https://graph.microsoft.com/v1.0/me" \
-H "Authorization: Bearer <YOUR_TOKEN>" \
-H "Content-Type: application/json"
{
"error": {
"code": "InvalidAuthenticationToken",
"message": "Access token has expired or is not yet valid.",
"innerError": {
"date": "2026-06-23T14:02:11",
"request-id": "a1b2c3d4-1111-2222-3333-444455556666",
"client-request-id": "a1b2c3d4-1111-2222-3333-444455556666"
}
}
}
Common Root Causes
1. Token has expired (past the exp claim)
Microsoft Graph access tokens have a default lifetime of 3600 seconds (1 hour). If your application caches the token and does not refresh it before expiry, every subsequent request fails.
# Decode the JWT payload (base64url decode the middle segment)
TOKEN="<YOUR_TOKEN>"
echo "$TOKEN" | cut -d'.' -f2 | base64 -d 2>/dev/null | jq '{exp, iat, aud, iss}'
{
"exp": 1750688400,
"iat": 1750684800,
"aud": "https://graph.microsoft.com",
"iss": "https://sts.windows.net/aaaabbbb-1111-2222-3333-ccccddddeeee/"
}
Compare exp to the current Unix timestamp (date +%s). If exp is in the past, the token is expired.
2. Wrong audience (aud) claim
A token acquired for one resource (e.g., https://management.azure.com/) cannot be used against Graph. The aud claim must be exactly https://graph.microsoft.com.
# Acquire a token and check its audience
az account get-access-token --resource "https://management.azure.com/" \
| jq -r '.accessToken' \
| cut -d'.' -f2 | base64 -d 2>/dev/null | jq '.aud'
"https://management.azure.com/"
If aud is anything other than https://graph.microsoft.com, Graph will reject it with InvalidAuthenticationToken.
3. Token acquired for a different tenant
If the application authenticates against tenant A but the Teams resource belongs to tenant B, Graph returns 401. The iss claim in the token will reference the wrong tenant ID.
# Get a token for the correct tenant explicitly
az account get-access-token \
--tenant "<CORRECT_TENANT_ID>" \
--resource "https://graph.microsoft.com" \
| jq -r '.accessToken' \
| cut -d'.' -f2 | base64 -d 2>/dev/null | jq '{iss, tid}'
{
"iss": "https://sts.windows.net/wrongtenant-1111-2222-3333-444455556666/",
"tid": "wrongtenant-1111-2222-3333-444455556666"
}
The tid must match the tenant that owns the Teams resource.
4. Empty or malformed Authorization header
The header must be Authorization: Bearer <token> — the word Bearer (capital B) followed by a single space and the raw JWT. A missing Bearer prefix, double spaces, or a truncated token all produce InvalidAuthenticationToken.
# Test with a deliberately malformed header
curl -s -X GET "https://graph.microsoft.com/v1.0/me" \
-H "Authorization: bearer<NO_SPACE><TOKEN>" \
-H "Content-Type: application/json" | jq .error.code
"InvalidAuthenticationToken"
5. Clock skew between the token-issuing host and Graph
If the host clock is significantly ahead of UTC, the nbf (not-before) claim may be in the future from Graph’s perspective. Microsoft Graph tolerates a 5-minute skew; anything beyond that causes rejection.
# Check host clock against a known time server
date -u
curl -sI https://graph.microsoft.com/v1.0/ | grep -i date
Mon, 23 Jun 2026 14:02:11 GMT # host
Mon, 23 Jun 2026 14:09:55 GMT # Graph response Date header
More than 5 minutes of difference indicates a clock synchronization problem.
6. Client secret or certificate has expired in Azure AD
If the credential used to acquire tokens has expired in the app registration, the token-acquisition step itself may silently return an invalid or rejected token, or the OAuth flow returns an error that is mistakenly forwarded.
# List credentials for your app registration
az ad app credential list \
--id "<APP_CLIENT_ID>" \
--query "[].{id:keyId, endDate:endDateTime, type:type}" \
-o table
Id EndDate Type
------------------------------------ ------------------------ --------
aabb1122-3344-5566-7788-99aabbccddee 2026-05-01T00:00:00+00:00 Password
An endDate in the past means the secret has expired and token acquisition will fail or return garbage.
Diagnostic Workflow
Step 1: Decode the token and check exp
TOKEN=$(az account get-access-token \
--resource "https://graph.microsoft.com" \
| jq -r '.accessToken')
echo "$TOKEN" | cut -d'.' -f2 | base64 -d 2>/dev/null \
| jq '{aud, iss, tid, exp, nbf, iat}'
Confirm aud is https://graph.microsoft.com, exp is in the future, and tid matches your tenant. If any are wrong, the token itself is the problem — not the API call.
Step 2: Re-acquire a fresh token and retry immediately
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/me" \
-H "Authorization: Bearer $TOKEN" | jq .
If this succeeds, the issue is stale token caching in your application, not an auth configuration problem.
Step 3: Verify the app registration credential is still valid
az ad app credential list \
--id "<APP_CLIENT_ID>" \
--query "[].{endDate:endDateTime,type:type}" \
-o table
If all credentials are expired, rotate the secret or upload a new certificate immediately.
Step 4: Check host clock synchronization
# Compare host UTC to Graph's Date header
echo "Host UTC: $(date -u)"
echo "Graph UTC: $(curl -sI https://graph.microsoft.com/v1.0/ | grep -i '^date:' | awk '{print $2,$3,$4,$5,$6}')"
If the skew exceeds 5 minutes, synchronize via chronyc or timedatectl:
sudo timedatectl set-ntp true
chronyc tracking | grep 'System time'
Step 5: Validate the Authorization header format in code
# Capture the exact header your application sends with verbose curl
curl -v -X GET "https://graph.microsoft.com/v1.0/me" \
-H "Authorization: Bearer $TOKEN" 2>&1 | grep -i "> authorization"
Confirm the output is > Authorization: Bearer eyJ0... — the literal string Bearer with a capital B and a single space before the JWT.
Example Root Cause Analysis
A CI pipeline that posts deployment notifications to a Teams channel begins failing every morning between 09:00 and 09:05 UTC with InvalidAuthenticationToken. Other API calls outside that window succeed.
Checking the token expiry in the pipeline logs shows the token was issued at 08:00 UTC the previous day and has a 3600-second lifetime — it expired at 09:00 UTC. The pipeline caches the token in a shared environment variable across jobs that run over several hours without refreshing.
Decoding the stored token confirms exp is 3600 seconds after iat:
echo "$CACHED_TOKEN" | cut -d'.' -f2 | base64 -d 2>/dev/null \
| jq '{iat, exp, lifetime: (.exp - .iat)}'
{
"iat": 1750640400,
"exp": 1750644000,
"lifetime": 3600
}
Fix: acquire a fresh token at the start of each job using az account get-access-token rather than sharing a pre-generated token across jobs, or implement a refresh check that re-acquires if exp - now < 300 (5-minute buffer).
Prevention Best Practices
- Always treat access tokens as short-lived (1 hour default). Acquire a new token per job or per request batch — never cache beyond the
expminus a 5-minute safety margin. - Implement token refresh logic that checks
expbefore each API call and re-acquires if within 300 seconds of expiry. - Use the
expiresOnfield fromaz account get-access-tokendirectly rather than computing expiry manually. - Rotate app registration client secrets before their expiry date — set a calendar reminder at 60, 30, and 7 days ahead.
- Synchronize NTP on all hosts running Graph API clients; a clock drift beyond 5 minutes will cause intermittent
InvalidAuthenticationTokenfailures that are hard to reproduce. - For ad-hoc triage of repeated auth failures, the incident assistant can correlate token timestamps and pipeline logs into a likely root cause quickly.
Quick Command Reference
# Acquire a fresh Graph token via Azure CLI
TOKEN=$(az account get-access-token \
--tenant "<TENANT_ID>" \
--resource "https://graph.microsoft.com" \
| jq -r '.accessToken')
# Decode token claims (exp, aud, iss, tid)
echo "$TOKEN" | cut -d'.' -f2 | base64 -d 2>/dev/null \
| jq '{aud, iss, tid, exp, nbf, iat}'
# Check token expiry vs now
echo "Token exp: $(echo "$TOKEN" | cut -d'.' -f2 | base64 -d 2>/dev/null | jq '.exp')"
echo "Now: $(date +%s)"
# Test token against Graph /me
curl -s -X GET "https://graph.microsoft.com/v1.0/me" \
-H "Authorization: Bearer $TOKEN" | jq '{displayName, mail, id}'
# List app credentials and their expiry
az ad app credential list \
--id "<APP_CLIENT_ID>" \
--query "[].{id:keyId, endDate:endDateTime, type:type}" -o table
# Check host clock vs Graph
date -u && curl -sI https://graph.microsoft.com/v1.0/ | grep -i '^date:'
Conclusion
A 401 InvalidAuthenticationToken from Microsoft Graph always means the bearer token itself failed validation before Graph checked any permissions. The root causes in order of frequency:
- The access token has expired past its
expclaim — acquire a fresh token. - The token was issued for the wrong audience (not
https://graph.microsoft.com). - The token was acquired against a different tenant than the Teams resource belongs to.
- The
Authorizationheader is malformed (missingBearerprefix, double spaces, truncated token). - Host clock skew of more than 5 minutes causes the
nbfclaim to be rejected. - The app registration client secret or certificate has expired, corrupting token acquisition.
Decode the JWT payload first — the aud, tid, and exp claims identify the root cause in under a minute. 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.