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

Microsoft Teams Error Guide: 'AADSTS7000215: Invalid client secret provided' — Fix Expired Entra Credentials

Quick answer

Fix AADSTS7000215 on Teams Graph integrations: an expired, wrong, or malformed Entra app client secret. Rotate with overlapping credentials, verify the token, and prevent recurrence.

Part of the Microsoft Entra ID (Azure AD) Sign-in Errors hub
  • #microsoft-teams
  • #adaptive-cards
  • #troubleshooting
  • #errors
Free toolkit

Fixing errors like this? Get 500 free DevOps AI prompts

500 copy-paste AI prompts for the stack you actually run — one PDF, free.

Overview

AADSTS7000215 is the error Entra ID (Azure AD) returns during the OAuth2 client-credentials flow when the client secret your Teams integration presents does not match a valid, unexpired secret on the app registration. The token endpoint returns HTTP 401 with:

{
  "error": "invalid_client",
  "error_description": "AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'a1b2c3d4-5678-90ab-cdef-1234567890ab'.\r\nTrace ID: 8f2c...\r\nCorrelation ID: 3e9a...\r\nTimestamp: 2026-07-06 09:14:03Z"
}

Because this is a token-acquisition failure, it takes down everything the app does through Microsoft Graph at once: the bot stops replying, change-notification handlers stop renewing subscriptions, provisioning scripts fail, and cards stop posting — all with no code change on your side. The most common trigger is a secret that silently reached its expiry date.

Symptoms

  • Every Graph call fails at the token step; the app cannot get an access token at all.
  • The token endpoint returns HTTP 401 with error: invalid_client and AADSTS7000215 in error_description.
  • The failure started at a specific timestamp with no deployment — matching a secret’s endDateTime.
  • A Teams bot returns 401/500 to the Bot Framework because it cannot exchange credentials for a Graph token.
  • The same code works locally (using a fresh secret) but fails in production (using the expired one).
  • AADSTS7000215 also appears when the secret ID (a GUID) was stored instead of the secret value.

Common Root Causes

1. The client secret expired

Entra client secrets have a hard endDateTime (max 24 months). There is no grace period — at the expiry instant, AADSTS7000215 begins immediately. List the app’s secrets and check expiry:

az ad app credential list --id "$APP_ID" \
  --query "[].{id:keyId, end:endDateTime}" -o table
Id                                    End
------------------------------------  --------------------
7c9e6f...                             2026-07-06T00:00:00Z

If the newest end is in the past, the secret expired.

2. The secret ID was stored instead of the secret value

Entra shows a secret’s value only once, at creation. If your config holds the secret’s keyId (a GUID) rather than the one-time value, every token request fails with AADSTS7000215. The value is typically ~40 characters and is not a GUID.

3. Trailing whitespace, newline, or truncation in the stored secret

A secret copied into an env file, systemd unit, or CI variable with a trailing \n, a shell-escaped character, or a truncated tail no longer matches. The token endpoint sees a different string and rejects it.

4. Wrong client_id / tenant paired with the secret

A secret is valid only for the app registration it belongs to. Using a secret from app A with app B’s client_id, or hitting the wrong tenant’s token endpoint, yields AADSTS7000215 (or the related AADSTS700016 for an unknown app).

5. The secret was rotated/deleted out from under the running app

Someone deleted or replaced the secret in the portal. The running process still presents the old value and starts failing at the next token request (or when its cached token expires, typically within the hour).

6. Using a secret where the app expects a certificate

If the app registration was configured for certificate credentials (or federated identity) and the client sends a client_secret, the request is rejected. Related certificate error: AADSTS700027.

Diagnostic Workflow

Step 1: Reproduce the token request directly

Isolate the failure from application code by calling the token endpoint with curl:

curl -s -X POST \
  "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=$APP_ID" \
  -d "scope=https://graph.microsoft.com/.default" \
  -d "client_secret=$CLIENT_SECRET" \
  -d "grant_type=client_credentials" | jq .

A body containing AADSTS7000215 confirms the secret — not the network, scope, or Graph endpoint — is the problem.

Step 2: Check for whitespace or wrong value shape

# Length and any hidden trailing characters
printf '%s' "$CLIENT_SECRET" | wc -c
printf '%s' "$CLIENT_SECRET" | od -c | tail -n 2

A secret value is roughly 40 characters and is not a GUID. If your value looks like 7c9e6f...-....-.... (a GUID), you stored the secret ID, not the value.

Step 3: Confirm the secret exists and is unexpired on the app

az ad app credential list --id "$APP_ID" \
  --query "[].{id:keyId, start:startDateTime, end:endDateTime}" -o table

Compare the current UTC time against every end. If all are in the past, or the list is empty, you need a new secret.

Step 4: Verify you are hitting the right tenant and app

echo "tenant=$TENANT_ID app=$APP_ID"
az ad app show --id "$APP_ID" --query "{displayName:displayName, appId:appId}" -o json

Ensure the appId matches the client_id your app sends and the tenant matches your token URL.

Step 5: On success, confirm one live Graph call

Once a token comes back, prove the credential really works end to end:

TOKEN=$(curl -s -X POST \
  "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=$APP_ID" -d "scope=https://graph.microsoft.com/.default" \
  -d "client_secret=$CLIENT_SECRET" -d "grant_type=client_credentials" \
  | jq -r .access_token)

curl -s "https://graph.microsoft.com/v1.0/organization" \
  -H "Authorization: Bearer $TOKEN" | jq '.value[0].displayName'

Example Root Cause Analysis

A Teams change-notification handler that had run untouched for a year started returning 500s at 00:00 UTC. No deploy had gone out. The service log showed every Graph call failing at token acquisition with invalid_client.

Reproducing the token request:

curl -s -X POST \
  "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=$APP_ID" -d "scope=https://graph.microsoft.com/.default" \
  -d "client_secret=$CLIENT_SECRET" -d "grant_type=client_credentials" \
  | jq -r .error_description
AADSTS7000215: Invalid client secret provided...

Listing the app’s credentials showed the only secret had endDateTime: 2026-07-06T00:00:00Z — exactly when failures began. The secret had a 24-month lifetime that no one tracked.

The fix used the overlapping-credential pattern to avoid a second outage. A new secret was added before removing anything:

# Add a new secret (2-year lifetime) without deleting the old one
az ad app credential reset --id "$APP_ID" \
  --append --years 2 \
  --query "{value:password, end:endDateTime}" -o json

The new value was written to the secret manager, the service was rolled to read it, a live GET /v1.0/organization succeeded, and only then was the expired credential cleaned up. Finally, an expiry-audit job was scheduled so the next rotation is planned, not reactive.

Prevention Best Practices

  • Track every secret’s endDateTime and rotate on a schedule — run a periodic Graph/az audit that flags any credential expiring within 30 days and post it to Teams.
  • Use the overlapping-credential pattern: add the new secret (az ad app credential reset --append) and cut over before deleting the old one, so there is never zero valid credentials.
  • Prefer certificate credentials or workload identity federation over long-lived client secrets — longer life, no plaintext secret, and no accidental “stored the ID not the value” mistakes.
  • Store the secret value (not the keyId) in a secret manager, and read it at runtime — never bake it into an image or commit it.
  • Strip trailing whitespace/newlines when loading the secret; validate its length and shape at startup and fail fast with a clear message.
  • Add a startup self-test that acquires a token and makes one harmless Graph read, so a bad or expired credential is caught at deploy time, not at 00:00 UTC.
  • Cross-reference credential failures against the Microsoft Teams guides and the incident assistant.

Quick Command Reference

# Reproduce the token request in isolation
curl -s -X POST "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=$APP_ID" -d "scope=https://graph.microsoft.com/.default" \
  -d "client_secret=$CLIENT_SECRET" -d "grant_type=client_credentials" | jq .

# List app credentials and expiry
az ad app credential list --id "$APP_ID" \
  --query "[].{id:keyId, end:endDateTime}" -o table

# Add a new secret WITHOUT removing the old one (overlap)
az ad app credential reset --id "$APP_ID" --append --years 2 \
  --query "{value:password, end:endDateTime}" -o json

# Detect a stored ID vs value (a GUID means you stored the ID)
printf '%s' "$CLIENT_SECRET" | wc -c
printf '%s' "$CLIENT_SECRET" | od -c | tail -n 2

# Verify a live Graph call after rotation
TOKEN=$(curl -s -X POST "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=$APP_ID" -d "scope=https://graph.microsoft.com/.default" \
  -d "client_secret=$CLIENT_SECRET" -d "grant_type=client_credentials" | jq -r .access_token)
curl -s "https://graph.microsoft.com/v1.0/organization" \
  -H "Authorization: Bearer $TOKEN" | jq '.value[0].displayName'

Conclusion

AADSTS7000215 is a credential-mismatch error at the OAuth2 token step, and it takes the whole Teams integration offline at once because nothing can get a token. In order of frequency the causes are:

  1. The client secret expired (no grace period).
  2. The secret ID was stored instead of the secret value.
  3. Trailing whitespace, a newline, or truncation corrupted the stored secret.
  4. A secret paired with the wrong client_id or tenant.
  5. The secret was rotated or deleted out from under the running app.
  6. A secret sent to an app configured for certificate credentials.

The fastest fix path is: reproduce the token request with curl to confirm it is the secret, check the value’s shape and the app’s endDateTime, then rotate using an overlapping credential — add the new secret, verify a live Graph call, and only then remove the old one. Schedule an expiry audit so it never recurs; see the Microsoft Teams guides.

Free download · 368-page PDF

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.