Skip to content

AADSTS90023: Entra rejected the request as invalid (fix)

Short answer: AADSTS90023 means Microsoft Entra ID rejected the token request as invalid. Microsoft's reference describes it only as InvalidRequest. The lab has returned it for two different causes with two different messages: a client_assertion_type that is not the JWT bearer URN, and a device code polled with a different client_id than the one that requested it. Read the message, not just the code.

Fix: Read the message: the code covers more than one cause. Developer, assertion case: send `client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer`, URL-encoded once, with `client_assertion`. Device code case: poll `/token` with the same `client_id` that called `/devicecode`. Full steps

At a glance
CodeAADSTS90023
Microsoft nameInvalidRequest
CategoryCredentials and lockout
TypeFailure
Attack signalLow: usually configuration or user error
Where to lookAADServicePrincipalSignInLogs
Microsoft lookuplogin.microsoftonline.com/error?code=90023

Microsoft’s description:

InvalidRequest: The authentication service request isn’t valid.

That is as much as the reference says, and 90023 is a bucket, not a single fault. It is the code Entra uses when it refuses to process a token request at all, and the lab has produced it from two unrelated causes on two different flows. The code is the same; only the message tells them apart:

Message returned Cause Flow
“Unexpected client assertion type.” client_assertion_type is not the JWT bearer URN Client credentials
“ClientId passed in the request doesn’t match the one in cache” A device code is polled with a different client_id than the one that requested it Device code

Both are request-validation failures: something about the shape or the consistency of the request is wrong, so Entra rejects it before doing any real authentication work. Expect other causes to exist that we have not seen. Always read ResultDescription, or the error_description returned to the caller, before acting on a 90023.

AADSTS90023: Unexpected client assertion type.

An app that authenticates with a certificate or a federated credential sends two parameters instead of a secret: the assertion itself, and a parameter saying what kind of assertion it is. There is exactly one accepted value. Microsoft’s certificate credentials page:

Parameter Value Microsoft’s description
client_assertion_type urn:ietf:params:oauth:client-assertion-type:jwt-bearer This is a fixed value, indicating that you’re using a certificate credential.
client_assertion JWT This is the JWT created above.

The client credentials flow page repeats it: “The value must be set to urn:ietf:params:oauth:client-assertion-type:jwt-bearer.” Anything else, including a shortened form, a URL-style variant or a double-encoded string, is “unexpected”.

Three codes sit close to this one, and the difference is how far the request got:

Code Meaning
90023 The assertion type is not the accepted URN. Entra didn’t get as far as reading the assertion.
50027 The type was accepted, but the assertion is not a well-formed JWT.
700027 “Client assertion failed signature validation” in Microsoft’s reference. A valid JWT, wrong signature.
7000216 Nothing was sent at all on a client_credentials request.

Cause 2: device code polled with a different client ID

Section titled “Cause 2: device code polled with a different client ID”

AADSTS90023: ClientId passed in the request doesn’t match the one in cache

The device code flow spans two requests. The app calls /devicecode with its client_id and gets a device_code back; it then polls /token with that device_code. Microsoft’s device authorization grant page requires the second request to carry the same client: the client_id on the token request “Must match the client_id used in the initial request.”

The message says what the server is doing. The device_code is a handle to state Entra is holding (“the one in cache”), and the client ID that created that state is part of it. Poll with a different client ID and the handle no longer matches its owner, so the request is invalid.

What this means in practice: the device_code is not a bearer token you can redeem from anywhere with anything. It is bound to the application that asked for it.

Code When you get it in the device code flow
90023 Right device_code, wrong client_id.
70016 Right device_code and client_id, but nobody has signed in yet. Normal; keep polling.
7000014 The device_code value itself isn’t recognised.

Our reading: this check happens before the “has the user approved yet?” check, because the lab got 90023 rather than 70016 for a code that was still outstanding. That ordering comes from one run.

Both causes were reproduced against a lab tenant. The frontmatter records the first one.

Unexpected client assertion type (30 October 2026)

Section titled “Unexpected client assertion type (30 October 2026)”

On 30 October 2026 an automated lab run sent a client-credentials token request with a valid client_assertion but a client_assertion_type that Entra does not accept. Entra returned:

AADSTS90023: Unexpected client assertion type.

Both rounds of the run returned the same code and the same one-line message, which is short enough that the lab’s 160-character cut does not truncate it. The run had expected either 9002313 or 700027, so this is a case where guessing the code from the documentation would have been wrong. In the same run, a client_assertion that was not a JWT at all, sent with the correct assertion type, returned 50027 instead, so the two checks run in that order: type first, then format.

No sign-in log row was captured. See how pages are checked.

Device code redeemed with a different client ID (31 October 2026)

Section titled “Device code redeemed with a different client ID (31 October 2026)”

On 31 October 2026 a later run requested a device code with one application’s client_id and then polled the token endpoint for that same device_code with a different client_id. Entra returned:

AADSTS90023: ClientId passed in the request doesn’t match the one in cache

That run had expected 70016, 7000215 or 70002. It got none of them, which is the second time this code has turned up where the documentation gave no reason to expect it. Note the message has no trailing full stop, unlike the assertion-type message, so match on a substring rather than the whole line.

No sign-in log row was captured for this case either, and the message mentions no application, user or tenant, so the attacker’s view of it discloses nothing.

Usually not, in either case. A hand-built token request, or a library being driven in a way it wasn’t designed for, is the normal cause. The accepted assertion type is long and easy to mistype or to encode twice, and the device code case needs two requests to disagree with each other.

Pattern Likely meaning
A new integration fails on every attempt The parameter is wrong in the code or the configuration. It has never worked.
Started after a library or SDK upgrade The request is now being assembled differently. Compare the parameters sent before and after.
Appears alongside 50027 and 700027 for one app Someone is iterating on a client assertion implementation. Benign, and usually the app’s own developer.
For one app ID from an address you don’t own Someone else is constructing requests for your app ID. Nothing is disclosed by this error, but check whether the same address produced a success.
The device code message, from a tool that worked yesterday Two code paths in the tool now use different client IDs, or a config change replaced one of them.
The device code message for an app you did not configure for device code flow Someone is trying a device_code against a client ID that did not request it. It fails, but device code flow is a phishing vector and the app’s own device code traffic is worth reviewing.

Which sign-in table 90023 lands in is undocumented, and the generic description means it can arrive from flows other than client assertions. Start wide and read the descriptions:

union withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs
| where TimeGenerated > ago(30d)
| where ResultType == "90023" or ResultSignature == "90023"
| summarize Rows = count(),
LastSeen = max(TimeGenerated),
Descriptions = make_set(ResultDescription, 10)
by SourceTable

ResultDescription is documented as “the error message or the reason for failure for the corresponding sign-in activity” in SigninLogs, and as “the error description for the sign-in operation” in AADServicePrincipalSignInLogs. The set of descriptions is what tells you which cause your 90023 rows are, and whether there is a third one we have not documented.

Split the two known causes apart. The device code case is a public client flow, so look for it in the user sign-in tables rather than the service principal one:

union withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs
| where TimeGenerated > ago(30d)
| where ResultType == "90023" or ResultSignature == "90023"
| extend Cause = case(ResultDescription has "assertion type", "client assertion type",
ResultDescription has "doesn't match the one in cache", "device code client ID mismatch",
"other or unknown")
| summarize Rows = count(), LastSeen = max(TimeGenerated), Apps = make_set(AppId, 10)
by SourceTable, Cause
| sort by Rows desc

Rows landing in other or unknown are the interesting ones: a cause of 90023 neither of the lab runs produced.

Narrow to the client assertion case per app:

AADServicePrincipalSignInLogs
| where TimeGenerated > ago(30d)
| where ResultType == "90023" or ResultSignature == "90023"
| where ResultDescription has "assertion type"
| summarize Failures = count(),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated),
CredentialTypes = make_set(ClientCredentialType, 5),
IPs = make_set(IPAddress, 10)
by AppId, ServicePrincipalName
| sort by Failures desc

The has "assertion type" filter assumes the message reaches the log. Confirm on one row before building an alert on it; if ResultDescription is empty in your workspace, drop that line and separate the cases another way.

Counting all three assertion errors together is often more useful than any one of them, because a developer working on a certificate credential produces a mix:

AADServicePrincipalSignInLogs
| where TimeGenerated > ago(7d)
| where ResultType in ("90023", "50027", "700027") or ResultSignature in ("90023", "50027", "700027")
| summarize Attempts = count(), LastSeen = max(TimeGenerated)
by AppId, ServicePrincipalName, ResultType, ResultSignature
| sort by Attempts desc

As a starting point, a burst over a few minutes for one app is a developer; a slow trickle for weeks is a job nobody has fixed.

Start by reading the message. The two known causes have nothing to do with each other.

For developers, “Unexpected client assertion type”:

  1. Set client_assertion_type to exactly urn:ietf:params:oauth:client-assertion-type:jwt-bearer. Nothing else is accepted.
  2. Check the encoding. The value is URL-encoded once as part of the form body. Encoding the colons twice produces a different string, and Entra sees an unexpected type.
  3. Send client_assertion in the same request. The type parameter alone is not a credential.
  4. Prefer a library over hand-built requests. MSAL sets both parameters for you when you configure a certificate credential.
  5. If the error changes to 50027, the type is now right and the assertion itself needs work: the header needs alg, typ and x5t#S256, and the claims need aud, iss, sub, jti, nbf and exp, as Microsoft’s certificate credentials page sets out.

For developers, “ClientId passed in the request doesn’t match the one in cache”:

  1. Use one client_id for the whole device code flow. The token request must carry the same client ID as the /devicecode request that produced the device_code.
  2. Check for two configurations in one process: a hard-coded default client ID in one code path and a configured one in the other is the usual way the two requests drift apart.
  3. Keep the device_code with the client ID that obtained it. Do not cache or pass device codes between applications; the binding is enforced.
  4. Keep the authority the same across both requests too. A tenant mismatch produces 50059 or 90133 rather than 90023.
  5. Once both requests agree, expect 70016 until the user signs in. That is the normal reply, not an error.

For admins:

  1. Nothing to change in the tenant. This is the shape of the request, not a setting on the app or a policy.
  2. Use the queries above to find which app and which owner, and pass them Microsoft’s certificate credentials page, or its device code page for the device code case.
  3. If the device code message appears for apps you do not expect to use the flow, review device code flow in the tenant: see 70016 for the inventory query and the Conditional Access control.

More: 50013, 90014, 900561

Sources

  1. Microsoft Entra authentication and authorization error codes
  2. Microsoft identity platform application authentication certificate credentials
  3. Microsoft identity platform and the OAuth 2.0 client credentials flow
  4. AADServicePrincipalSignInLogs table reference (Azure Monitor)
  5. Microsoft identity platform and the OAuth 2.0 device authorization grant flow