{
  "schemaVersion": "1.0",
  "code": "7000222",
  "name": "InvalidClientSecretExpiredKeysProvided",
  "title": "AADSTS7000222: The provided client secret keys are expired",
  "url": "https://brambrinkmeier.com/entra/aadsts/7000222/",
  "alternates": {
    "markdown": "https://brambrinkmeier.com/entra/aadsts/7000222.md",
    "json": "https://brambrinkmeier.com/api/aadsts/7000222.json",
    "html": "https://brambrinkmeier.com/entra/aadsts/7000222/"
  },
  "description": "AADSTS7000222: an app sent an expired client secret. A missed rotation, low attack value; a cue to move to certificates or managed identity. KQL.",
  "summary": "AADSTS7000222 means an app or service principal sent a client secret that has passed its expiry date. It is almost always a missed rotation, not an attack. The security angle is secret hygiene: an expired secret still sitting in config files shows where secrets live. Microsoft recommends managed identities or certificates instead. Hunt it in AADServicePrincipalSignInLogs.",
  "inMicrosoftReference": true,
  "unlistedChecked": null,
  "meaningText": "Microsoft's description:\n\n> **InvalidClientSecretExpiredKeysProvided**: The provided client secret keys are expired. Create new keys for your app, or consider using certificate credentials for added security\n\nAn app tried to get a token with a client secret that was valid once but has passed its end date. The secret itself was right. Nobody replaced it in time.\n\nTwo siblings help you tell the cases apart:\n\n- **[7000215](https://brambrinkmeier.com/entra/aadsts/7000215/)**: *\"Invalid client secret is provided.\"* A secret was sent and it was wrong.\n- **7000218**: *\"The request body must contain the following parameter: 'client_assertion' or 'client_secret'.\"* No secret or assertion was sent at all.",
  "sections": [
    {
      "heading": "What does AADSTS7000222 mean?",
      "text": "Microsoft's description:\n\n> **InvalidClientSecretExpiredKeysProvided**: The provided client secret keys are expired. Create new keys for your app, or consider using certificate credentials for added security\n\nAn app tried to get a token with a client secret that was valid once but has passed its end date. The secret itself was right. Nobody replaced it in time.\n\nTwo siblings help you tell the cases apart:\n\n- **[7000215](https://brambrinkmeier.com/entra/aadsts/7000215/)**: *\"Invalid client secret is provided.\"* A secret was sent and it was wrong.\n- **7000218**: *\"The request body must contain the following parameter: 'client_assertion' or 'client_secret'.\"* No secret or assertion was sent at all."
    },
    {
      "heading": "Is it an attack?",
      "text": "Almost never. 7000222 is what a missed rotation looks like: a script, scheduled job or integration still holds a secret that expired. It usually shows up as a sudden outage of that integration.\n\n| Pattern | Likely meaning |\n|---|---|\n| 7000222 for one app from your own servers, starting on a specific day | The secret expired that day and nobody rotated it. Benign, but the integration is broken. |\n| 7000222 from your own servers after you created a new secret | You added a new secret but missed one system that still holds the old one. |\n| 7000222 for an app from an IP you don't own | Someone outside holds a copy of an old secret for your app. Our interpretation, not a Microsoft statement: the expiry stopped them, but the secret leaked at some point. Find out where it was stored. |\n\nThe useful security reading is the third row and the inventory it gives you. Every IP that sends an expired secret is a place where that secret was stored. If one of those systems was compromised, read [hunting credential reuse after a web-server breach](https://brambrinkmeier.com/guides/hunt-credential-reuse-after-web-server-breach/) for which other secrets to rotate."
    },
    {
      "heading": "How do I find it in my logs (KQL)?",
      "text": "Which apps are failing, and from where:\n\n```kusto\nAADServicePrincipalSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType == \"7000222\"\n| summarize Attempts = count(),\n            FirstSeen = min(TimeGenerated),\n            LastSeen = max(TimeGenerated),\n            KeyIds = make_set(ServicePrincipalCredentialKeyId, 10)\n          by AppId, ServicePrincipalName, IPAddress\n| sort by Attempts desc\n```\n\nExpired-secret attempts from IPs that never had a success for the same app in the last 30 days:\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(7d)\n| where ResultType == \"7000222\"\n| join kind=leftanti knownAppIPs on AppId, IPAddress\n| summarize Attempts = count(), LastSeen = max(TimeGenerated) by AppId, ServicePrincipalName, IPAddress\n| sort by Attempts desc\n```\n\nCheck two things in your own workspace before relying on these queries:\n\n- The Azure Monitor reference for `AADServicePrincipalSignInLogs` describes `ResultType` as *\"Success or Failure\"*, while the user sign-in tables document it as the numeric error code as a string. Look at a few failed rows and confirm they carry `\"7000222\"` and that successes carry `\"0\"`. If they don't, filter on `ResultSignature` instead.\n- Whether `ServicePrincipalCredentialKeyId` is filled in on an expired-secret failure isn't documented. If it is, it tells you exactly which secret is still in use, which makes cleanup easy.\n\nA secret used by a confidential client in a delegated (user) flow may surface in the user sign-in tables instead. That is our reading of how the flows work, not a documented statement. If the first query returns nothing but users report the error, search `SigninLogs` for the same `ResultType`."
    },
    {
      "heading": "How does Conditional Access affect it?",
      "text": "- 7000222 is a failed credential check, so no policy grants or blocks it. Conditional Access for workload identities can't fix an expired secret.\n- Microsoft notes that workload identities *\"Can't perform multifactor authentication\"* and *\"Need to store their credentials or secrets somewhere.\"* The control that matters is the credential type.\n- Conditional Access for workload identities can add location and risk conditions to single-tenant service principals, which limits where a leaked secret works. It needs Workload Identities Premium licenses."
    },
    {
      "heading": "How do I fix AADSTS7000222?",
      "text": "**For developers:** create a new credential on the app registration's **Certificates and secrets** page and update every system that uses it. Don't stop at the first system that works again.\n\n**For admins:**\n1. Use the first query to list every IP still sending the expired secret, and update or retire each of those systems.\n2. Remove the expired secret from the app registration once nothing uses it. Microsoft: *\"An unused credential on an application can result in a security breach.\"*\n3. Move away from secrets. Microsoft's order of preference is a managed identity first, then certificate credentials: *\"Don't use password credentials, also known as secrets.\"* Certificate credentials work *\"anywhere a client secret could be used\"*, including the client credentials flow.\n4. Use application management policies to limit secret lifetimes or block new secrets, so the next rotation is planned rather than an outage.\n5. If the second query returns an IP you don't own, treat every credential that was stored alongside that secret as exposed."
    }
  ],
  "microsoftWording": {
    "text": "**InvalidClientSecretExpiredKeysProvided**: The provided client secret keys are expired. Create new keys for your app, or consider using certificate credentials for added security",
    "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": "low",
      "label": "Low: usually configuration or user error"
    },
    "mitre": [],
    "tables": [
      "AADServicePrincipalSignInLogs"
    ],
    "isItAnAttack": "Almost never. 7000222 is what a missed rotation looks like: a script, scheduled job or integration still holds a secret that expired. It usually shows up as a sudden outage of that integration.\n\n| Pattern | Likely meaning |\n|---|---|\n| 7000222 for one app from your own servers, starting on a specific day | The secret expired that day and nobody rotated it. Benign, but the integration is broken. |\n| 7000222 from your own servers after you created a new secret | You added a new secret but missed one system that still holds the old one. |\n| 7000222 for an app from an IP you don't own | Someone outside holds a copy of an old secret for your app. Our interpretation, not a Microsoft statement: the expiry stopped them, but the secret leaked at some point. Find out where it was stored. |\n\nThe useful security reading is the third row and the inventory it gives you. Every IP that sends an expired secret is a place where that secret was stored. If one of those systems was compromised, read [hunting credential reuse after a web-server breach](https://brambrinkmeier.com/guides/hunt-credential-reuse-after-web-server-breach/) for which other secrets to rotate.",
    "conditionalAccess": {
      "heading": "How does Conditional Access affect it?",
      "text": "- 7000222 is a failed credential check, so no policy grants or blocks it. Conditional Access for workload identities can't fix an expired secret.\n- Microsoft notes that workload identities *\"Can't perform multifactor authentication\"* and *\"Need to store their credentials or secrets somewhere.\"* The control that matters is the credential type.\n- Conditional Access for workload identities can add location and risk conditions to single-tenant service principals, which limits where a leaked secret works. It needs Workload Identities Premium licenses."
    }
  },
  "kql": [
    {
      "title": null,
      "leadIn": "Which apps are failing, and from where:",
      "query": "AADServicePrincipalSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType == \"7000222\"\n| summarize Attempts = count(),\n            FirstSeen = min(TimeGenerated),\n            LastSeen = max(TimeGenerated),\n            KeyIds = make_set(ServicePrincipalCredentialKeyId, 10)\n          by AppId, ServicePrincipalName, IPAddress\n| sort by Attempts desc"
    },
    {
      "title": null,
      "leadIn": "Expired-secret attempts from IPs that never had a success for the same app in the last 30 days:",
      "query": "let knownAppIPs =\n    AADServicePrincipalSignInLogs\n    | where TimeGenerated > ago(30d)\n    | where ResultType == \"0\"\n    | distinct AppId, IPAddress;\nAADServicePrincipalSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType == \"7000222\"\n| join kind=leftanti knownAppIPs on AppId, IPAddress\n| summarize Attempts = count(), LastSeen = max(TimeGenerated) by AppId, ServicePrincipalName, IPAddress\n| sort by Attempts desc"
    }
  ],
  "kqlSectionText": "Which apps are failing, and from where:\n\n```kusto\nAADServicePrincipalSignInLogs\n| where TimeGenerated > ago(7d)\n| where ResultType == \"7000222\"\n| summarize Attempts = count(),\n            FirstSeen = min(TimeGenerated),\n            LastSeen = max(TimeGenerated),\n            KeyIds = make_set(ServicePrincipalCredentialKeyId, 10)\n          by AppId, ServicePrincipalName, IPAddress\n| sort by Attempts desc\n```\n\nExpired-secret attempts from IPs that never had a success for the same app in the last 30 days:\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(7d)\n| where ResultType == \"7000222\"\n| join kind=leftanti knownAppIPs on AppId, IPAddress\n| summarize Attempts = count(), LastSeen = max(TimeGenerated) by AppId, ServicePrincipalName, IPAddress\n| sort by Attempts desc\n```\n\nCheck two things in your own workspace before relying on these queries:\n\n- The Azure Monitor reference for `AADServicePrincipalSignInLogs` describes `ResultType` as *\"Success or Failure\"*, while the user sign-in tables document it as the numeric error code as a string. Look at a few failed rows and confirm they carry `\"7000222\"` and that successes carry `\"0\"`. If they don't, filter on `ResultSignature` instead.\n- Whether `ServicePrincipalCredentialKeyId` is filled in on an expired-secret failure isn't documented. If it is, it tells you exactly which secret is still in use, which makes cleanup easy.\n\nA secret used by a confidential client in a delegated (user) flow may surface in the user sign-in tables instead. That is our reading of how the flows work, not a documented statement. If the first query returns nothing but users report the error, search `SigninLogs` for the same `ResultType`.",
  "fix": {
    "short": "Developer: create a new credential on the app registration's Certificates and secrets page and update every system that uses it. Admin: find every IP still sending the expired secret, update those systems, then remove the expired secret. Move to a managed identity or certificate where you can.",
    "steps": "**For developers:** create a new credential on the app registration's **Certificates and secrets** page and update every system that uses it. Don't stop at the first system that works again.\n\n**For admins:**\n1. Use the first query to list every IP still sending the expired secret, and update or retire each of those systems.\n2. Remove the expired secret from the app registration once nothing uses it. Microsoft: *\"An unused credential on an application can result in a security breach.\"*\n3. Move away from secrets. Microsoft's order of preference is a managed identity first, then certificate credentials: *\"Don't use password credentials, also known as secrets.\"* Certificate credentials work *\"anywhere a client secret could be used\"*, including the client credentials flow.\n4. Use application management policies to limit secret lifetimes or block new secrets, so the next rotation is planned rather than an outage.\n5. If the second query returns an IP you don't own, treat every credential that was stored alongside that secret as exposed."
  },
  "labReproduced": null,
  "labSectionText": null,
  "labRuns": [],
  "verification": "documentation",
  "related": [
    "7000215",
    "700016",
    "500011",
    "70002",
    "700024",
    "5002730",
    "7000216",
    "7000218"
  ],
  "coveredBy": [],
  "sources": [
    {
      "title": "Microsoft Entra authentication and authorization error codes",
      "url": "https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes"
    },
    {
      "title": "Security best practices for application properties",
      "url": "https://learn.microsoft.com/en-us/entra/identity-platform/security-best-practices-for-app-registration"
    },
    {
      "title": "Microsoft identity platform application authentication certificate credentials",
      "url": "https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials"
    },
    {
      "title": "Conditional Access for workload identities",
      "url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/workload-identity"
    },
    {
      "title": "AADServicePrincipalSignInLogs table reference (Azure Monitor)",
      "url": "https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/aadserviceprincipalsigninlogs"
    }
  ],
  "published": null,
  "lastUpdated": "2026-10-24",
  "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, \"AADSTS7000222: The provided client secret keys are expired\", https://brambrinkmeier.com/entra/aadsts/7000222/ (CC BY 4.0)",
  "microsoftErrorLookup": "https://login.microsoftonline.com/error?code=7000222"
}
