Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Microsoft Teams By James Joyner IV · · 10 min read

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/... returns 401.
  • Application logs show InvalidAuthenticationToken or Access 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 exp minus a 5-minute safety margin.
  • Implement token refresh logic that checks exp before each API call and re-acquires if within 300 seconds of expiry.
  • Use the expiresOn field from az account get-access-token directly 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 InvalidAuthenticationToken failures 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:

  1. The access token has expired past its exp claim — acquire a fresh token.
  2. The token was issued for the wrong audience (not https://graph.microsoft.com).
  3. The token was acquired against a different tenant than the Teams resource belongs to.
  4. The Authorization header is malformed (missing Bearer prefix, double spaces, truncated token).
  5. Host clock skew of more than 5 minutes causes the nbf claim to be rejected.
  6. 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.

Free download · 368-page PDF

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.