← Back to Blog

KQL Sign-In Log Analysis — What ResultType != 0 Actually Tells You

27 May 2026 Detection & Hunting 10 min read
SIGNINLOGS RESULTTYPE — WHAT EACH CATEGORY MEANS ResultType = 0 Successful sign-in 50xxx codes Authentication failures 53xxx codes CA blocks and compliance MFA & interrupt codes 50074, 50076, 500121 THE QUERY MOST PEOPLE WRITE: SigninLogs | where ResultType != 0 This returns everything that is not a clean success — but not every non-zero is a threat MFA prompts, CA blocks, and user interrupts are also non-zero — filtering matters

The single most common KQL query analysts paste into Google:

SigninLogs
| where ResultType != 0

It works. It returns failed sign-ins. But it also returns MFA prompts, conditional access blocks, user interrupts, and a dozen other non-zero result types that are not attack indicators. Without understanding what each ResultType means, you are staring at thousands of rows with no way to separate the signal from the noise.

Here is what the result codes actually mean and the queries that turn them into actionable detections.

The ResultType field

ResultType is a string field (not an integer — this trips up new KQL users who try numeric comparisons). A value of "0" means the sign-in completed successfully. Everything else is a non-zero code indicating what happened.

The codes fall into categories:

50xxx — authentication failures. These are the ones you care about for attack detection. 50126 is invalid credentials (wrong password). 50053 is account lockout. 50057 is a disabled account. 50055 is an expired password. When you see hundreds of 50126 events across multiple accounts from the same IP, that is a password spray.

53xxx — conditional access and compliance blocks. 53003 is the big one — access blocked by conditional access policy. This is not an attack. This is your policy working. 53000 is device compliance failure. These are operational signals, not threat signals.

MFA and interrupt codes. 50074 means MFA was required. 50076 means the user did not complete MFA. 500121 means MFA failed. These become interesting when 50076 appears repeatedly for one account followed by a successful 0 — that is the MFA fatigue pattern.

The queries that matter

Failed credentials — password spray detection

SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == "50126"
| summarize
    FailedAttempts = count(),
    TargetAccounts = dcount(UserPrincipalName),
    FirstAttempt = min(TimeGenerated),
    LastAttempt = max(TimeGenerated)
    by IPAddress
| where TargetAccounts > 5
| sort by TargetAccounts desc

This finds IPs that failed password authentication against more than 5 distinct accounts in the last 24 hours. That is the signature of a password spray — the attacker tries a small number of passwords against many accounts to stay below lockout thresholds.

The dcount(UserPrincipalName) is the key operator. A brute force attack hits one account many times. A password spray hits many accounts a few times each. The dcount separates them.

Account lockouts — who is locked and why

SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == "50053"
| summarize
    LockoutCount = count(),
    DistinctIPs = dcount(IPAddress),
    LastLockout = max(TimeGenerated)
    by UserPrincipalName
| sort by LockoutCount desc

A user locked out from one IP is probably typing the wrong password. A user locked out from multiple IPs — check DistinctIPs — might be under active attack.

MFA fatigue — repeated push denials followed by approval

SigninLogs
| where TimeGenerated > ago(24h)
| where UserPrincipalName == "target@yourdomain.com"
| where ResultType in ("50074", "50076", "500121", "0")
| project TimeGenerated, ResultType, IPAddress, DeviceDetail, Location = LocationDetails.city
| sort by TimeGenerated asc

Replace the UPN with the account you are investigating. What you are looking for: a sequence of 50076 (MFA not completed) or 500121 (MFA failed) from an unusual IP, followed by a 0 (success) from the same IP. That sequence — deny, deny, deny, approve — is the MFA fatigue attack pattern.

Conditional access blocks — is your policy working

SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == "53003"
| summarize
    BlockCount = count(),
    DistinctUsers = dcount(UserPrincipalName),
    DistinctApps = dcount(AppDisplayName)
    by ConditionalAccessPolicies
| sort by BlockCount desc

This is not a threat query — it is an operational query. If one conditional access policy is blocking hundreds of legitimate sign-ins, the policy needs tuning. If no policies are blocking anything, they might not be enforcing.

Interactive vs non-interactive — the table you are probably missing

The query SigninLogs | where ResultType != 0 only covers interactive sign-ins — the events where a user actively authenticated. Background token refreshes, silent SSO, and automated app authentication are in a separate table:

AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(24h)
| where ResultType != "0"
| summarize FailCount = count() by UserPrincipalName, AppDisplayName, ResultType
| sort by FailCount desc

If an attacker has a stolen refresh token, the evidence might only appear in the non-interactive table. The interactive sign-in was the initial compromise. Everything after that is token refresh — silent, automatic, and invisible if you only query SigninLogs. The full explanation of why both tables matter is in Interactive vs Non-Interactive Sign-Ins.

The result code reference

The codes you will see most often in a security investigation:

0 — Success. 50126 — Invalid credentials (wrong password). 50053 — Account locked out. 50057 — Disabled account attempted sign-in. 50055 — Expired password. 50074 — MFA required, user redirected to MFA. 50076 — User did not complete MFA (closed the prompt, timed out). 500121 — MFA authentication failed. 53003 — Access blocked by conditional access policy. 53000 — Device compliance failure. 50158 — External security challenge not satisfied. 65004 — User declined consent to app permissions.

The full reference is in Microsoft's Entra ID sign-in error codes documentation, but the codes above cover the scenarios you will encounter in 90% of investigations.

What to do this week

Pick one of the queries above and run it against your tenant. Start with the password spray detection query — it takes 30 seconds and either confirms you have no spray activity or reveals that you do. If you have never run it before, the answer might surprise you.

If you want to go deeper into sign-in log analysis — building behavioral baselines, cross-table correlation, and detection rules from the patterns you find — the first two modules of Entra ID Security are free. Module 1 covers the sign-in log architecture field by field, including the KQL fundamentals and advanced query patterns that build on everything in this post. The KQL course covers the query language itself from the tabular data model through production-grade security queries.

Next week: What detection engineering actually is — and why your SOC needs it before it needs more rules.

Ridgeline Cyber Defence Written by security practitioners. Published weekly on Tuesdays.

Get security ops insights weekly

One email every Tuesday. Detection techniques, investigation methods, and operational security. Unsubscribe anytime.

Ridgeline Training

Want to go deeper?

Hands-on courses covering Detection & Hunting with labs, deployable artifacts, and free foundation modules.

Microsoft Entra ID Security → Mastering KQL →