SharePointVault ~/gotchas/graph ⌕ Search 404 pages, error messages, scripts…Ctrl K ★ Star
M↓ README.mdM↓ per-user-mfa-state-lies-under-conditional-access.md
sharepoint-vault›gotchas›graph›per-user-mfa-state-lies-under-conditional-access.mdhistoryedit on GitHub
● GOTCHAgraph· 4 min read

perUserMfaState says "disabled" even when MFA is enforced

applies to: Microsoft Graph (beta) /users/{id}/authentication/requirements, Microsoft Graph v1.0 /reports/authenticationMethods/userRegistrationDetailsreviewed: 2026-07-31#graph#mfa#entra-id#conditional-access#security#reporting

Per-user MFA (perUserMfaState) is the legacy enforcement switch. When a tenant enforces MFA through Conditional Access or security defaults — which is the norm — every user stays disabled while being fully protected. Reading that field and rendering "no MFA" is a false statement, not a rounding error. To report on MFA, use /reports/authenticationMethods/userRegistrationDetails (registration is a fact regardless of enforcement method); use perUserMfaState only alongside the tenant's actual enforcement context.

Per-user MFA (perUserMfaState) je legacy přepínač vynucení. Když tenant vynucuje MFA přes Conditional Access nebo výchozí nastavení zabezpečení – tedy běžný stav – zůstávají všichni uživatelé disabled, přestože chránění jsou. Vzít tohle pole a napsat „uživatel nemá MFA“ není nepřesnost, ale nepravda. Pro reporting používej /reports/authenticationMethods/userRegistrationDetails (registrace platí bez ohledu na způsob vynucení); perUserMfaState ukazuj jen spolu s kontextem, čím tenant MFA reálně vynucuje.

Symptom

You build an "MFA status" column over the obvious-looking endpoint:

GET https://graph.microsoft.com/beta/users/{id}/authentication/requirements
→ { "perUserMfaState": "disabled" }

Every user in the tenant comes back disabled. The dashboard reports "0 % of users have MFA" — while the same users are demonstrably prompted for MFA at every sign-in, and the admin center shows the tenant as protected.

Nothing is broken. The API answered correctly; the question was wrong.

Cause

Entra ID has three independent ways to require MFA:

Mechanism Where it lives Effect on perUserMfaState
Conditional Access policy /identity/conditionalAccess/policies none — users stay disabled
Security defaults /policies/identitySecurityDefaultsEnforcementPolicy none — users stay disabled
Per-user MFA (legacy) /users/{id}/authentication/requirements this is the field itself

Microsoft states it plainly in the per-user MFA documentation: "Enabling Microsoft Entra multifactor authentication through a Conditional Access policy doesn't change the state of the user. Don't be alarmed if users appear disabled."

So perUserMfaState answers only "is this user enrolled in the legacy per-user mechanism", which for most modern tenants is "no, and that's fine". It is not a measure of whether the user has MFA.

The same docs also warn against mixing the two: "Don't enable or enforce per-user Microsoft Entra multifactor authentication if you use Conditional Access policies." A UI that offers a per-user toggle without surfacing that context invites admins to break their own tenant.

Fix

For "does this user have MFA?" use the registration report (v1.0, delegated AuditLog.Read.All):

GET https://graph.microsoft.com/v1.0/reports/authenticationMethods/userRegistrationDetails
{
  "id": "86462606-…",
  "userPrincipalName": "alex@contoso.com",
  "isMfaRegistered": true,      // has a strong method registered
  "isMfaCapable": true,         // …and policy allows it for MFA
  "isAdmin": false,             // admin accounts: missing MFA matters most here
  "methodsRegistered": ["microsoftAuthenticatorPush", "softwareOneTimePasscode"]
}

Registration is independent of how the tenant enforces MFA, so it is the one number that holds in every tenant.

If you do show the per-user state, show the enforcement context next to it. Two cheap reads (delegated Policy.Read.All) answer "what actually enforces MFA here":

GET /policies/identitySecurityDefaultsEnforcementPolicy   → { "isEnabled": true }
GET /identity/conditionalAccess/policies                  → filter state == "enabled"
                                                            && grantControls.builtInControls contains "mfa"

Treat each of those as three-valued: true / false / couldn't read it. Collapsing "couldn't read" into false reintroduces the same lie one level up.

Watch out for

  • 🔴 The registration report needs an Entra ID P1/P2 tenant — and the docs don't say so. On a tenant without a premium license the call fails with:

    HTTP 403 · Authentication_RequestFromNonPremiumTenantOrB2CTenant:
    Tenant is not a B2C tenant and doesn't have premium license

    The endpoint's permissions table lists only the scope and the supported directory roles, so this condition cannot be derived from the documentation — only from a live call. Handle it as its own case: telling an admin to "add the Reports Reader role" when the tenant simply has no P1 sends them somewhere that cannot help.

    The licence gate is per-endpoint, not per-feature — verify each call separately. On the same Entra ID Free tenant where the report returns 403, the beta per-user endpoint works fine: GET/PATCH /users/{id}/authentication/requirements both succeeded and the change took effect (verified live 2026-07-31). So "no P1" means you cannot see which methods a user registered, not you cannot manage MFA. Don't disable a whole feature because one of its reads hit a licence wall.

  • The registration report omits disabled (blocked) accounts. The docs note: "This method doesn't work for disabled users." A missing row therefore means no data, never no MFA. Render it as "not stated".

  • Reading the report needs a directory role, not just the scope: Reports Reader, Security Reader, Security Administrator, or Global Reader. A tenant admin's app registration having AuditLog.Read.All is not enough if the signed-in user holds none of those roles — you get 403. Fail loudly with the actual Graph error rather than rendering an empty (i.e. "nobody has MFA") table.

  • Writing perUserMfaState is beta-only. There is no v1.0 equivalent; Microsoft's standard warning applies ("Use of these APIs in production applications is not supported"). Writing needs delegated Policy.ReadWrite.AuthenticationMethod plus the Authentication Policy Administrator role (least privileged supported).

  • isMfaRegistered vs isMfaCapable are not the same: a user can have a method registered that the authentication methods policy no longer permits for MFA. Reporting only isMfaRegistered overstates coverage.

See also

Was this helpful?ask or add your experience on GitHub →
Saved you an hour? A star on GitHub helps other SharePoint people find the vault.★ Star on GitHub
last reviewed: 2026-07-31last updated: 2026-09-24historyedit on GitHubview source