Skip to content

How to detect MFA fatigue (push bombing) in Microsoft Entra ID

MFA fatigue shows up in Entra ID sign-in logs as repeated 500121 or 50074 rows for one user, from an IP that user doesn’t normally use. Each row means the password was already accepted. The question that decides severity is whether a successful sign-in from the same IP followed the burst.

An attacker who already has a user’s password signs in again and again. Each attempt sends an MFA push to the user’s phone. The attacker is waiting for the user to approve one, by mistake or to make the prompts stop. MITRE ATT&CK tracks this as T1621, Multi-Factor Authentication Request Generation.

The precondition matters for detection. Every MFA prompt in the logs means the password stage succeeded. So an MFA fatigue investigation is also a compromised-password investigation, even if the user never approved anything.

Microsoft’s password spray playbook says the same thing about failed MFA in general: “Ensure you record successful passwords but failed MFA responses, as this activity indicates that the attacker is getting the password but not passing MFA.”

Code Microsoft’s description How it reads in a fatigue attack
500121 “User didn’t complete the MFA prompt.” The most direct signal. A denied or ignored push. Microsoft adds that it “often appears if the user hasn’t finished MFA setup”, so a single one is often benign.
50074 “Strong authentication is required and the user did not pass the MFA challenge.” MFA was required and not passed. The playbook lists it as “50074 - MFA required”. In normal sign-ins it can also appear just before a success, so count it per user rather than treating one row as failure.
50076 “…the user must use multifactor authentication to access the resource. Retry with a new authorize request for the resource.” MFA is required, often because of a policy or “because you moved to a new location”. A 50076 from an IP the user has never used is often the first row of an attack, before the pushes start.
50079 “…Either a managed user needs to register security info to complete multifactor authentication…” The user has no MFA method yet. Nothing to fatigue. Instead, the attacker is offered the chance to register their own method. See the registration section below.
50072 (sibling on the 50079 page) “User needs to enroll for second factor authentication (interactive).” Same reading as 50079.

The fatigue reading of 500121 and 50074 is mine, not Microsoft’s. Microsoft documents what each code means. It does not publish a rule for how many prompts make an attack.

Microsoft documents one more fact that explains why fatigue happens at all. On sign-in frequency it warns that “Triggering reauthentication too frequently can increase security friction to a point that it causes users to experience MFA fatigue and open the door to phishing.” A user who sees prompts all day is easier to push into approving one more.

How do you investigate MFA fatigue step by step?

Section titled “How do you investigate MFA fatigue step by step?”

The five queries below build on each other. They all use SigninLogs, because an MFA prompt is an interactive sign-in. ResultType is a string column, so compare it with "500121", not 500121.

The thresholds (3 prompts per hour, 30 days of history) are starting points I chose, not Microsoft numbers. Tune them against your own baseline.

Step 1: Which users had repeated MFA prompts fail?

Section titled “Step 1: Which users had repeated MFA prompts fail?”
SigninLogs
| where TimeGenerated > ago(1d)
| where ResultType in ("500121", "50074")
| summarize Prompts = count(),
Codes = make_set(ResultType, 5),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated),
Apps = make_set(AppDisplayName, 5),
UserAgents = make_set(UserAgent, 5)
by UserPrincipalName, IPAddress, bin(TimeGenerated, 1h)
| where Prompts >= 3
| sort by Prompts desc

A user stuck on a broken phone also produces this. The next step separates those from attacks.

Step 2: Did the prompts come from an IP the user has never signed in from?

Section titled “Step 2: Did the prompts come from an IP the user has never signed in from?”

The attacker signs in from their own infrastructure. A user’s genuine retries come from IPs that already appear in their successful sign-ins. This query keeps only bursts from IPs with no success for that user in the previous 30 days.

let lookback = 1d;
let history = 30d;
let bursts =
SigninLogs
| where TimeGenerated > ago(lookback)
| where ResultType in ("500121", "50074")
| summarize Prompts = count(),
FirstPrompt = min(TimeGenerated),
LastPrompt = max(TimeGenerated),
Country = take_any(tostring(LocationDetails.countryOrRegion))
by UserPrincipalName, IPAddress
| where Prompts >= 3;
let knownIPs =
SigninLogs
| where TimeGenerated between (ago(history) .. ago(lookback))
| where ResultType == "0"
| distinct UserPrincipalName, IPAddress;
bursts
| join kind=leftanti knownIPs on UserPrincipalName, IPAddress
| sort by Prompts desc

Every row left is a user whose password is in use from an unfamiliar IP. Treat each one as a compromised password, whatever happens in the next steps. Mobile users change IPs often, so expect some benign rows. Check Country and the user agent before escalating.

AuthenticationDetails holds one entry per authentication step: the method used and its result. It is a string column holding JSON, so parse it before expanding. Don’t use MfaDetail. Microsoft marks it as deprecated.

SigninLogs
| where TimeGenerated > ago(1d)
| where ResultType in ("500121", "50074")
| mv-expand Step = parse_json(AuthenticationDetails)
| extend Method = tostring(Step.authenticationMethod),
MethodDetail = tostring(Step.authenticationMethodDetail),
StepRequirement = tostring(Step.authenticationStepRequirement),
StepResult = tostring(Step.authenticationStepResultDetail),
Succeeded = tobool(Step.succeeded)
| summarize Rows = count(), Users = dcount(UserPrincipalName)
by ResultType, Method, StepResult, Succeeded
| sort by Rows desc

Verify before relying on this. The field names above follow the Microsoft Graph authenticationDetail object, which is my source for them, not a fetched schema page. Check one real row in your workspace and fix the names if they differ.

What to read in the output:

  • Method tells you whether the pushes went to the Authenticator app or another method. Fatigue needs a method the user can approve with one tap.
  • StepResult carries Microsoft’s text for the outcome. Expect separate values for “denied” and “did not respond”. The exact wording is not something I can quote from Microsoft documentation, so take it from your own output.
  • A user who denied the prompts noticed something. A user who ignored them may not have. Both need the same response, but the denying user is your best witness.

Step 4: Did the user approve a prompt in the end?

Section titled “Step 4: Did the user approve a prompt in the end?”

This is the step that decides severity. Look for a successful sign-in for the same user during the burst or in the hour after it.

let lookback = 1d;
let bursts =
SigninLogs
| where TimeGenerated > ago(lookback)
| where ResultType in ("500121", "50074")
| summarize Prompts = count(),
FirstPrompt = min(TimeGenerated),
LastPrompt = max(TimeGenerated)
by UserPrincipalName, BurstIP = IPAddress
| where Prompts >= 3;
SigninLogs
| where TimeGenerated > ago(lookback)
| where ResultType in ("0", "50140")
| join kind=inner bursts on UserPrincipalName
| where TimeGenerated between (FirstPrompt .. (LastPrompt + 1h))
| extend SameIPAsBurst = IPAddress == BurstIP
| project TimeGenerated, UserPrincipalName, IPAddress, BurstIP, SameIPAsBurst, ResultType,
Prompts, AppDisplayName, UserAgent
| sort by TimeGenerated asc

How to read it:

  • SameIPAsBurst is true: a sign-in from the attacker’s IP completed. Treat this as account takeover.
  • SameIPAsBurst is false: the user signed in from their own location during the burst. On its own that is normal. It does not clear the burst IP.
  • 50140 is the “Keep me signed in” prompt. Microsoft calls it “an expected part of the sign in flow”. It is the last prompt in the flow, so a 50140 from the burst IP almost certainly means MFA was passed. That is my reading, not Microsoft’s statement.

If Step 4 finds a takeover, build a full timeline for that user across both sign-in tables. The password spray guide has that query (Step 3 there).

Step 5: Did the attacker register their own MFA method?

Section titled “Step 5: Did the attacker register their own MFA method?”

There are two routes to this.

The first is an account with no MFA method yet. The attacker enters the password and gets 50079 or 50072 instead of a push. Nothing to fatigue: the sign-in is inviting them to register security info. This is reasoning from the code’s meaning, but it is the most dangerous row in this guide. The 50079 page has a query that finds 50079 from spraying IPs.

The second is after an approved prompt. An attacker with a live session may add a method so they no longer need the user to approve anything. That is also reasoning, not a Microsoft statement.

Both leave a trace in AuditLogs. This query lists security info changes made after a burst started, for every user who had one:

let bursts =
SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType in ("500121", "50074")
| summarize Prompts = count(), FirstPrompt = min(TimeGenerated)
by UserPrincipalName = tolower(UserPrincipalName), BurstIP = IPAddress
| where Prompts >= 3;
AuditLogs
| where TimeGenerated > ago(7d)
| where OperationName has "security info"
| extend UserPrincipalName = tolower(tostring(InitiatedBy.user.userPrincipalName)),
AuditIP = tostring(InitiatedBy.user.ipAddress)
| join kind=inner bursts on UserPrincipalName
| where TimeGenerated > FirstPrompt
| project RegisteredAt = TimeGenerated, UserPrincipalName, OperationName, Result,
AuditIP, BurstIP, Prompts, FirstPrompt
| sort by RegisteredAt desc

A row where AuditIP equals BurstIP is the attacker registering a method. Check the exact OperationName values for security info registration in your own workspace before relying on the has "security info" filter. Also check that InitiatedBy carries the user and IP for those events.

For every user from Step 2, whether or not they approved:

  1. Reset the password. The attacker has it. Every prompt in Step 1 proves that. If Step 5 found a method the attacker registered, remove it in the same change (see 4), otherwise the attacker can sign in with the new password and satisfy MFA with their own method as soon as they learn it.
  2. Talk to the user. Ask whether they approved any prompt they didn’t start. Tell them to deny and report any further prompts.

If Step 4 or Step 5 found something:

  1. Revoke sessions. In the Entra admin center, open the user and select Revoke sessions, or run:

    Terminal window
    Revoke-MgUserSignInSession -UserId $User.Id

    The Graph documentation says this “Invalidates all the refresh tokens issued to applications for a user (and session cookies in a user’s browser)”, with “a small delay of a few minutes” possible. Microsoft’s revoke-access procedure says that after its steps (disable, reset, revoke, disable devices) “the user can’t gain new tokens for any application tied to Microsoft Entra ID.” Existing access tokens are not withdrawn: “By default, access tokens issued by Microsoft Entra ID last for 1 hour.” Continuous Access Evaluation enforces an admin revocation on CAE-capable clients and services (Exchange Online, SharePoint Online, Teams) faster, with “latency of up to 15 minutes” per Microsoft. The session revocation guide covers this and how to prove the cut-off.

  2. Remove any MFA method the attacker registered. Use the Step 5 output to find it. Review the user’s authentication methods in the admin center and delete anything the user doesn’t recognise. Do this together with the password reset (step 1), not after it.

  3. Confirm the cut-off. After a reset or revocation, clients holding the old refresh token fail with 50173. Microsoft lists the causes as “The user changes or resets their password. The refresh token expires. An administrator revokes the refresh token.” 50173 from the burst IP in AADNonInteractiveUserSignInLogs means the attacker’s session was cut. A new 0 from that IP means they got back in. The 50173 page has the query.

  4. Review what the session accessed. Use the apps and resources from the user’s timeline.

These are general hardening measures. Apart from the quoted Microsoft text and the 53010 code, they are based on general practice rather than the Microsoft pages cited on this page. Check the current Microsoft documentation for each before changing policy.

  • Number matching. The user types a number shown on the sign-in screen into the Authenticator app instead of tapping Approve. A user who did not start the sign-in cannot see that number, so a blind approval no longer works. Microsoft enforced number matching for all Microsoft Authenticator push notifications in May 2023 (verify the date on the current Microsoft page), so in most tenants a plain Approve tap no longer exists for Authenticator. That doesn’t end the attack: it does not stop an attacker who phones the user and reads the number out, and it doesn’t cover third-party or NPS-extension MFA paths that still use a plain approve. Check your Authentication methods policy and any non-Authenticator MFA integration.
  • Additional context in the prompt. Showing the app name and sign-in location in the Authenticator notification gives the user a reason to deny a prompt from another country.
  • Phishing-resistant methods. FIDO2 security keys, passkeys, Windows Hello for Business and certificate-based authentication don’t send a push that can be approved remotely. With Conditional Access authentication strengths you can require them, starting with admin accounts.
  • Fewer routine prompts. Microsoft’s own warning about frequent reauthentication applies. If users approve prompts all day, one more doesn’t stand out.
  • Control where MFA can be registered. A Conditional Access policy on registering security info, limited to trusted locations or devices, closes the 50079 route. When that policy blocks a registration, the code is 53010.
  • Block legacy authentication. It doesn’t stop push bombing, but it removes protocols that can use the password without MFA at all.
  • Ran Step 1 and listed users with 3 or more failed MFA prompts in an hour.
  • Ran Step 2 and kept bursts from IPs with no recent success for that user.
  • Treated every user from Step 2 as having a compromised password.
  • Checked AuthenticationDetails (Step 3) for method and result, after confirming the field names in my own workspace.
  • Ran Step 4 and checked for a 0 or 50140 from the burst IP.
  • Ran Step 5 and checked AuditLogs for security info registered from the burst IP.
  • Checked for 50079 or 50072 from unfamiliar IPs on accounts without MFA.
  • Reset passwords, removed unknown methods and revoked sessions where needed.
  • Confirmed 50173, or no further attacker activity, after revocation.
  • Reviewed number matching, phishing-resistant methods and registration controls.

500121 · 50074 · 50076 · 50079 · 50126 · 53003 · 50140 · 50173

Sources

  1. How to troubleshoot sign-in errors
  2. Microsoft Entra authentication and authorization error codes
  3. Password spray investigation (Microsoft incident response playbook)
  4. Sign-in log activity details
  5. Conditional Access: session lifetime
  6. Revoke user access in Microsoft Entra ID
  7. Troubleshoot AADSTS50173: grant expired or revoked
  8. Microsoft Graph signIn resource
  9. Microsoft Graph: user: revokeSignInSessions
  10. Continuous access evaluation
  11. SigninLogs table reference (Azure Monitor)