# AADSTS90023: Entra rejected the request as invalid (fix)

Canonical: https://brambrinkmeier.com/entra/aadsts/90023/
By Bram Brinkmeier. JSON: https://brambrinkmeier.com/api/aadsts/90023.json

**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`.

| Field | Value |
|---|---|
| Code | AADSTS90023 |
| Microsoft name | InvalidRequest |
| Category | Credentials and lockout |
| Type | Failure |
| Attack signal | Low: usually configuration or user error |
| Where to look | `AADServicePrincipalSignInLogs` |
| In Microsoft's error reference | Yes |
| Verification | lab-reproduced (2026-10-30) |
| Last updated | 2026-10-31 |

## What does AADSTS90023 mean?

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.

### Cause 1: unexpected client assertion type
> 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](https://brambrinkmeier.com/entra/aadsts/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](https://brambrinkmeier.com/entra/aadsts/7000216/) | Nothing was sent at all on a `client_credentials` request. |

### 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](https://learn.microsoft.com/entra/identity-platform/v2-oauth2-device-code) 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](https://brambrinkmeier.com/entra/aadsts/70016/) | Right `device_code` and `client_id`, but nobody has signed in yet. Normal; keep polling. |
| [7000014](https://brambrinkmeier.com/entra/aadsts/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](https://brambrinkmeier.com/entra/aadsts/70016/) for a code that was still outstanding. That ordering comes from one run.

## Reproduced in the lab

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

### 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](https://brambrinkmeier.com/entra/aadsts/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](https://brambrinkmeier.com/methodology/).

### 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.

## Is it an attack?

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](https://brambrinkmeier.com/entra/aadsts/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](https://brambrinkmeier.com/entra/aadsts/70016/) and the app's own device code traffic is worth reviewing. |

## How do I find it in my logs (KQL)?

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:

```kusto
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:

```kusto
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:

```kusto
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:

```kusto
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.

## How do I fix AADSTS90023?

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](https://brambrinkmeier.com/entra/aadsts/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](https://brambrinkmeier.com/entra/aadsts/50059/) or [90133](https://brambrinkmeier.com/entra/aadsts/90133/) rather than 90023.
5. Once both requests agree, expect [70016](https://brambrinkmeier.com/entra/aadsts/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](https://brambrinkmeier.com/entra/aadsts/70016/) for the inventory query and the Conditional Access control.

## Evidence

Reproduced in a lab tenant on 2026-10-30 (Client-credentials token request against a lab tenant; scenario: Unknown client_assertion_type). First line returned:

> AADSTS90023: Unexpected client assertion type.

Lab-run rows naming this code (9; full list at https://brambrinkmeier.com/lab-runs.json):

| Date | Scenario | Expected | Got | Message (first line) |
|---|---|---|---|---|
| 2026-10-30 | Unknown client_assertion_type | 9002313 / 700027 | 90023 | AADSTS90023: Unexpected client assertion type. |
| 2026-10-30 | Unknown client_assertion_type | 9002313 / 700027 | 90023 | AADSTS90023: Unexpected client assertion type. |
| 2026-10-30 | Unknown client_assertion_type | 9002313 / 700027 | 90023 | AADSTS90023: Unexpected client assertion type. |
| 2026-10-30 | Unknown client_assertion_type | 9002313 / 700027 | 90023 | AADSTS90023: Unexpected client assertion type. |
| 2026-10-30 | Unknown client_assertion_type | 9002313 / 700027 | 90023 | AADSTS90023: Unexpected client assertion type. |
| 2026-10-31 | Unknown client_assertion_type | 9002313 / 700027 | 90023 | AADSTS90023: Unexpected client assertion type. |
| 2026-10-31 | Device code redeemed with a different client ID | 70016 / 7000215 / 70002 | 90023 | AADSTS90023: ClientId passed in the request doesn't match the one in cache |
| 2026-10-31 | Unknown client_assertion_type | 9002313 / 700027 | 90023 | AADSTS90023: Unexpected client assertion type. |
| 2026-10-31 | Device code redeemed with a different client ID | 70016 / 7000215 / 70002 | 90023 | AADSTS90023: ClientId passed in the request doesn't match the one in cache |

## Sources

1. [Microsoft Entra authentication and authorization error codes](https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes)
2. [Microsoft identity platform application authentication certificate credentials](https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials)
3. [Microsoft identity platform and the OAuth 2.0 client credentials flow](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow)
4. [AADServicePrincipalSignInLogs table reference (Azure Monitor)](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/aadserviceprincipalsigninlogs)
5. [Microsoft identity platform and the OAuth 2.0 device authorization grant flow](https://learn.microsoft.com/entra/identity-platform/v2-oauth2-device-code)

## Related codes

[AADSTS50027](https://brambrinkmeier.com/entra/aadsts/50027/), [AADSTS700024](https://brambrinkmeier.com/entra/aadsts/700024/), [AADSTS7000216](https://brambrinkmeier.com/entra/aadsts/7000216/), [AADSTS7000218](https://brambrinkmeier.com/entra/aadsts/7000218/), [AADSTS70016](https://brambrinkmeier.com/entra/aadsts/70016/), [AADSTS7000014](https://brambrinkmeier.com/entra/aadsts/7000014/), [AADSTS50013](https://brambrinkmeier.com/entra/aadsts/50013/), [AADSTS90014](https://brambrinkmeier.com/entra/aadsts/90014/), [AADSTS900561](https://brambrinkmeier.com/entra/aadsts/900561/)

## License and citation

CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). 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.

Cite as: Bram Brinkmeier, "AADSTS90023: Entra rejected the request as invalid (fix)", https://brambrinkmeier.com/entra/aadsts/90023/ (CC BY 4.0), last updated 2026-10-31.
