{
  "schemaVersion": "1.0",
  "code": "70002",
  "name": "InvalidClient",
  "title": "AADSTS70002 fix: error validating client credentials",
  "url": "https://brambrinkmeier.com/entra/aadsts/70002/",
  "alternates": {
    "markdown": "https://brambrinkmeier.com/entra/aadsts/70002.md",
    "json": "https://brambrinkmeier.com/api/aadsts/70002.json",
    "html": "https://brambrinkmeier.com/entra/aadsts/70002/"
  },
  "description": "AADSTS70002: Entra couldn't validate an app's credentials. Read the inner code, send the secret Value not its ID, and compare with 7000215.",
  "summary": "AADSTS70002 means Microsoft Entra ID couldn't validate the credentials an app sent, typically a client secret that doesn't match. A wrong secret today often comes back as 7000215 instead. 70002 is also seen as an outer code in front of a more specific one, so read the whole message. Usually a setup mistake; repeated failures from IPs you don't own deserve a look.",
  "inMicrosoftReference": true,
  "unlistedChecked": null,
  "meaningText": "Microsoft's description:\n\n> **InvalidClient**: Error validating the credentials. The specified client_secret does not match the expected value for this client. Correct the client_secret and try again. For more info, see [Use the authorization code to request an access token](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#redeem-a-code-for-an-access-token).\n\nA confidential client, such as a web app, a daemon or a connector, proves who it is with a client secret or a certificate. Microsoft's authorization code flow reference says the `client_secret` is *\"the application secret that you created in the app registration portal for your app\"* and that it *\"must be URL-encoded before being sent\"*. The SDK does that for you; a hand-built request may not. When Entra can't validate what was sent, the token request fails.\n\n### 70002 or 7000215?\nBoth are about a bad client secret, and they are easy to confuse.\n\n- [7000215](https://brambrinkmeier.com/entra/aadsts/7000215/) is *\"Invalid client secret is provided\"*. Its page records a token request with a wrong secret that came back as 7000215, with Microsoft's current hint to send *\"the client secret value, not the client secret ID\"*. A Microsoft Q&A thread that people find when searching for 70002, a Postman call with the wrong secret, also shows 7000215.\n- 70002 still appears in user reports, often with a second code after it. Quest's knowledge base shows *\"AADSTS70002: Error validating credentials. AADSTS50126: Invalid username or password\"*. Microsoft's own external MFA provider reference shows *\"AADSTS70002: Error validating credentials. AADSTS50012: External ID token from issuer ... failed signature verification\"* (Microsoft prints the prefix there as `ENTRA IDSTS`).\n\nOur reading: 70002 works as a general \"credential check failed\" code, and the code that follows it says what actually failed. If your message has a second AADSTS code, look that one up: [50126](https://brambrinkmeier.com/entra/aadsts/50126/) is a wrong user password, and 50012 is a token whose signature didn't verify. If 70002 stands alone, treat it as a wrong client secret.\n\nOther neighbours:\n\n- [7000222](https://brambrinkmeier.com/entra/aadsts/7000222/): the secret was right but has expired.\n- [7000218](https://brambrinkmeier.com/entra/aadsts/7000218/): no secret or assertion was sent at all.\n- [700016](https://brambrinkmeier.com/entra/aadsts/700016/): the client ID isn't found in the tenant. Check this first if the secret is definitely right.",
  "sections": [
    {
      "heading": "What does AADSTS70002 mean?",
      "text": "Microsoft's description:\n\n> **InvalidClient**: Error validating the credentials. The specified client_secret does not match the expected value for this client. Correct the client_secret and try again. For more info, see [Use the authorization code to request an access token](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#redeem-a-code-for-an-access-token).\n\nA confidential client, such as a web app, a daemon or a connector, proves who it is with a client secret or a certificate. Microsoft's authorization code flow reference says the `client_secret` is *\"the application secret that you created in the app registration portal for your app\"* and that it *\"must be URL-encoded before being sent\"*. The SDK does that for you; a hand-built request may not. When Entra can't validate what was sent, the token request fails.\n\n### 70002 or 7000215?\nBoth are about a bad client secret, and they are easy to confuse.\n\n- [7000215](https://brambrinkmeier.com/entra/aadsts/7000215/) is *\"Invalid client secret is provided\"*. Its page records a token request with a wrong secret that came back as 7000215, with Microsoft's current hint to send *\"the client secret value, not the client secret ID\"*. A Microsoft Q&A thread that people find when searching for 70002, a Postman call with the wrong secret, also shows 7000215.\n- 70002 still appears in user reports, often with a second code after it. Quest's knowledge base shows *\"AADSTS70002: Error validating credentials. AADSTS50126: Invalid username or password\"*. Microsoft's own external MFA provider reference shows *\"AADSTS70002: Error validating credentials. AADSTS50012: External ID token from issuer ... failed signature verification\"* (Microsoft prints the prefix there as `ENTRA IDSTS`).\n\nOur reading: 70002 works as a general \"credential check failed\" code, and the code that follows it says what actually failed. If your message has a second AADSTS code, look that one up: [50126](https://brambrinkmeier.com/entra/aadsts/50126/) is a wrong user password, and 50012 is a token whose signature didn't verify. If 70002 stands alone, treat it as a wrong client secret.\n\nOther neighbours:\n\n- [7000222](https://brambrinkmeier.com/entra/aadsts/7000222/): the secret was right but has expired.\n- [7000218](https://brambrinkmeier.com/entra/aadsts/7000218/): no secret or assertion was sent at all.\n- [700016](https://brambrinkmeier.com/entra/aadsts/700016/): the client ID isn't found in the tenant. Check this first if the secret is definitely right."
    },
    {
      "heading": "Is it an attack?",
      "text": "Usually not. The causes users report are setup mistakes: the secret ID pasted instead of the value, an old secret left in a config file after a rotation, or the right secret for the wrong app or tenant. UiPath's documentation, for example, tells users to check the tenant ID and application ID and to *\"copy the Secret Value, not the Secret ID\"*.\n\n| Pattern | Likely meaning |\n|---|---|\n| Failures for one app from your own servers, starting at a rotation date | A system still has the old secret. Update it. |\n| Failures since day one of a new integration | Secret ID instead of value, or a secret for another app registration. |\n| 70002 followed by 50126 | A user's password was wrong. See [50126](https://brambrinkmeier.com/entra/aadsts/50126/), not this page. |\n| Repeated failures for one app from IPs you don't own | Someone has an old or leaked secret, or is guessing. Treat it like [7000215](https://brambrinkmeier.com/entra/aadsts/7000215/) from a foreign IP (T1078.004 if it ever succeeds). |"
    },
    {
      "heading": "How do I find it in my logs (KQL)?",
      "text": "Microsoft doesn't document which sign-in table 70002 lands in. App-only requests would be expected in `AADServicePrincipalSignInLogs`, and requests on behalf of a user in the user tables; that is an assumption. Check, and compare with 7000215 so you see which code your tenant actually returns. One caveat, as on the [7000215 page](https://brambrinkmeier.com/entra/aadsts/7000215/): the reference for `AADServicePrincipalSignInLogs` describes `ResultType` as *\"Success or Failure\"*, so confirm failed rows carry the code; if they don't, filter on `ResultSignature` instead:\n\n```kusto\nunion withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(30d)\n| where ResultType in (\"70002\", \"7000215\")\n| summarize Rows = count() by SourceTable, ResultType\n```\n\nWhich apps fail, from where, and with which key. Microsoft documents `ClientCredentialType` as the type of client credential used (for example client assertion or client secret) and `ServicePrincipalCredentialKeyId` as the key ID of the credential:\n\n```kusto\nAADServicePrincipalSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType in (\"70002\", \"7000215\")\n| summarize Attempts = count(), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated),\n            CredTypes = make_set(ClientCredentialType, 5), KeyIds = make_set(ServicePrincipalCredentialKeyId, 10)\n          by AppId, ServicePrincipalName, IPAddress, ResultType\n| sort by Attempts desc\n```\n\nFailures from IPs that have never signed in successfully for that app in the last 30 days. Worth an alert; the 30-day window and a threshold of one are starting points:\n\n```kusto\nlet knownAppIPs =\n    AADServicePrincipalSignInLogs\n    | where TimeGenerated > ago(30d)\n    | where ResultType == \"0\"\n    | distinct AppId, IPAddress;\nAADServicePrincipalSignInLogs\n| where TimeGenerated > ago(1d)\n| where ResultType in (\"70002\", \"7000215\")\n| join kind=leftanti knownAppIPs on AppId, IPAddress\n| summarize Attempts = count(), LastSeen = max(TimeGenerated) by AppId, ServicePrincipalName, IPAddress, ResultType\n| sort by Attempts desc\n```\n\nIf the first query shows 70002 only in the user tables, run the third one against those with `UserPrincipalName` added. `ResultType` is a **string** column, so compare it with `\"70002\"`, not `70002`."
    },
    {
      "heading": "How do I fix AADSTS70002?",
      "text": "**For developers:**\n1. Read the full message. If a second AADSTS code follows, fix that one.\n2. Send the secret's **Value**, not its ID, for the same app registration as the `client_id`, against the tenant that app lives in.\n3. If you build the request by hand, URL-encode the secret, as Microsoft's reference requires.\n4. If you can't find the value any more, create a new client secret and use that.\n\n**For admins:**\n1. After a rotation, use the second query to find every system still failing, and update it.\n2. If failures come from IPs you don't own, treat the app's secrets as exposed: create a new credential, remove the old ones from the app registration, and update your own systems.\n3. Move the app to a certificate where it supports one. Microsoft *\"recommends that you use a certificate instead of a client secret before moving the application to a production environment.\"*"
    }
  ],
  "microsoftWording": {
    "text": "**InvalidClient**: Error validating the credentials. The specified client_secret does not match the expected value for this client. Correct the client_secret and try again. For more info, see [Use the authorization code to request an access token](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#redeem-a-code-for-an-access-token).",
    "source": "Microsoft's error code reference, as quoted on the page",
    "sourceUrl": "https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes"
  },
  "securityReading": {
    "category": {
      "id": "app-config",
      "label": "App configuration"
    },
    "kind": {
      "id": "failure",
      "label": "Failure"
    },
    "attackSignal": {
      "level": "medium",
      "label": "Medium: worth a look in context"
    },
    "mitre": [
      {
        "id": "T1078.004",
        "name": "Valid Accounts: Cloud Accounts",
        "url": "https://attack.mitre.org/techniques/T1078/004/"
      }
    ],
    "tables": [],
    "isItAnAttack": "Usually not. The causes users report are setup mistakes: the secret ID pasted instead of the value, an old secret left in a config file after a rotation, or the right secret for the wrong app or tenant. UiPath's documentation, for example, tells users to check the tenant ID and application ID and to *\"copy the Secret Value, not the Secret ID\"*.\n\n| Pattern | Likely meaning |\n|---|---|\n| Failures for one app from your own servers, starting at a rotation date | A system still has the old secret. Update it. |\n| Failures since day one of a new integration | Secret ID instead of value, or a secret for another app registration. |\n| 70002 followed by 50126 | A user's password was wrong. See [50126](https://brambrinkmeier.com/entra/aadsts/50126/), not this page. |\n| Repeated failures for one app from IPs you don't own | Someone has an old or leaked secret, or is guessing. Treat it like [7000215](https://brambrinkmeier.com/entra/aadsts/7000215/) from a foreign IP (T1078.004 if it ever succeeds). |",
    "conditionalAccess": null
  },
  "kql": [
    {
      "title": null,
      "leadIn": "Microsoft doesn't document which sign-in table 70002 lands in. App-only requests would be expected in `AADServicePrincipalSignInLogs`, and requests on behalf of a user in the user tables; that is an assumption. Check, and compare with 7000215 so you see which code your tenant actually returns. One caveat, as on the [7000215 page](/entra/aadsts/7000215/): the reference for `AADServicePrincipalSignInLogs` describes `ResultType` as *\"Success or Failure\"*, so confirm failed rows carry the code; if they don't, filter on `ResultSignature` instead:",
      "query": "union withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(30d)\n| where ResultType in (\"70002\", \"7000215\")\n| summarize Rows = count() by SourceTable, ResultType"
    },
    {
      "title": null,
      "leadIn": "Which apps fail, from where, and with which key. Microsoft documents `ClientCredentialType` as the type of client credential used (for example client assertion or client secret) and `ServicePrincipalCredentialKeyId` as the key ID of the credential:",
      "query": "AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType in (\"70002\", \"7000215\")\n| summarize Attempts = count(), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated),\n            CredTypes = make_set(ClientCredentialType, 5), KeyIds = make_set(ServicePrincipalCredentialKeyId, 10)\n          by AppId, ServicePrincipalName, IPAddress, ResultType\n| sort by Attempts desc"
    },
    {
      "title": null,
      "leadIn": "Failures from IPs that have never signed in successfully for that app in the last 30 days. Worth an alert; the 30-day window and a threshold of one are starting points:",
      "query": "let knownAppIPs =\n    AADServicePrincipalSignInLogs\n    | where TimeGenerated > ago(30d)\n    | where ResultType == \"0\"\n    | distinct AppId, IPAddress;\nAADServicePrincipalSignInLogs\n| where TimeGenerated > ago(1d)\n| where ResultType in (\"70002\", \"7000215\")\n| join kind=leftanti knownAppIPs on AppId, IPAddress\n| summarize Attempts = count(), LastSeen = max(TimeGenerated) by AppId, ServicePrincipalName, IPAddress, ResultType\n| sort by Attempts desc"
    }
  ],
  "kqlSectionText": "Microsoft doesn't document which sign-in table 70002 lands in. App-only requests would be expected in `AADServicePrincipalSignInLogs`, and requests on behalf of a user in the user tables; that is an assumption. Check, and compare with 7000215 so you see which code your tenant actually returns. One caveat, as on the [7000215 page](https://brambrinkmeier.com/entra/aadsts/7000215/): the reference for `AADServicePrincipalSignInLogs` describes `ResultType` as *\"Success or Failure\"*, so confirm failed rows carry the code; if they don't, filter on `ResultSignature` instead:\n\n```kusto\nunion withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(30d)\n| where ResultType in (\"70002\", \"7000215\")\n| summarize Rows = count() by SourceTable, ResultType\n```\n\nWhich apps fail, from where, and with which key. Microsoft documents `ClientCredentialType` as the type of client credential used (for example client assertion or client secret) and `ServicePrincipalCredentialKeyId` as the key ID of the credential:\n\n```kusto\nAADServicePrincipalSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType in (\"70002\", \"7000215\")\n| summarize Attempts = count(), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated),\n            CredTypes = make_set(ClientCredentialType, 5), KeyIds = make_set(ServicePrincipalCredentialKeyId, 10)\n          by AppId, ServicePrincipalName, IPAddress, ResultType\n| sort by Attempts desc\n```\n\nFailures from IPs that have never signed in successfully for that app in the last 30 days. Worth an alert; the 30-day window and a threshold of one are starting points:\n\n```kusto\nlet knownAppIPs =\n    AADServicePrincipalSignInLogs\n    | where TimeGenerated > ago(30d)\n    | where ResultType == \"0\"\n    | distinct AppId, IPAddress;\nAADServicePrincipalSignInLogs\n| where TimeGenerated > ago(1d)\n| where ResultType in (\"70002\", \"7000215\")\n| join kind=leftanti knownAppIPs on AppId, IPAddress\n| summarize Attempts = count(), LastSeen = max(TimeGenerated) by AppId, ServicePrincipalName, IPAddress, ResultType\n| sort by Attempts desc\n```\n\nIf the first query shows 70002 only in the user tables, run the third one against those with `UserPrincipalName` added. `ResultType` is a **string** column, so compare it with `\"70002\"`, not `70002`.",
  "fix": {
    "short": "Developer: read the second AADSTS code in the message, then send the secret's Value (not its ID) for the right app and tenant, or create a new secret. Admin: after a rotation, update every system that still fails. If failures come from IPs you don't own, replace the app's credentials.",
    "steps": "**For developers:**\n1. Read the full message. If a second AADSTS code follows, fix that one.\n2. Send the secret's **Value**, not its ID, for the same app registration as the `client_id`, against the tenant that app lives in.\n3. If you build the request by hand, URL-encode the secret, as Microsoft's reference requires.\n4. If you can't find the value any more, create a new client secret and use that.\n\n**For admins:**\n1. After a rotation, use the second query to find every system still failing, and update it.\n2. If failures come from IPs you don't own, treat the app's secrets as exposed: create a new credential, remove the old ones from the app registration, and update your own systems.\n3. Move the app to a certificate where it supports one. Microsoft *\"recommends that you use a certificate instead of a client secret before moving the application to a production environment.\"*"
  },
  "labReproduced": null,
  "labSectionText": null,
  "labRuns": [
    {
      "date": "2026-10-31",
      "run": "round 8",
      "method": "token-endpoint and device-code requests against a lab tenant",
      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/72",
      "scenario": "Device code redeemed with a different client ID",
      "expected": "70016 / 7000215 / 70002",
      "got": "90023",
      "matchedExpectation": false,
      "message": "AADSTS90023: ClientId passed in the request doesn't match the one in cache"
    },
    {
      "date": "2026-10-31",
      "run": "round 9",
      "method": "token-endpoint and device-code requests against a lab tenant",
      "issue": "https://github.com/GrantZero604/Brambrinkmeier.com/issues/73",
      "scenario": "Device code redeemed with a different client ID",
      "expected": "70016 / 7000215 / 70002",
      "got": "90023",
      "matchedExpectation": false,
      "message": "AADSTS90023: ClientId passed in the request doesn't match the one in cache"
    }
  ],
  "verification": "documentation",
  "related": [
    "7000215",
    "7000222",
    "7000218",
    "50126",
    "700016"
  ],
  "coveredBy": [],
  "sources": [
    {
      "title": "Microsoft Entra authentication and authorization error codes",
      "url": "https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes"
    },
    {
      "title": "Microsoft identity platform and OAuth 2.0 authorization code flow",
      "url": "https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow"
    },
    {
      "title": "Microsoft Entra multifactor authentication external method provider reference",
      "url": "https://learn.microsoft.com/entra/identity/authentication/concept-authentication-external-method-provider"
    },
    {
      "title": "Add credentials to your application",
      "url": "https://learn.microsoft.com/en-us/entra/identity-platform/how-to-add-credentials"
    },
    {
      "title": "AADServicePrincipalSignInLogs table reference (Azure Monitor)",
      "url": "https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/aadserviceprincipalsigninlogs"
    },
    {
      "title": "Microsoft Q&A: Problem using Postman to call REST APIs",
      "url": "https://learn.microsoft.com/en-us/answers/questions/323082/problem-using-postman-to-call-rest-apis-error-desc"
    },
    {
      "title": "UiPath documentation: AADSTS70002 Error validating credentials",
      "url": "https://docs.uipath.com/activities/other/latest/productivity/aadsts70002-error-validating-credentials.md"
    },
    {
      "title": "Quest knowledge base: AADSTS70002 Error validating credentials. AADSTS50126",
      "url": "https://support.quest.com/kb/4258624/aadsts70002-error-validating-credentials-aadsts50126-invalid-username-or-password"
    }
  ],
  "published": "2026-11-01",
  "lastUpdated": "2026-11-01",
  "lastVerified": null,
  "license": "CC BY 4.0",
  "licenseUrl": "https://creativecommons.org/licenses/by/4.0/",
  "licenseNote": "CC BY 4.0 covers the author's own text and classification fields. Quoted Microsoft error wording belongs to Microsoft, is quoted with its source and can change.",
  "attribution": "Bram Brinkmeier, \"AADSTS70002 fix: error validating client credentials\", https://brambrinkmeier.com/entra/aadsts/70002/ (CC BY 4.0)",
  "microsoftErrorLookup": "https://login.microsoftonline.com/error?code=70002"
}
