May 27 2020 07:50 AM - edited May 27 2020 07:51 AM
Is there any effective way to get a report of the actual MFA state of your users?
I mean, the individual MFA state as well as MFA enabled via Conditional Access.
It's easy to report on the individual MFA state. You get nice results: Enabled, Disabled, Enforced...
However, if MFA is enabled via Conditional Access I can't seem to find an effective way to report on them.
Below Powershell snippet is the closest I can get.
It will check if MFA is enabled individually. If not, it will check the "StrongAuthenticationMethods.IsDefault" attribute and report on that.
But this is not always accurate, because if the "Phone" or "Alternate Phone" are configured in the Azure user object, it will still report it here even if the user is not member of a Conditional Access policy.
There is a built-in Azure report for this, but it is completely incorrect. It says that, for instance, I'm not enabled for MFA even though I'm enabled for the last 6 years.
Report: https://portal.azure.com/#blade/Microsoft_AAD_IAM/AuthMethodsOverviewBlade
Has anyone figured this out yet?
$user = get-msoluser -UserPrincipalName yourUserName@contoso.com
$StrongAuthenticationMethodsresult = $user.StrongAuthenticationMethods | Select-Object MethodType, IsDefault
[PSCustomObject]@{
UserPrincipalName = $user.UserPrincipalName
ObjectID = $user.objectid
DisplayName = $user.DisplayName
AuthEmail = $user.StrongAuthenticationUserDetails.Email
AuthPhoneNumber = $user.StrongAuthenticationUserDetails.PhoneNumber
PhoneDeviceName = $user.StrongAuthenticationPhoneAppDetails.DeviceName
AuthAltPhone = $user.StrongAuthenticationUserDetails.AlternativePhoneNumber
State = if ($user.StrongAuthenticationRequirements.State -ne $null) { $user.StrongAuthenticationRequirements.State } elseif ( $user.StrongAuthenticationMethods.IsDefault -eq $true) { "ConditionalAccess ($(($user.StrongAuthenticationMethods| Where IsDefault -eq $True).MethodType))" } else { "Disabled" }
PhoneAppNotification = if ($StrongAuthenticationMethodsresult | Where-Object { $_.MethodType -eq "PhoneAppNotification" }) { $true } else { $false }
PhoneAppNotificationIsDefault = IF ( ($StrongAuthenticationMethodsresult | Where-Object { $_.MethodType -eq "PhoneAppNotification" }).isDefault -eq "True") { $true } Else { $false }
PhoneAppOTP = if ($StrongAuthenticationMethodsresult | Where-Object { $_.MethodType -eq "PhoneAppOTP" }) { $true } else { $false }
PhoneAppOTPIsDefault = IF ( ($StrongAuthenticationMethodsresult | Where-Object { $_.MethodType -eq "PhoneAppOTPIsDefault" }).isDefault -eq "True") { $true } Else { $false }
TwoWayVoiceMobile = if ($StrongAuthenticationMethodsresult | Where-Object { $_.MethodType -eq "TwoWayVoiceMobile" }) { $true } else { $false }
TwoWayVoiceMobileIsDefault = IF ( ($StrongAuthenticationMethodsresult | Where-Object { $_.MethodType -eq "TwoWayVoiceMobileIsDefault" }).isDefault -eq "True") { $true } Else { $false }
OneWaySMS = if ($StrongAuthenticationMethodsresult | Where-Object { $_.MethodType -eq "OneWaySMS" }) { $true } else { $false }
OneWaySMSIsDefault = IF ( ($StrongAuthenticationMethodsresult | Where-Object { $_.MethodType -eq "OneWaySMSIsDefault" }).isDefault -eq "True") { $true } Else { $false }
}
May 28 2020 02:41 AM - edited May 28 2020 02:42 AM
I've been using this script, it reports on users that are mfa enforced via CA policy and have a disabled mfa user state.
https://gallery.technet.microsoft.com/office/Export-Office-365-Users-81747c73
May 28 2020 06:31 AM
@n3vers
Thanks. I already came across that script.
It basically does the same as mine.
It's not accurate.
If there is a Conditional Access policy, but due to some conditions a particular account is not affacted by it and he has an Authentication Phone configured, the script (like mine) will report that MFA is enabled even though it's not enforced.
We have a couple of these accounts in our environment.
While everything is under control here, I wanted to have a reliable report where I can look at occasionally to identify such accounts if they, for some reason, slip through.
May 30 2021 04:38 PM
@NidalT
Did you ever find an accurate way to report on MFA as have just found our current reporting have the same issue.
May 30 2021 10:38 PM
Jul 07 2022 01:38 PM
Jul 07 2022 01:45 PM
@justJustinian
So far I've seen 2 methods used.
1. You can report on the MFA registration type, so if you have simple conditional access policies you may be able to assume coverage if they are registered.
2. I've seen some third party tools actually parse the login audit logs and report on any logins without MFA.
But no, nothing direct from MS.
Jul 08 2022 04:38 AM
The way I have resolved this is by creating a Dynamic Azure AD Group that adds all users eligible for MFA via Conditional Access (e.g. having the correct license assigned).
Then scoped the Conditional Access policy to that group.
Then, for the inventory, I have Powershell script using MsGraph that will chcek to see if any Authentication Method exists for all users and what method it is.
In the same script, I cross-reference these users with the Azure AD Group membership for the group that's scoped for Conditional License).
If user has an Authentication Method configured and is member of the group, MFA is enabled and enforced.
If user has an Authentication Method configured and not a member of the group, MFA is not enforced.
If user does not have an Authentication Method configured but is a member of the group, MFA is enabled but not yet enforced (e.g. user didn't enroll yet).
If user is not a member of the group, MFA is disabled.
Now this all sounds too much. And it is.
It's unbelievable that we have to do all of this to be able to report on such a basic feature.
But I really didn't see any other way to have a reliable inventory in our environment for MFA.
I would share the script, but it's really fully customized for our own environment and it wouldn't be usefull for you.
It does a complete inventory of all users, guests, licenses, last login, mfa, etc...
But as I've said... it's specific to our environment and it would be useless to share it with anyone.
The MFA part is loosely based on this script:
I took snippets of that script because it's very well written.
But if you use it as is, and add a few lines to get AAD Group membership, you would have the same.
Jul 11 2022 11:40 AM
Sep 25 2022 04:43 AM - edited Sep 25 2022 05:05 AM
I've been using this script for a while but only this week realised that the reporting was incorrect.
I've just edited the script to be more accurate in a generic way. Hopefully this helps others.
Where the script originally 'checked' for Conditional Access, I replaced this
$MFAStatus='Enabled via Conditional Access'
With this
$mfaPolicies = Get-AzureADMSConditionalAccessPolicy | Where {$_.GrantControls.BuiltInControls -contains "Mfa"}
$aadUser = Get-AzureADUser -ObjectId $Upn
$userObjectId = $aadUser.ObjectId
$userMembership = ($aadUser | Get-AzureADUserMembership).ObjectId
if (!$userMembership) {
$userMembership = ""
}
if ($mfaPolicies | Where {
$_.Conditions.Users.IncludeUsers -eq "All" -or `
$_.Conditions.Users.IncludeUsers -contains $aadUser.ObjectId -or `
(Compare-Object -ReferenceObject $_.Conditions.Users.IncludeGroups -DifferenceObject $userMembership -IncludeEqual -ErrorAction SilentlyContinue).SideIndicator -contains "==" -or `
(Compare-Object -ReferenceObject $_.Conditions.Users.IncludeRoles -DifferenceObject $userMembership -IncludeEqual -ErrorAction SilentlyContinue).SideIndicator -contains "==" -and `
$_.Conditions.Users.ExcludeUsers -notcontains $aadUser.ObjectId -or `
(Compare-Object -ReferenceObject $_.Conditions.Users.ExcludeGroups -DifferenceObject $userMembership -IncludeEqual -ErrorAction SilentlyContinue).SideIndicator -contains "==" -or `
(Compare-Object -ReferenceObject $_.Conditions.Users.ExcludeRoles -DifferenceObject $userMembership -IncludeEqual -ErrorAction SilentlyContinue).SideIndicator -contains "==" -and `
$_.State -eq "enabled"
}) {
$MFAStatus="Enabled via Conditional Access"
} else {
$MFAStatus="Disabled"
}
All it really does is get the current users ObjectID, get the ObjectID of all the roles and groups to which the user is assigned and then checks the conditional access policies which are configured for MFA to see if the user belongs to them, is not excluded from them, and the policy is enabled.
I had to use the AzureAD module to do this so I also have to authenticate with that module so I had to update the part of the script which connects to MSOnline to also connect to Azure AD by replacing this
if(($UserName -ne "") -and ($Password -ne ""))
{
$SecuredPassword = ConvertTo-SecureString -AsPlainText $Password -Force
$Credential = New-Object System.Management.Automation.PSCredential $UserName,$SecuredPassword
Connect-MsolService -Credential $credential
}
else
{
Connect-MsolService | Out-Null
}
With this
if(($UserName -ne "") -and ($Password -ne ""))
{
$SecuredPassword = ConvertTo-SecureString -AsPlainText $Password -Force
$Credential = New-Object System.Management.Automation.PSCredential $UserName,$SecuredPassword
Connect-MsolService -Credential $credential
Connect-AzureAD -Credential $credential
}
else
{
Write-Host "You will be prompted to sign-in twice, once to Microsoft 365 and then for Azure AD!" -ForegroundColor Yellow
Connect-MsolService | Out-Null
Connect-AzureAD | Out-Null
}
The issue seems to be that the script writer assumed that not explicitly enabling/enforcing MFA for a user meant that it was "Enabled via Conditional Access" and hard coded that value. This should have been "Controlled via Conditional Access", but it never meant a policy was actually applied.
UPADTE: These changes do cause the script to throw errors when a user is not a member of any roles/groups. They are nothing to worry about and the script is still accurate according to my testing. I'll try and figure out how to suppress them without making it really complicated.
UPDATE: Added a quick if statement to check if `$userMembership` is null, if it is then it's created as a blank variable which doesn't result in an error for the `compare-object`.
Sep 25 2022 04:47 AM
Sep 25 2022 04:51 AM
Sep 25 2022 05:42 AM
Another flaw with the admindroid script is that it only report the admin status for users who are actively assigned admin roles. If users are eligible for use through PIM, it does not recognise them as admin accounts.
This will require use of the AzureADPreview module by the looks of it. I am not opening that can of worms today.
Feb 02 2023 12:39 PM
any chance I can download that script ty @SimonBrown
Jul 23 2024 10:44 PM
@DonDDragon @SimonBrown please can anybody share the final script. Many thanks
Jul 24 2024 12:53 AM
Sorry @DonDDragon, I didn't see the notification for your message
Unfortunately I have not kept my script up to date to use the new MS Graph modules.
The steps above should help you adapt the latest version of the AdminDroid script.
When I get some time I will try and fork the current version of the AdminDroid script, update it and then link it here.
I can't remember the exact status of the old modules, they may still work. In which case, the latest version of my code is:
Param
(
[Parameter(Mandatory = $false)]
[switch]$DisabledOnly,
[switch]$EnabledOnly,
[switch]$EnforcedOnly,
[switch]$ConditionalAccessOnly,
[switch]$AdminOnly,
[switch]$LicensedUserOnly,
[Nullable[boolean]]$SignInAllowed = $null,
[string]$UserName,
[string]$Password
)
#Check for MSOnline module
$Modules=Get-Module -Name MSOnline -ListAvailable
if($Modules.count -eq 0)
{
Write-Host Please install MSOnline module using below command: `nInstall-Module MSOnline -ForegroundColor yellow
Exit
}
#Storing credential in script for scheduling purpose/ Passing credential as parameter
if(($UserName -ne "") -and ($Password -ne ""))
{
$SecuredPassword = ConvertTo-SecureString -AsPlainText $Password -Force
$Credential = New-Object System.Management.Automation.PSCredential $UserName,$SecuredPassword
Connect-MsolService -Credential $credential
Connect-AzureAD -Credential $credential
}
else
{
Write-Host "You will be prompted to sign-in twice, once to Microsoft 365 and then for Azure AD!" -ForegroundColor Yellow
Connect-MsolService | Out-Null
Connect-AzureAD | Out-Null
}
$Result=""
$Results=@()
$UserCount=0
$PrintedUser=0
#Output file declaration
$ExportCSV=".\MFADisabledUserReport_$((Get-Date -format yyyy-MMM-dd-ddd` hh-mm` tt).ToString()).csv"
$ExportCSVReport=".\MFAEnabledUserReport_$((Get-Date -format yyyy-MMM-dd-ddd` hh-mm` tt).ToString()).csv"
#Loop through each user
Get-MsolUser -All | foreach{
$UserCount++
$DisplayName=$_.DisplayName
$Upn=$_.UserPrincipalName
$MFAStatus=$_.StrongAuthenticationRequirements.State
$MethodTypes=$_.StrongAuthenticationMethods
$Title=$_.Title
$UsageLocation=$_.UsageLocation
$RolesAssigned=""
Write-Progress -Activity "`n Processed user count: $UserCount "`n" Currently Processing: $DisplayName"
if($_.BlockCredential -eq "True")
{
$SignInStatus="False"
$SignInStat="Denied"
}
else
{
$SignInStatus="True"
$SignInStat="Allowed"
}
#Filter result based on SignIn status
if(($SignInAllowed -ne $null) -and ([string]$SignInAllowed -ne [string]$SignInStatus))
{
return
}
#Filter result based on License status
if(($LicensedUserOnly.IsPresent) -and ($_.IsLicensed -eq $False))
{
return
}
if($_.IsLicensed -eq $true)
{
$LicenseStat="Licensed"
}
else
{
$LicenseStat="Unlicensed"
}
#Check for user's Admin role
$Roles=(Get-MsolUserRole -UserPrincipalName $upn).Name
if($Roles.count -eq 0)
{
$RolesAssigned="No roles"
$IsAdmin="False"
}
else
{
$IsAdmin="True"
foreach($Role in $Roles)
{
$RolesAssigned=$RolesAssigned+$Role
if($Roles.indexof($role) -lt (($Roles.count)-1))
{
$RolesAssigned=$RolesAssigned+","
}
}
}
#Filter result based on Admin users
if(($AdminOnly.IsPresent) -and ([string]$IsAdmin -eq "False"))
{
return
}
#Check for MFA enabled user
if(($MethodTypes -ne $Null) -or ($MFAStatus -ne $Null) -and (-Not ($DisabledOnly.IsPresent) ))
{
#Check for Conditional Access
if($MFAStatus -eq $null)
{
$mfaPolicies = Get-AzureADMSConditionalAccessPolicy | Where {$_.GrantControls.BuiltInControls -contains "Mfa"}
$aadUser = Get-AzureADUser -ObjectId $Upn
$userMembership = ($aadUser | Get-AzureADUserMembership).ObjectId
if (!$userMembership) {
$userMembership = ""
}
if ($mfaPolicies | Where {
$_.Conditions.Users.IncludeUsers -eq "All" -or `
$_.Conditions.Users.IncludeUsers -contains $aadUser.ObjectId -or `
(Compare-Object -ReferenceObject $_.Conditions.Users.IncludeGroups -DifferenceObject $userMembership -IncludeEqual -ErrorAction SilentlyContinue).SideIndicator -contains "==" -or `
(Compare-Object -ReferenceObject $_.Conditions.Users.IncludeRoles -DifferenceObject $userMembership -IncludeEqual -ErrorAction SilentlyContinue).SideIndicator -contains "==" -and `
$_.Conditions.Users.ExcludeUsers -notcontains $aadUser.ObjectId -or `
(Compare-Object -ReferenceObject $_.Conditions.Users.ExcludeGroups -DifferenceObject $userMembership -IncludeEqual -ErrorAction SilentlyContinue).SideIndicator -contains "==" -or `
(Compare-Object -ReferenceObject $_.Conditions.Users.ExcludeRoles -DifferenceObject $userMembership -IncludeEqual -ErrorAction SilentlyContinue).SideIndicator -contains "==" -and `
$_.State -eq "enabled"
}) {
$MFAStatus="Enabled via Conditional Access"
} else {
$MFAStatus="Disabled"
}
}
#Filter result based on EnforcedOnly filter
if((([string]$MFAStatus -eq "Enabled") -or ([string]$MFAStatus -eq "Enabled via Conditional Access")) -and ($EnforcedOnly.IsPresent))
{
return
}
#Filter result based on EnabledOnly filter
if(([string]$MFAStatus -eq "Enforced") -and ($EnabledOnly.IsPresent))
{
return
}
#Filter result based on MFA enabled via Other source
if((($MFAStatus -eq "Enabled") -or ($MFAStatus -eq "Enforced")) -and ($ConditionalAccessOnly.IsPresent))
{
return
}
$Methods=""
$MethodTypes=""
$MethodTypes=$_.StrongAuthenticationMethods.MethodType
$DefaultMFAMethod=($_.StrongAuthenticationMethods | where{$_.IsDefault -eq "True"}).MethodType
$MFAPhone=$_.StrongAuthenticationUserDetails.PhoneNumber
$MFAEmail=$_.StrongAuthenticationUserDetails.Email
if($MFAPhone -eq $Null)
{ $MFAPhone="-"}
if($MFAEmail -eq $Null)
{ $MFAEmail="-"}
if($MethodTypes -ne $Null)
{
$ActivationStatus="Yes"
foreach($MethodType in $MethodTypes)
{
if($Methods -ne "")
{
$Methods=$Methods+","
}
$Methods=$Methods+$MethodType
}
}
else
{
$ActivationStatus="No"
$Methods="-"
$DefaultMFAMethod="-"
$MFAPhone="-"
$MFAEmail="-"
}
#Print to output file
$PrintedUser++
$Result=@{'DisplayName'=$DisplayName;'UserPrincipalName'=$upn;'Title'=$Title;'UsageLocation'=$UsageLocation;'MFAStatus'=$MFAStatus;'ActivationStatus'=$ActivationStatus;'DefaultMFAMethod'=$DefaultMFAMethod;'AllMFAMethods'=$Methods;'MFAPhone'=$MFAPhone;'MFAEmail'=$MFAEmail;'LicenseStatus'=$LicenseStat;'IsAdmin'=$IsAdmin;'AdminRoles'=$RolesAssigned;'SignInStatus'=$SigninStat}
$Results= New-Object PSObject -Property $Result
$Results | Select-Object DisplayName,UserPrincipalName,Title,UsageLocation,MFAStatus,ActivationStatus,DefaultMFAMethod,AllMFAMethods,MFAPhone,MFAEmail,LicenseStatus,IsAdmin,AdminRoles,SignInStatus | Export-Csv -Path $ExportCSVReport -Notype -Append
}
#Check for MFA disabled user
elseif(($DisabledOnly.IsPresent) -and ($MFAStatus -eq $Null) -and ($_.StrongAuthenticationMethods.MethodType -eq $Null))
{
$MFAStatus="Disabled"
$Department=$_.Department
if($Department -eq $Null)
{ $Department="-"}
$PrintedUser++
$Result=@{'DisplayName'=$DisplayName;'UserPrincipalName'=$upn;'Department'=$Department;'MFAStatus'=$MFAStatus;'LicenseStatus'=$LicenseStat;'IsAdmin'=$IsAdmin;'AdminRoles'=$RolesAssigned; 'SignInStatus'=$SigninStat}
$Results= New-Object PSObject -Property $Result
$Results | Select-Object DisplayName,UserPrincipalName,Department,Title,UsageLocation,MFAStatus,LicenseStatus,IsAdmin,AdminRoles,SignInStatus | Export-Csv -Path $ExportCSV -Notype -Append
}
}
#Open output file after execution
Write-Host `nScript executed successfully
if((Test-Path -Path $ExportCSV) -eq "True")
{
Write-Host "MFA Disabled user report available in: $ExportCSV"
$Prompt = New-Object -ComObject wscript.shell
$UserInput = $Prompt.popup("Do you want to open output file?",`
0,"Open Output File",4)
If ($UserInput -eq 6)
{
Invoke-Item "$ExportCSV"
}
Write-Host Exported report has $PrintedUser users
}
elseif((Test-Path -Path $ExportCSVReport) -eq "True")
{
Write-Host "MFA Enabled user report available in: $ExportCSVReport"
$Prompt = New-Object -ComObject wscript.shell
$UserInput = $Prompt.popup("Do you want to open output file?",`
0,"Open Output File",4)
If ($UserInput -eq 6)
{
Invoke-Item "$ExportCSVReport"
}
Write-Host Exported report has $PrintedUser users
}
Else
{
Write-Host No user found that matches your criteria.
}
#Clean up session
Get-PSSession | Remove-PSSession
Jul 24 2024 01:11 AM