Table of Contents
Per-User MFA State Available for User Accounts Through the Graph
On June 10, 2024, the Microsoft Graph changelog included some interesting additions to the beta version of the authentication resource type to make the settings for per-user MFA retrievable for user accounts. Until now, it’s been possible to see this information through the Entra admin center but not to fetch it programmatically.
The addition of the per-user MFA state is interesting because Microsoft is doing its level best to eliminate per-user MFA from Entra ID. Today, Office 365 E3 and above licenses include the ability to use per-user MFA when connecting to Office 365 services. Per-user Entra ID MFA covers all connections processed by the Microsoft identity service.
Microsoft’s long-term plan for enforcement of multifactor authentication is to use conditional access policies. They’ve put enormous effort over the past few years to build out the capabilities of these policies. The latest update was the ability to block connections using the device code authentication flow, something that all tenants should consider unless a solid business need exists to support device code authentication.
Moving Away from Per-User MFA
To make the transition easier for tenants, some Microsoft-managed conditional access policies are available to organizations with Entra ID P1 or P2 licenses, including one to assist the migration of per-user MFA. A potential issue for those with Office 365 MFA is that moving to conditional access policies requires Entra ID P1 licenses. This isn’t a problem if the organization has purchased Entra ID P1 for other reasons, like self-service password reset, but it is a hurdle to overcome for others. Security defaults is another option for tenants who don’t want to use conditional access policies, especially in the small to medium-sized sectors.
Knowing who still uses per-user MFA is invaluable information for anyone planning to migrate. It’s possible to get details about per-user MFA through the Users section of the Entra admin center, but the user interface is antiquated and unwieldy and the list includes both member and guest accounts (Figure 1).
Being able to extract the information via the Graph allows us to do something like this to find licensed member accounts, check each account for its per-user MFA state, and report the findings. The new capability is in beta for now with no indication of when the V1.0 (production) will support it. Reading the per-user MFA state requires consent to use the Policy.ReadWrite.AuthenticationMethod application permission.
Connect-MgGraph -Scope Policy.ReadWrite.AuthenticationMethod, User.Read.All -NoWelcome # Get licensed users [array]$Users = Get-MgUser -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member'" -ConsistencyLevel eventual -CountVariable UsersFound -All -PageSize 999 If ($Users) { Write-Host ("{0} users found" -f $Users.Count) } Else { Write-Host "No users found" Break } $Report = [System.Collections.Generic.List[Object]]::new() Foreach ($User in $Users){ $Uri = ("https://graph.microsoft.com/beta/users/{0}/authentication/requirements" -f $user.id) $Data = Invoke-MgGraphRequest -Uri $Uri -Method Get $ReportLine = [PSCustomObject][ordered]@{ User = $User.UserPrincipalName Name = $User.displayName "MFA State" = $Data.PerUserMfaState } $Report.Add($ReportLine) } $Report | Export-CSV -Path "C:\Temp\MFAState.csv" -NoTypeInformation -Encoding utf8
Accounts can be in one of three states for per-user MFA: disabled, enabled, or enforced. To update the per-user MFA state for an account, use the Patch method:
$Body= @{} $Body.Add("perUserMFAState", "enabled") $Uri = ("https://graph.microsoft.com/beta/users/{0}/authentication/requirements" -f $user.id) Invoke-MgGraphRequest -Uri $Uri -Method Patch -Body $Body
Enhancing the User Passwords and MFA Report
Being able to generate a quick report of per-user MFA states is nice; integrating that data with other sources to create a comprehensive view of account password and MFA properties is even better. In January, I wrote about the inability to query the Graph to find which accounts use MFA. This is because when you use conditional access policies, MFA is the outcome of an assessment against policy for inbound connections rather than a fixed property of user accounts (like per-user MFA). The script I described in the article therefore uses information from several sources, including Entra ID sign-in logs, to report registration of MFA methods, and password change information. The report shows the last time when accounts successfully used MFA to connect, which is the acid test to know if an account uses MFA or not. User registration of MFA methods is one step along the path; using those methods when connecting to Entra ID is what we want to see.
Now that per-user MFA state information is available, I have updated the script (available from GitHub) to include that data. The HTML report generated by the script highlights accounts enabled or enforced for per-user MFA (Figure 2).
The end of the report includes a summary of the findings (Figure 3), including the number of accounts in the enabled or enforced per-user MFA states and the display names of the users in those categories.
The report also generates a CSV file for you to slice and dice the data as you wish.
Nice Addition to Entra ID Data
Being able to report per-user MFA states is a nice addition to the data available to Entra ID administrators. Whether it will convince organizations currently using per-user MFA to move to conditional access policies remains to be seen.
Support the work of the Office 365 for IT Pros team by subscribing to the Office 365 for IT Pros eBook. Your support pays for the time we need to track, analyze, and document the changing world of Microsoft 365 and Office 365.
Woohoo!! Thanks for highlighting this, Tony! I’ve been having to painfully and slowly retrieve per-user MFAStatus via a PHP call to a Windows server running PowerShell (!) for the last couple of years, but this finally properly exposes MFAStatus through the Graph API. I would have had no idea that MS finally got it together and released this new method without your post.
Now if they’ll just let me *set* the Per-User MFA Status through the API, I can shut down that Windows PowerShell kludge permanently.
You can set it already, the endpoint supports PATCH requests as well (you’d need Policy.ReadWrite.AuthenticationMethod scope).
Thanks Vasil… I updated the text to show how to update the per-user MFA state for an account.
Thanks Vasil, that’s awesome! I noticed that you mentioned that the “Policy.ReadWrite.AuthenticationMethod” was required for the PATCH request, but I found that it was required for the GET request as well. With only the previous permissions that I had in place for other operations (UserAuthenticationMethod.Read.All and User.Read.All), I was getting Access Denied errors. Unlike the UserAuthenticationMethod and User domains, which have both .Read and .ReadWrite permissions, I didn’t see separate .Read permission for the Policy domain, so it seems like you must have the Policy.ReadWrite.AuthenticationMethod permission even if you only intend to use GET.
I wondered why the permissions structure was different for this operation, based inside the “Policy” domain rather than the existing “UserAuthenticationMethod” domain. Is Microsoft changing how these permissions are structured?
There is only a https://graphpermissions.merill.net/permission/Policy.ReadWrite.AuthenticationMethod Policy.ReadWrite.AuthenticationMethod permission. There isn’t one restricted to read.
Hey, Tony!
I couldn’t use the “Update” method through partner permission, do you know why?
https://learn.microsoft.com/en-us/graph/api/authentication-update?view=graph-rest-beta&tabs=powershell#example-2-update-a-users-mfa-state
I receive this error bellow:
Invoke-WebRequest -Uri “https://graph.microsoft.com/beta/users/$($MgUser.Id)/authentication/requirements” -Headers $authHeader -Method Patch -Body $Body -UseBasicParsing
Invoke-WebRequest : The remote server returned an error: (403) Forbidden.
At line:22 char:13
+ $Response = Invoke-WebRequest -Uri “https://graph.microsoft.com/beta/ …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand
403 Forbidden means that your session doesn’t have the right permission to execute the command, Do yuu have consent for Policy.ReadWrite.AuthenticationMethod?
Hello Tony,
Could you please tell where to add this permission ?
Run Connect-MgGraph with the scopes stated in the script and it will ask you to consent for any permissions it doesn’t already have. If you are signed in with an admin account, you can grant consent.
Connect-MgGraph -NoWelcome -Scopes AuditLog.Read.All, Directory.Read.All, UserAuthenticationMethod.Read.All, Policy.ReadWrite.AuthenticationMethod
That permission was missing. Thank you, Tony!
Hello Tony,
thanks for the script!
I’m trying to use it using PowerShell ISE.
The script executes with the following error at each user, and the report generates with no information in the ” Per user MFA state” column
Error is:
Invoke-MgGraphRequest : GET https://graph.microsoft.com/beta/users/831add8a-c129-47f7-860d-e7bc20425514/authentication/requirements
HTTP/1.1 403 Forbidden
Transfer-Encoding: chunked
Vary: Accept-Encoding
Strict-Transport-Security: max-age=31536000
request-id: 7a19ba1b-582c-4459-9b0d-675a567c5237
client-request-id: b22995e8-ed97-4296-b100-b4a786b6ec2e
x-ms-ags-diagnostic: {“ServerInfo”:{“DataCenter”:”Poland Central”,”Slice”:”E”,”Ring”:”2″,”ScaleUnit”:”001″,”RoleInstance”:”WA2PEPF00000385″}}
Date: Sat, 15 Jun 2024 11:16:11 GMT
Content-Encoding: gzip
Content-Type: application/json
{“error”:{“code”:”accessDenied”,”message”:”Request Authorization failed”,”innerError”:{“message”:”Request Authorization failed”,”date”:”2024-06-15T11:16:11″,”request-id”:”7a19ba1b-582c-4459-9b0d-675a567c5
237″,”client-request-id”:”b22995e8-ed97-4296-b100-b4a786b6ec2e”}}}
At line:104 char:13
+ $Data = Invoke-MgGraphRequest -Uri $Uri -Method Get
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (Method: GET, Re…b4a786b6ec2e
}:HttpRequestMessage) [Invoke-MgGraphRequest], HttpResponseException
+ FullyQualifiedErrorId : InvokeGraphHttpResponseException,Microsoft.Graph.PowerShell.Authentication.Cmdlets.InvokeMgGraphRequest
Am I missing any permission?
Could you help me verify which one and where to grant it ?
Thank you very much!
Reading the per-user MFA state requires consent to use the Policy.ReadWrite.AuthenticationMethod application permission. You don’t have the permission and that’s why you get a 403 Forbidden.
Dear Tony,
Could you add more information in the results for Authentication types, such as phone number, email address assigned in the user’s MFA ?
There’s an API available to report authentication methods configured for accounts. I have a script that illustrates how to use the API in https://office365itpros.com/2022/10/07/authentication-methods-scripts/. It would be easy to take the code from that and insert it into the MFA report script to output authentication methods.
I did some tinkering. Try V1.3 of the script (now available from GitHub). There’s now a PrivacyFlag parameter. If set to False (the default), you get full details of MFA authentication methods. If true, you get the output as seen in V1.2.