ConsentFix, also called AuthCodeFix, targets the handoff of a post-login callback URL rather than the password. A user can sign in and complete MFA at the genuine identity provider, then give an external page an address containing an authorization code. Another party may obtain a resource token if it meets the redemption requirements.
The January 2026 evidence involves Azure CLI and Microsoft Entra sign-in logs. Separating the authorization request, browser callback, non-interactive sign-in and resource call makes the anomaly easier to investigate without treating every cross-location sign-in as authorization-code phishing.
An authorization code is not an access token
In the normal flow, the browser returns a short-lived code, the client redeems it at the token endpoint, and the client uses the resulting access token at a resource API. The code is an intermediate result. The token's audience, scopes, user privileges and resource-side checks still constrain access.
sequenceDiagram participant B as Browser participant I as Identity platform participant C as Client participant R as Resource API B->>I: Sign-in and authorization request I-->>B: Code in callback URI B->>C: Deliver authorization response C->>I: Code and binding parameters I-->>C: Access token C->>R: Request with token R-->>C: Permission-checked response
Redemption must satisfy client and redirect-URI constraints. With PKCE, the redeemer must also supply the corresponding code_verifier; confidential clients have client-authentication requirements. The recorded request does not show a code_challenge. That does not establish that all application flows lack binding.
PKCE protects a legitimate client's intercepted code from a redeemer that lacks the verifier. If an external party initiates the authorization request itself and holds that verifier, PKCE alone does not establish that the user is handing the result to the right application. Ask who initiated the request and who holds the binding material.
Locate the browser error in the flow

Interface illustration. The account, application and code are examples; these three screens do not necessarily appear in sequence.
Existing grants and application configuration may remove the need for another consent prompt. Selecting an account does not necessarily require a new password or MFA challenge, and login_hint is not an authentication result. A genuine sign-in page and a valid session do not establish that a later request to copy the address is trustworthy.
| Observation | Layer | Meaning for the authorization response |
|---|---|---|
AADSTS50011 |
Identity-platform redirect validation | The requested callback does not match the registered configuration |
DNS_PROBE_FINISHED_NXDOMAIN |
Callback-host DNS resolution | No HTTP response has been obtained from the destination service |
ERR_CONNECTION_REFUSED |
Connection to the local callback port | No connection to a callback receiver was established |
| HTTP 404 | An HTTP service that was reached | The service reports a missing path; this is not connection refusal |
The other application sample shows a DNS error after returning to https://aadrm.com/adminpowershell. The Azure CLI sample stops at a localhost connection error. An accepted redirect URI and a successfully loaded callback page are separate conditions. The address bar may still contain code when the error page appears.
Match the binding material across requests
These are the relevant Azure CLI request fields, with an illustrative account. They are a reading aid, not a complete authorization request.
The callback has the form http://localhost:1605/?code=<redacted>&session_state=<redacted>. Match the client_id, redirect_uri, code and applicable PKCE or client-authentication material against the redemption record. OAuth state, OIDC session_state, and the Entra log's SessionId have distinct meanings; similar names do not make them universally interchangeable.
The record also mixes an authorization endpoint without /v2.0 and a v2 token endpoint. Preserve complete endpoint versions during investigation instead of generalizing one successful exchange to arbitrary combinations. The resource suffix /.default is a Microsoft identity platform convention, not a wildcard granting every API permission.
These token-response fields are visible in the record. Scopes and credentials are omitted. The expires_in value of 4535 belongs to this reply, not a fixed product-wide lifetime.
Ask which timestamp produced the interval
The two Azure CLI records share the account, application and a nonempty session identifier, but their source addresses differ. The displayed locations are GR / Athens and DE / Frankfurt Am Main. IP geolocation is supporting context rather than ground truth.
| Source table | Displayed TimeGenerated, UTC |
Location |
|---|---|---|
SigninLogs |
2026-01-24T21:39:55.8257112Z | GR |
AADNonInteractiveUserSignInLogs |
2026-01-24T21:44:32.3584638Z | DE |
The timestamp difference is 276.5327526 seconds; counting whole-second boundaries gives 277. The displayed field is TimeGenerated, so this first establishes an interval between log timestamps, not a measured authorization-code holding time. Use CreatedDateTime for event correlation and retain TimeGenerated when checking ingestion delay.
A non-interactive sign-in in the same session can also be an ordinary token renewal. The table name and matching SessionId do not, by themselves, prove redemption of a copied code. Request evidence, browser activity and application context still matter.
Correlate event keys without deduplicating users
This candidate-detection query starts with the Azure CLI application from the sample. It joins on tenant, user object ID, application and nonempty session, requires an event-time interval of up to ten minutes in the correct order, and retains both event IDs.
let app_ids = dynamic(["04b07795-8ddb-461a-bbee-02f9e1bf7b46"]);
let lookback = 1d;
let window_seconds = 600.0;
let interactive = SigninLogs
| where TimeGenerated > ago(lookback)
| where IsInteractive == true and AppId in~ (app_ids)
| where tostring(ResultType) == "0"
| project TenantKey=tolower(AADTenantId), UserKey=tolower(UserId),
AppId=tolower(AppId), SessionId, InteractiveId=Id,
InteractiveTime=CreatedDateTime, InteractiveIP=IPAddress,
InteractiveCountry=Location, InteractiveAgent=UserAgent;
let noninteractive = AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(lookback)
| where AppId in~ (app_ids)
| where toint(parse_json(tostring(Status)).errorCode) == 0
| project TenantKey=tolower(AADTenantId), UserKey=tolower(UserId),
AppId=tolower(AppId), SessionId, NonInteractiveId=Id,
NonInteractiveTime=CreatedDateTime, NonInteractiveIP=IPAddress,
NonInteractiveCountry=Location, NonInteractiveAgent=UserAgent,
ResourceDisplayName, UniqueTokenIdentifier;
interactive
| where isnotempty(TenantKey) and isnotempty(UserKey) and isnotempty(SessionId)
| join kind=inner noninteractive on TenantKey, UserKey, AppId, SessionId
| extend Seconds=(NonInteractiveTime - InteractiveTime) / 1s
| where Seconds between (0.0 .. window_seconds)
| where isnotempty(InteractiveIP) and isnotempty(NonInteractiveIP)
| where InteractiveIP != NonInteractiveIP
| extend CrossCountry=isnotempty(InteractiveCountry)
and isnotempty(NonInteractiveCountry)
and InteractiveCountry != NonInteractiveCountry
| project InteractiveId, NonInteractiveId, InteractiveTime, NonInteractiveTime,
Seconds, TenantKey, UserKey, AppId, SessionId, InteractiveIP,
NonInteractiveIP, CrossCountry, InteractiveCountry,
NonInteractiveCountry, InteractiveAgent, NonInteractiveAgent,
ResourceDisplayName, UniqueTokenIdentifierThe query has not been run in a real workspace. An offline predicate model passed 12 cases covering empty sessions, different tenants and users, failures, negative intervals, the ten-minute boundary and equal IP addresses. It also retains a one-second anomaly and both matches when two earlier events are candidates.
Four deployment details affect the result:
SigninLogs.ResultTypeuses string0for success; the other table's documentation describes Success / Failure. The non-interactive branch readsStatus.errorCoderather than assuming identical encodings.Statuscan be a dynamic object or a string across tables.parse_json(tostring(Status))normalizes access. Measure records with missing status codes separately instead of silently counting them as successful.AADTenantIdidentifies the identity tenant;TenantIdin these tables identifies the Log Analytics workspace. Keep that distinction in multi-tenant or cross-workspace investigations.join kind=innerpreserves candidate pairs. A default join that deduplicates left-side keys can quietly discard activity. Output rows are not a count of attacks either.
String comparison of source addresses is only a first pass. IPv4-mapped addresses, IPv6 formatting, VPNs and proxy egress may require normalization or review. CrossCountry adds context; it is not a mandatory detection condition.
Follow the session into resource activity
The two later resource-tracing records belong to Aadrm Admin Powershell, application ID 90f610bf-206d-4950-b61d-37fa6fd1b224, not Azure CLI. They are a separate example, not the established continuation of the preceding 277-second correlation.
In that example, the non-interactive sign-in's UniqueTokenIdentifier matches SignInActivityId in the Graph log. The observed request path is /v1.0/me. This establishes a profile-query record; its success and any broader data access require response status and further resource logs.
The following query uses a placeholder token identifier. An investigation would use the identifier extracted from the earlier event. Tenant and user constraints remain in the join, alongside request ID, method, URI and result.
let tokens = AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(1d)
| where UniqueTokenIdentifier == "TOKEN_ID_EXAMPLE"
| project TenantKey=tolower(AADTenantId), UserKey=tolower(UserId),
TokenKey=UniqueTokenIdentifier, SignInId=Id;
MicrosoftGraphActivityLogs
| where TimeGenerated > ago(1d)
| extend TenantKey=tolower(AadTenantId), UserKey=tolower(UserId),
TokenKey=SignInActivityId
| join kind=inner tokens on TenantKey, UserKey, TokenKey
| project TimeGenerated, SignInId, RequestId, RequestMethod,
RequestUri, ResponseStatusCode, TokenKeyGraph activity logs must already be routed to the relevant destination. No matching row may reflect collection, retention, field coverage or use of a non-Graph resource, rather than absence of access. Keep raw authorization codes and access tokens out of alert titles, tickets and shared screenshots.
Investigate the handoff, not whether MFA worked
MFA can succeed while the user gives away the authorization result. That neither establishes a universal Conditional Access failure nor means every first-party application has identical permissions. Check the particular application, resource, client type and policy evaluation.
Preserve sign-in and resource logs, establish the affected session, tokens and operations, then follow the organization's process for revoking affected sessions or grants and verifying subsequent activity. Configuration review should cover necessary application access, redirects and permissions, the actual PKCE initiator, and token-protection support for the relevant clients and resources.
The strongest combination is a genuine post-login callback, evidence of the user's handoff, cross-source activity in the same session, and resource requests linked to the token. A CLI sign-in or a distant IP alone is a reason to investigate, not a verdict.
References
- Microsoft: OAuth 2.0 authorization code flow
- RFC 7636: PKCE
- Microsoft: scopes and /.default
- Microsoft: AADSTS50011 redirect mismatch
- Microsoft: interactive sign-ins and timestamps
- Microsoft: SigninLogs schema
- Microsoft: non-interactive user sign-in schema
- Microsoft: linkable identifiers and Graph logs
- Microsoft: Graph activity logs
- Microsoft: token protection support