Your users are getting better at spotting phishing. It does not matter anymore. The phishing kits attackers use today - Evilginx-style reverse proxies, sold as a service, wrapped around AI-generated pretexting that reads like it came from your actual finance director - do not ask the user to hand over a password in a form that gets typed into a spreadsheet somewhere. They sit between the user and the real Microsoft sign-in page, relay every keystroke and every one-time code in real time, and walk away with the live session token five seconds after the user finishes typing.
That token is not a password. Multifactor authentication (MFA) does not stop it, because MFA already ran - the user completed it, on the real Microsoft page, relayed through the attacker's proxy without either side needing to fake anything. The only thing that stops this is the shape of the authentication method itself: whether it can be relayed through a proxy at all. That is a Conditional Access configuration question, not a user-training question, and most tenants have never actually checked the answer.
Adversary-in-the-middle (AiTM) phishing kits relay a real sign-in session, including whatever MFA prompt the user completes, so any Conditional Access policy that only requires mfa or a mixed authentication strength (password+SMS, password+push, and so on) can be defeated by a convincing enough lure - and AI-generated pretexting makes the lure far more convincing. Only phishing-resistant methods - Windows Hello for Business, FIDO2 security keys, and Entra certificate-based authentication - are cryptographically bound to the real origin and cannot be relayed this way. Entra ID's built-in Phishing-resistant MFA strength authentication strength enforces exactly that, but it has to be explicitly selected in each policy's grant control - the generic Require multifactor authentication control does not upgrade itself. This post shows how to query every Conditional Access policy in your tenant via Microsoft Graph, tell which ones actually require phishing-resistant authentication versus which ones only require "MFA (any method)," and close the gap.
The problem: "Require MFA" and "Require phishing-resistant MFA" are not the same grant control
Open the Conditional Access policy list in the Entra admin center and most tenants have at least a handful of policies with a grant control that just says Require multifactor authentication. That looks like a strong control. It has been the recommended baseline for years. The problem is what "multifactor authentication" is allowed to mean.
A user who satisfies "Require multifactor authentication" (the mfa built-in grant control) can do it with a text message code, a phone call, a Microsoft Authenticator push notification, a software OATH token, or a hardware OATH token, in combination with a password. Every single one of those can be entered into, or approved from within, a phishing page that is relaying the real authentication flow. The policy is satisfied. The sign-in succeeds. The session token that comes back belongs to the attacker's browser, not the user's.
Entra ID has had a separate, more specific control since 2022: Require authentication strength, with a built-in option called Phishing-resistant MFA strength. It only accepts three method combinations: Windows Hello for Business (or platform credential), a FIDO2 security key, and Entra certificate-based authentication used as a multifactor method. None of the three can be relayed through a reverse-proxy phishing kit, because the cryptographic handshake is bound to the actual origin the browser is talking to - not to whatever origin the phishing page presents.
mfa (not authenticationStrength), a user with WHfB registered can still complete sign-in with a push notification or an SMS code instead - and an AiTM kit will happily accept whichever one shows up.The gap this post audits is specific: for every Conditional Access policy that actually grants access to a population of users, does its grant control reference an authentication strength whose allowed combinations are exclusively phishing-resistant - or does it only require generic MFA, or a strength that still allows a relayable combination like password+SMS alongside FIDO2? Most tenants have never run that query across their whole policy set. They have looked at one or two "sensitive" policies and assumed the rest follow the same pattern.
Why it happens: how AiTM relay actually defeats SMS, push, and OTP-based MFA
An adversary-in-the-middle (AiTM) phishing kit is a reverse proxy. The user's browser talks to the attacker's server, thinking it is talking to login.microsoftonline.com. The attacker's server, in turn, opens its own connection to the real Microsoft sign-in endpoint and relays everything back and forth in both directions, live. Microsoft's own write-up on a large-scale campaign using this technique - which used the Evilginx2 toolkit and targeted more than 10,000 organizations starting in 2021 - describes the mechanism directly: once the attacker's proxy captures the session cookie issued after a successful sign-in, "the attacker can inject it into their browser to skip the authentication process, even if the target's MFA is enabled."
Read that sentence again: even if MFA is enabled. The proxy does not need to defeat MFA. It relays MFA. The user gets the real SMS code, the real push notification, the real "approve this sign-in" prompt - because it genuinely is the real Microsoft authentication flow, just with an eavesdropper sitting in the middle of it. What the attacker walks away with is not a password. It is a valid session token, issued by the real identity provider, for the real user, at the moment the user finished authenticating.
# 1. User clicks an AI-personalized phishing link in a convincing, context-aware lure User Browser -> Attacker Reverse Proxy -> login.microsoftonline.com # 2. User types their real password into what looks like the real sign-in page Password ................. relayed straight through ................. accepted # 3. Entra ID prompts for MFA. The proxy relays the prompt back to the user. SMS code / push / OTPaccepted # 4. Entra ID issues a real session token. The proxy captures it in transit. Session token stolen <- captured by attacker's proxy <- token issued # 5. Attacker replays the stolen token from their own browser. No password needed again. Attacker signs in as the user — MFA already satisfied, nothing left to prompt for.
The part that has changed in the last two years is the lure quality, not the relay mechanism. Storm-1167 and the wave of AiTM kits that followed it (Tycoon2FA and similar phishing-as-a-service platforms are current examples Microsoft has documented) used generic bait: fake voicemail notifications, fake document-sharing emails. What gets a user to click today is increasingly AI-generated: pretexting drafted by a language model that reads their public LinkedIn activity and recent company news, and in the more advanced helpdesk social-engineering cases, a voice-cloned or deepfake-assisted phone call impersonating an executive or an IT technician requesting an MFA reset. Microsoft's own security blog has continued to track this category of attack, including a 2026 breakdown of the Tycoon2FA AiTM phishing-as-a-service platform and a multi-stage 2026 campaign that used a fake "code of conduct" pretext to drive victims to an AiTM kit.
A more convincing lure does not need a smarter exploit. It just needs to survive human suspicion for one extra click. Once the click happens, the outcome depends entirely on whether the authentication method behind it can be relayed - which is a tenant configuration fact, not a training outcome.
How to verify: reading grantControls.authenticationStrength via Microsoft Graph
Every Conditional Access policy is a conditionalAccessPolicy object in Microsoft Graph. Its grant control section is a conditionalAccessGrantControls object with two properties that matter here: builtInControls (a plain array that can contain the string "mfa" among other values) and authenticationStrength, a nested authenticationStrengthPolicy object that is only populated when the policy uses the newer "Require authentication strength" control.
Step 1 - read every policy in the tenant. This single call returns the full policy set with grant controls and, where used, the authentication strength expanded inline:
Connect-MgGraph -Scopes "Policy.Read.All" $policies = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" $policies.value | Select-Object displayName, state, @{n='grant';e={$_.grantControls.builtInControls -join ','}}, @{n='strength';e={$_.grantControls.authenticationStrength.displayName}}
What good looks like: a policy targeting a sensitive app or an admin role shows strength = Phishing-resistant MFA strength (or a custom strength you built the same way). What a gap looks like: grant = mfa with an empty strength column - that policy accepts any registered MFA method, full stop.
The built-in authentication strengths are documented with their exact allowed combinations. Microsoft's own example response for a policy using the built-in "Multifactor authentication" strength (not the phishing-resistant one) shows the full combination list, and it is worth reading closely because it explains exactly why generic MFA is not enough:
This is the built-in Multifactor authentication strength itself, documented by Microsoft with the ID 00000000-0000-0000-0000-000000000002. It is a superset that includes the three phishing-resistant methods alongside the relayable ones. Selecting "Require authentication strength › Multifactor authentication" in the portal is barely different from selecting "Require multifactor authentication" directly - both accept the weakest method a user has registered. Only the Phishing-resistant MFA strength built-in policy, or a custom strength you scope down yourself, actually closes the gap.
A Conditional Access authentication strength requirement is a server-side control, but it is only as good as the client-side provisioning behind it. The registry state below (read-only via Get-ItemProperty, never written by this audit) tells you whether a device has actually finished Windows Hello for Business provisioning - the most common reason an otherwise-correct phishing-resistant policy still generates lockout tickets.
| Value name (type) | Healthy value | If wrong |
|---|---|---|
DeviceEnrollmentType (REG_DWORD) | 0x6 (MDM-enrolled) | Missing/other = device not enrolled; WHfB provisioning can't be trusted to have run under Intune policy |
NgcSet under SOFTWARE\Microsoft\Ngc\{DeviceKeyId} (key presence) | Key present with a valid container | Absent = WHfB container was never provisioned on this device - user can only fall back to a relayable method |
UsePassportForWork under SOFTWARE\Policies\Microsoft\PassportForWork (REG_DWORD) | 1 | 0/absent = WHfB policy not applied; device will keep offering password/OTP sign-in |
This registry state matters for the audit because it explains a failure mode you will see in the field: a Conditional Access policy correctly requires the Phishing-resistant MFA strength, but a subset of users still cannot sign in, because their device never finished WHfB provisioning and they have no FIDO2 key. That is not a Conditional Access bug - it is a rollout gap that the policy is correctly enforcing. Fix the provisioning, not the policy.
The fix: requiring the built-in Phishing-resistant MFA strength where it matters most
You do not need to require phishing-resistant authentication for every sign-in on day one. Start with the populations where a stolen token does the most damage: administrators, anyone with standing access to finance or payroll systems, and anyone who can approve a payment or a wire. Widen the scope as WHfB and FIDO2 rollout completes.
Step 1 - Sign in to the Entra admin center and go to Protection › Conditional Access › Policies.
Step 2 - Select an existing policy that currently uses Require multifactor authentication for a sensitive population (or create a new one scoped to Global Administrators, Privileged Role Administrators, and other Tier-0 roles).
Step 3 - Under Grant, select Grant access, then change the control from Require multifactor authentication to Require authentication strength.
Step 4 - Choose the built-in Phishing-resistant MFA strength from the dropdown. Do not build a custom strength that also allows password+SMS "just in case" - that reintroduces the exact gap you are closing.
Step 5 - Set the policy to Report-only first and review the sign-in logs for a week. Anyone who would have been blocked has not finished WHfB or FIDO2 registration - fix their registration before you flip the policy to On.
Step 6 - Turn the policy On once the report-only period shows zero would-be blocks for the target population.
Doing this policy-by-policy by hand does not scale past a handful of exceptions, and it tells you nothing about the other twenty policies you have not looked at yet. The companion script below runs the same classification logic across every enabled Conditional Access policy in the tenant in one pass.
# Interactive sign-in, no app registration required .\Get-PhishingResistantCAGapReport.ps1 -UseDeviceCode # Unattended, app-only auth for a scheduled task .\Get-PhishingResistantCAGapReport.ps1 -TenantId "{aaaaaaaa-0b0b-1c1c-2d2d-333333333333}" ` -ClientId "{bbbbbbbb-1c1c-2d2d-3e3e-444444444444}" ` -CertificateThumbprint "AAAABBBBCCCCDDDDEEEEFFFF00001111AAAABBBB" ` -ExportCsv -CsvPath "C:\Reports\ca-authstrength-gap.csv"
-UseDeviceCode the first time on your own workstation before wiring up an app registration. It only needs the Policy.Read.All scope and makes no changes, so it is safe to run against production immediately.Proof it worked: reading the audit script's output
The script classifies every enabled grant-control policy into one of four buckets and prints a summary. Here is an illustrative run against a small test tenant (fictional policy names and IDs, reconstructed to show the shape of real output - never run a broad audit like this against production and paste the raw result into a public post):
=== Phishing-Resistant Authentication Strength Audit === Policies evaluated (enabled): 7 -- Enforcing phishing-resistant authentication strength -- [OK] CA012: Require phishing-resistant MFA for Global Administrators ({aaaaaaaa-0b0b-1c1c-2d2d-333333333333}) Target: 4 targeted user/group/role entries -- Gaps: relying on generic MFA or a non-phishing-resistant strength -- [GenericMfaOnly] CA001: Require MFA for all users ({bbbbbbbb-1c1c-2d2d-3e3e-444444444444}) Target: All users builtInControls = mfa, no authenticationStrength set. Any registered MFA method satisfies this, including SMS, voice, and push - all relayable by an AiTM phishing proxy. [MixedStrength] CA007: Require strong auth for Finance app ({cccccccc-2d2d-3e3e-4f4f-555555555555}) Target: 1 targeted user/group/role entry Authentication strength 'Finance Custom Strength' also allows non-phishing-resistant combos: password,sms Summary: 1 phishing-resistant, 2 flagged out of 7 grant-enabled policies.
What good looks like: every policy that targets a sensitive population, and ideally the "all users, all apps" baseline policy too, shows up under the [OK] section with an authentication strength whose allowed combinations are all phishing-resistant. What needs fixing: anything under GenericMfaOnly or MixedStrength - those are the policies an AiTM kit can walk straight through today, regardless of how convincing the AI-generated lure that got the user there was.
The script exits with code 1 when it finds at least one flagged policy, so it slots directly into a scheduled task or pipeline step that should fail loudly rather than silently pass. Exit code 0 means every grant-enabled policy in scope requires phishing-resistant authentication. Exit code 2 means the script could not authenticate or could not read policies at all - treat that as "could not verify," never as "clean."
Download Get-PhishingResistantCAGapReport.ps1 from Imran76Awan/Daily-Tasks on GitHub — no sign-in required. It is read-only (report-only, Graph GET calls only) — validate in your own environment before relying on it.
References
- Overview of Conditional Access authentication strengths - Microsoft Entra ID
- authenticationStrengthPolicy resource type - Microsoft Graph
- conditionalAccessGrantControls resource type - Microsoft Graph
- List Conditional Access policies - Microsoft Graph
- Protecting tokens in Microsoft Entra ID
- Risk detection types and levels - Microsoft Entra ID Protection
- From cookie theft to BEC: attackers use AiTM phishing sites as entry point to further financial fraud - Microsoft Security Blog
- Inside Tycoon2FA: how a leading AiTM phishing kit operated at scale - Microsoft Security Blog
- Breaking the code: multi-stage phishing campaign leads to AiTM token compromise - Microsoft Security Blog