Entra / Microsoft 365 · Users & guests
Report user password changes
Report user password settings including last password change dates, MFA enablement, registered authentication methods, and account activity.
Connect & set up
Run these once per session. All scopes are read-only unless the script makes changes.
Connect-MgGraph -NoWelcome -Scopes AuditLog.Read.All, Directory.Read.All, UserAuthenticationMethod.Read.All, Policy.ReadWrite.AuthenticationMethod
Run it
The main script. Copy it, or download the .ps1 and run it from your console.
function Get-AuthMethods {# Function to return details of the authentication methods registered for a user[CmdletBinding()]Param ([Parameter(Mandatory=$true)][string]$UserId)[array]$AllMethods = @()Try {[array]$AuthMethods = Get-MgBetaUserAuthenticationMethod -UserId $UserId -ErrorAction Stop} Catch {Write-Host ("Failed to retrieve authentication methods for user ID {0}: {1}" -f $UserId, $_.Exception.Message) -ForegroundColor RedReturn $null}ForEach ($AuthMethod in $AuthMethods) {$P1 = $null; $P2 = $null; $LastUsed = $null; $CreatedDate = $null; $DisplayMethod = $null; $MethodSummary = $null$Method = $AuthMethod.AdditionalProperties['@odata.type']If ($AuthMethod.LastUsedDateTime) {$LastUsed = Get-Date($AuthMethod.LastUsedDateTime) -format "dd-MMM-yyyy HH:mm"}If ($AuthMethod.CreatedDateTime) {$CreatedDate = Get-Date($AuthMethod.CreatedDateTime) -format "dd-MMM-yyyy HH:mm"}Switch ($Method) {"#microsoft.graph.passwordAuthenticationMethod" {$DisplayMethod = "Password"$P1 = "Traditional password"If ($LastUsed) {$P1 = $P1 + " Last used: " + $LastUsed} Else {$P1 = $P1 + "No date for last use"}$MethodSummary = "Password"}"#microsoft.graph.microsoftAuthenticatorAuthenticationMethod" {$DisplayMethod = "Authenticator app"$P1 = $AuthMethod.AdditionalProperties['displayName']$P2 = $AuthMethod.AdditionalProperties['deviceTag'] + " " + $AuthMethod.AdditionalProperties['phoneAppVersion']If ($LastUsed) {$P2 = $P2 + " (Last used: " + $LastUsed + ")"}$MethodSummary = ("{0} on {1} ({2})" -f $DisplayMethod, $P1, $P2)}"#microsoft.graph.fido2AuthenticationMethod" {If ($AuthMethod.AdditionalProperties['aaGuid'] -eq "90a3ccdf-635c-4729-a248-9b709135078f") {$DisplayMethod = ("Passkey: {0}" -f $AuthMethod.AdditionalProperties['model'])} Else {$DisplayMethod = "Fido 2 Key"}$P1 = $AuthMethod.AdditionalProperties['displayName']If ($LastUsed) {$P2 = "Last used: " + $LastUsed} Else {$P2 = "No date for last use"}$MethodSummary = ("Passkey on {0}" -f $AuthMethod.AdditionalProperties['model'])}"#microsoft.graph.phoneAuthenticationMethod" {$DisplayMethod = "SMS"If ($LastUsed) {$P1 = ("Last used: {0}" -f $LastUsed)} Else {$P1 = $null}$MethodSummary = ("{0} to {1} ({2}) {3}" -f $DisplayMethod, $AuthMethod.AdditionalProperties['phoneNumber'], $AuthMethod.AdditionalProperties['phoneType'], $P1)}"#microsoft.graph.emailAuthenticationMethod" {$DisplayMethod = "Email (SSPR)"If ($LastUsed) {$P1 = ("Address {0} Last used: {1}" -f $AuthMethod.AdditionalProperties['emailAddress'], $LastUsed)} Else {$P1 = "Address: " + $AuthMethod.AdditionalProperties['emailAddress']}$MethodSummary = ("{0} to {1}" -f $DisplayMethod, $P1)}"#microsoft.graph.passwordlessMicrosoftAuthenticatorAuthenticationMethod" {$DisplayMethod = "Passwordless"$P1 = $AuthMethod.AdditionalProperties['displayName']IF ($LastUsed) {$P2 = "Last used: " + $LastUsed} Else {$P2 = "No date for last use"}If ($CreatedDate) {$P2 = $CreatedDate} Else {$P2 = "Unknown date"}$MethodSummary = ("{0} {1} enabled on {2}" -f $DisplayMethod, $P1, $P2)}"#microsoft.graph.windowsHelloForBusinessAuthenticationMethod" {$DisplayMethod = "Windows Hello for Business"If ($LastUsed) {$P1 = "Last used: " + $LastUsed} Else {$P1 = "No date for last use"}If ($CreatedDate) {$P2 = $CreatedDate} Else {$P2 = "Unknown date"}$MethodSummary = ("{0} ({1}) enabled for {2} on {3}" -f $DisplayMethod, $P1, $AuthMethod.AdditionalProperties['displayName'], $P2)}Default {$DisplayMethod = "Unknown authentication method"$MethodSummary = ("Unknown method: {0}" -f $AuthMethod.AdditionalProperties['@odata.type'])}}$MethodSummary = $MethodSummary.Trim()$AllMethods += $MethodSummary}$AllMethods = $AllMethods -join ", "Return $AllMethods}Connect-MgGraph -NoWelcome -Scopes AuditLog.Read.All, Directory.Read.All, UserAuthenticationMethod.Read.All, Policy.ReadWrite.AuthenticationMethod[string]$RunDate = Get-Date -format "dd-MMM-yyyy HH:mm:ss"$Version = "1.5"$CSVOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\UserAuthenticationReport.CSV"$ReportFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\UserAuthenticationReport.html"[string]$RunDate = Get-Date -format "dd-MMM-yyyy HH:mm:ss"$Version = "1.5"$CSVOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\UserAuthenticationReport.CSV"$ReportFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\UserAuthenticationReport.html"# Define the optional MFA user sign-in file downloaded from the Entra ID admin center. To generate the file, go to the Users section# of the Entra ID admin center. Go to the sign-in logs. Select display for last 30 days. Select Download option and pick interactive# sign-ins. Then wait for Entra to download the file to the PC. The downloaded file will be named InteractiveSignIns_date.csv. Rename# the file to remove the date portion, so that the file is just InteractiveSignIns.csv. Then edit the file with Excel to remove# the “incoming token type” column as otherwise the file can't be imported.$UserSignInDataFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\InteractiveSignIns.csv"# Flag indicating if MFA user sign-in data is available$MFADataAvailable = $false# If the interactive sign-in file is available, import itIf (Test-Path $UserSignInDataFile) {[array]$MFASignIns = Import-CSV $UserSignInDataFile -ErrorAction SilentlyContinue# If some data was imported, reduce it down to unique entries for user accounts that have used MFAIf ($MFASignIns) {[array]$MFAUserData = $MFASignIns | Where-Object {$_.'user type' -eq 'Member' -and $_.Status -eq 'Success'} |`Sort-Object {$_.'Date (UTC)' -as [datetime]} | Sort-Object 'User ID' -Unique | `Select-Object 'User ID', 'User', 'UserName', 'Date (UTC)','Multifactor authentication result', 'Multifactor authentication auth method'$MFADataAvailable = $trueWrite-Host ("MFA User Sign In Data available: using file dated {0}" -f (Get-Item $UserSignInDataFile).LastWriteTime)} Else {Write-Host ("MFA User Sign In Data file ({0}) unavailable" -f $UserSignInDataFile)}}Write-Host "Retrieving user details"[array]$Users = Get-MgUser -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member' and accountEnabled ne false" -ConsistencyLevel eventual `-CountVariable UsersFound -Property id, displayName, userprincipalname, usertype, signInActivity, SignInSessionsValidFromDateTime, LastPasswordChangeDateTime, passwordPolicies `-All -PageSize 500 -Sort displayNameWrite-Host ("All available user accounts fetched ({0}) - now processing report" -f $Users.count) -ForegroundColor Yellow# Get MFA data[array]$MFAData = Get-MgReportAuthenticationMethodUserRegistrationDetail -All -PageSize 500$MFAData = $MFAData | Where-Object {$_.userType -eq 'Member'}# Report what we've found$Report = [System.Collections.Generic.List[Object]]::new()[int]$i = 0ForEach ($User in $Users) {$i++Write-Host ("Processing {0} ({1}/{2})..." -f $User.displayname, $i, $Users.count)$DaysSincePasswordChange = $null; $PasswordPoliciesOutput = $null$DaysSinceLastSignIn = "N/A"; $DaysSinceLastSuccessfulSignIn = "N/A"If (!([string]::IsNullOrWhiteSpace($User.signInActivity.lastSuccessfulSignInDateTime))) {[datetime]$LastSuccessfulSignIn = $User.signInActivity.lastSuccessfulSignInDateTime$DaysSinceLastSuccessfulSignIn = (New-TimeSpan $LastSuccessfulSignIn).Days}If (!([string]::IsNullOrWhiteSpace($User.signInActivity.lastSignInDateTime))) {[datetime]$LastSignIn = $User.signInActivity.lastSignInDateTime$DaysSinceLastSignIn = (New-TimeSpan $LastSignIn).Days}If (!([string]::IsNullOrWhiteSpace($User.LastPasswordChangeDateTime))) {$LastPasswordChange = $User.LastPasswordChangeDateTime$DaysSincePasswordChange = (New-TimeSpan $LastPasswordChange).Days}$SessionTokensValidFrom = $User.SignInSessionsValidFromDateTime$LastPasswordChange = $User.LastPasswordChangeDateTime[array]$PasswordPolicies = $User.passwordPoliciesIf ($PasswordPolicies) {$PasswordPoliciesOutput = $PasswordPolicies -join ", "}# Get MFA status for the user. If the privacy flag is set, we get the basic data reported for the user# If not, we get the full set of authentication methods registered for the user, including the details of each method$UserMFAStatus = $MFAData | Where-Object {$_.Id -eq $User.Id}If ($PrivacyFlag -eq $true) {$AuthenticationTypesOutput = $UserMFAStatus.MethodsRegistered -join ", "} Else {$AuthenticationTypesOutput = Get-AuthMethods -UserId $User.Id}# Get per-user MFA data if possibleTry {$Data = Get-MgBetaUserAuthenticationRequirement -UserId $User.Id -ErrorAction Stop} Catch {Write-Host ("Failed to retrieve MFA requirements for user {0}: {1}" -f $User.displayName, $_.Exception.Message) -ForegroundColor Red}# Make sure dates are all in a common formatIf ($LastSignIn) {$LastSignInOutput = (Get-Date $LastSignIn -format 'dd-MMM-yyyy HH:mm')}If ($LastPasswordChange) {$LastPasswordChangeOutput = (Get-Date $LastPasswordChange -format 'dd-MMM-yyyy HH:mm')}If ($LastSuccessfulSignIn) {$LastSuccessfulSignInOutput = (Get-Date $LastSuccessfulSignIn -format 'dd-MMM-yyyy HH:mm')}If ($SessionTokensValidFrom) {$SessionTokensValidFromOutput = (Get-Date $SessionTokensValidFrom -format 'dd-MMM-yyyy HH:mm')}Switch ($UserMFAStatus.UserPreferredMethodForSecondaryAuthentication) {"sms" {$PreferredMethod = "SMS"}"voice" {$PreferredMethod = "Voice call"}"mobileAppNotification" {$PreferredMethod = "Authenticator app notification"}"mobileAppCode" {$PreferredMethod = "Authenticator app code"}"email" {$PreferredMethod = "Email"}"push" {$PreferredMethod = "Push notification to device"}Default {$PreferredMethod = $UserMFAStatus.UserPreferredMethodForSecondaryAuthentication}}If ($MFADataAvailable) {$MFAVerifiedDate = $null$MFAVerifiedDate = $MFAUserData | Where-Object {$_.'User Id' -eq $User.Id} | Select-Object -ExpandProperty 'Date (UTC)'If ($MFAVerifiedDate) {$MFAVerifiedDate = (Get-Date $MFAVerifiedDate -format 'dd-MMM-yyyy HH:mm')}$DataLine = [PSCustomObject][Ordered]@{User = $User.displayNameUserId = $User.IdUPN = $User.userPrincipalNameUserType = $User.userType'Last password change' = $LastPasswordChangeOutput'Days since password change' = $DaysSincePasswordChange'Last successful sign in' = $LastSuccessfulSignInOutput'Last sign in' = $LastSignInOutput'Days since successful sign in' = $DaysSinceLastSuccessfulSignIn'Days since sign in' = $DaysSinceLastSignIn'Session tokens valid from' = $SessionTokensValidFromOutput'Password policies applied' = $PasswordPoliciesOutput'Authentication methods' = $AuthenticationTypesOutput'Admin flag' = $UserMFAStatus.isAdmin'MFA capable' = $UserMFAStatus.IsMfaCapable'MFA registered' = $UserMFAStatus.IsMfaRegistered'MFA last used' = $MFAVerifiedDate'Preferred MFA method' = $PreferredMethod'Per user MFA state' = $Data.perUserMfaState}} Else {$DataLine = [PSCustomObject][Ordered]@{User = $User.displayNameUserId = $User.IdUPN = $User.userPrincipalNameUserType = $User.userType'Last password change' = $LastPasswordChangeOutput'Days since password change' = $DaysSincePasswordChange'Last successful sign in' = $LastSuccessfulSignInOutput'Last sign in' = $LastSignInOutput'Days since successful sign in' = $DaysSinceLastSuccessfulSignIn'Days since sign in' = $DaysSinceLastSignIn'Session tokens valid from' = $SessionTokensValidFromOutput'Password policies applied' = $PasswordPoliciesOutput'Authentication methods' = $AuthenticationTypesOutput'Admin flag' = $UserMFAStatus.isAdmin'MFA capable' = $UserMFAStatus.IsMfaCapable'MFA registered' = $UserMFAStatus.IsMfaRegistered'Preferred MFA method' = $PreferredMethod'Per user MFA state' = $Data.perUserMfaState}}# Output report line$Report.Add($DataLine)}# Now to generate a HTML reportWrite-Host "Generating HTML report..."[array]$PerUserMFAStates = "enabled", "enforced"$OrgName = (Get-MgOrganization).DisplayName# First, define the header.$HTMLHead="<html><style>BODY{font-family: Arial; font-size: 8pt;}H1{font-size: 22px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}H2{font-size: 18px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}H3{font-size: 16px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}TABLE{border: 1px solid black; border-collapse: collapse; font-size: 8pt;}TH{border: 1px solid #969595; background: #dddddd; padding: 5px; color: #000000;}TD{border: 1px solid #969595; padding: 5px; }td.admin{background: #B7EB83;}td.mfacapable{background: #E3242B;}td.mfaperuserenabled{background: #FFFF00;}td.mfaregistered{background: #FF474C;}</style><body><div align=center><p><h1>User Password and Authentication Report</h1></p><p><h2><b>For the " + $Orgname + " tenant</b></h2></p><p><h3>Generated: " + $RunDate + "</h3></p></div>"# This section highlights whether a conditional access policy is enabled or disabled in the summary.# Idea from https://stackoverflow.com/questions/37662940/convertto-html-highlight-the-cells-with-special-values# First, convert the CA Policies report to HTML and then import it into an XML structure$HTMLTable = $Report | Select-Object User, UPN, 'Last password change', 'Days since password change', 'Last successful sign in', 'Days since sign in', 'Password policies applied', `'Authentication methods', 'Admin flag', 'MFA capable', 'MFA registered', 'MFA last used', 'Preferred MFA method', 'Per user MFA state' | ConvertTo-Html -Fragment[xml]$XML = $HTMLTable# Create an attribute class to use, name it, and append to the XML table attributes$TableClass = $XML.CreateAttribute("class")$TableClass.Value = "State"$XML.table.Attributes.Append($TableClass) | Out-Null# Conditional formatting for the table rows.ForEach ($TableRow in $XML.table.SelectNodes("tr")) {# each TR becomes a member of class "tablerow"$TableRow.SetAttribute("class","tablerow")# If row has the admin flag set to trueIf (($TableRow.td) -and ([string]$TableRow.td[8] -eq 'True')) {## tag the TD with the color for admin in the heading$TableRow.SelectNodes("td")[8].SetAttribute("class","admin")}# If MFA capableIf (($TableRow.td) -and ([string]$TableRow.td[9] -eq 'True')) {$TableRow.SelectNodes("td")[9].SetAttribute("class","mfacapable")}# If MFA registeredIf (($TableRow.td) -and ([string]$TableRow.td[10] -eq 'True')) {$TableRow.SelectNodes("td")[10].SetAttribute("class","mfaregistered")}# Per-user MFA statusIf (($TableRow.td) -and ([string]$TableRow.td[13] -in $PerUserMFAStates)) {$TableRow.SelectNodes("td")[13].SetAttribute("class","mfaperuserenabled")}}# Wrap the output table with a div tag$HTMLBody = [string]::Format('<div class="tablediv">{0}</div>',$XML.OuterXml)[array]$MFAUsers = $Report | Where-Object {$_.'MFA Registered' -eq $True}[array]$AdminUsers = $Report | Where-Object {$_.'Admin Flag' -eq $True}[array]$AdminNoMfA = $AdminUsers | Where-Object {$_.'MFA Registered' -eq $False}[string]$AdminNoMFANames = $AdminNoMFA.User -Join ", "[int]$NumberAdminNoMFA = $AdminNoMFA.Count[int]$NumberUsersNoMFA = ($Users.Count - $MFAUsers.count)$PercentMFAUsers = ($NumberUsersNoMFA/$Users.Count).ToString("P")If ($NumberofAdmins -eq 0) {$PercentMFAAdmins = "N/A"} Else {$PercentMFAAdmins = ($NumberAdminNoMFA/$AdminUsers.Count).ToString("P")}[array]$PerUserMFAEnabled = $Report | Where-Object {$_.'Per user MFA state' -eq 'enabled'}[array]$PerUserMFAEnforced = $Report | Where-Object {$_.'Per user MFA state' -eq 'enforced'}# If MFA data is available, find out how many MFA-capable users are actually connecting with MFAIf ($MFADataAvailable) {[array]$MFAActiveUsers = $Report | Where-Object {$_.'MFA last used' -ne $null}$PercentMFAActive = ($MFAActiveUsers.Count/$MFAUsers.Count).toString("P")}# End stuff to output$HTMLTail = "<p>Report created for the " + $OrgName + " tenant on " + $RunDate + "<p>" +"<p>-----------------------------------------------------------------------------------------------------------------------------</p>"+"<p>Number of user accouts analyzed: " + $Users.Count + "</p>" +"<p>Number of admin accounts found: " + $AdminUsers.Count + "</p>" +"<p>Number of accounts registered for MFA: " + $MFAUsers.Count + "</p>"IF ($PerUserMFAEnabled.Count -gt 0 -or $PerUserMFAEnforced.Count -gt 0) {$HTMLTail = $HTMLTail +"<p>Number of accounts with per-user MFA: " + $PerUserMFAEnabled.Count + "</p>" +"<p>Names of accounts enabled for per-user MFA: " + ($PerUserMFAEnabled.User -join ", ") + "</p>" +"<p>Number of accounts with per-user MFA enforced: " + $PerUserMFAEnforced.Count + "</p>" +"<p>Names of accounts with per-user MFA enforced: " + ($PerUserMFAEnforced.User -join ", ") + "</p>"}If ($MFADataAvailable) {$HTMLTail = $HTMLTail +"<p>Number of MFA accounts active: " + $MFAActiveUsers.count + "</p>" +"<p>Percentage of MFA accounts active: " + $PercentMFAActive + "</p>"}$HTMLTail = $HTMLTail +"<p>User accounts not registered for MFA: " + $NumberUsersNoMFA + " (" + $PercentMFAUsers + ")</p>" +"<p>Admin accounts not registered for MFA: " + $NumberAdminNoMFA + " (" + $PercentMFAAdmins + ")</p>" +"<p>Names of admin accounts not registrered: " + $AdminNoMFANames + "</p>" +"<p>-----------------------------------------------------------------------------------------------------------------------------</p>"+"<p>Entra ID User Passwords and Authentication Report<b> " + $Version + "</b>"$HTMLReport = $HTMLHead + $HTMLBody + $HTMLtail$HTMLReport | Out-File $ReportFile -Encoding UTF8$Report | Export-Csv -NoTypeInformation $CSVOutputFile -Encoding utf8Write-Host ("HTML format report is available in {0} and CSV file in {1}" -f $ReportFile, $CSVOutputFile)$UserSignInDataFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\InteractiveSignIns.csv"# Flag indicating if MFA user sign-in data is available$MFADataAvailable = $false# If the file is available, import it (the “incoming token type” column must be removed first)If (Test-Path $UserSignInDataFile) {[array]$MFASignIns = Import-CSV $UserSignInDataFile -ErrorAction SilentlyContinue# If some data was imported, reduce it down to unique entries for user accounts that have used MFAIf ($MFASignIns) {[array]$MFAUserData = $MFASignIns | Where-Object {$_.'user type' -eq 'Member' -and $_.Status -eq 'Success'} |`Sort-Object {$_.'Date (UTC)' -as [datetime]} | Sort-Object 'User ID' -Unique | `Select-Object 'User ID', 'User', 'UserName', 'Date (UTC)','Multifactor authentication result', 'Multifactor authentication auth method'$MFADataAvailable = $trueWrite-Host ("MFA User Sign In Data available: using file dated {0}" -f (Get-Item $UserSignInDataFile).LastWriteTime)} Else {Write-Host ("MFA User Sign In Data file ({0}) unavailable" -f $UserSignInDataFile)}}Write-Host "Retrieving user details"[array]$Users = Get-MgUser -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member' and accountEnabled ne false" -ConsistencyLevel eventual `-CountVariable UsersFound -Property id, displayName, userprincipalname, usertype, signInActivity, SignInSessionsValidFromDateTime, LastPasswordChangeDateTime, passwordPolicies `-All -PageSize 500 -Sort displayNameWrite-Host ("All available user accounts fetched ({0}) - now processing report" -f $Users.count) -ForegroundColor Yellow# Get MFA data[array]$MFAData = Get-MgReportAuthenticationMethodUserRegistrationDetail -All -PageSize 500$MFAData = $MFAData | Where-Object {$_.userType -eq 'Member'}# Report what we've found$Report = [System.Collections.Generic.List[Object]]::new()[int]$i = 0ForEach ($User in $Users) {$i++Write-Host ("Processing {0} ({1}/{2})..." -f $User.displayname, $i, $Users.count)$DaysSincePasswordChange = $null; $PasswordPoliciesOutput = $null$DaysSinceLastSignIn = "N/A"; $DaysSinceLastSuccessfulSignIn = "N/A"If (!([string]::IsNullOrWhiteSpace($User.signInActivity.lastSuccessfulSignInDateTime))) {[datetime]$LastSuccessfulSignIn = $User.signInActivity.lastSuccessfulSignInDateTime$DaysSinceLastSuccessfulSignIn = (New-TimeSpan $LastSuccessfulSignIn).Days}If (!([string]::IsNullOrWhiteSpace($User.signInActivity.lastSignInDateTime))) {[datetime]$LastSignIn = $User.signInActivity.lastSignInDateTime$DaysSinceLastSignIn = (New-TimeSpan $LastSignIn).Days}If (!([string]::IsNullOrWhiteSpace($User.LastPasswordChangeDateTime))) {$LastPasswordChange = $User.LastPasswordChangeDateTime$DaysSincePasswordChange = (New-TimeSpan $LastPasswordChange).Days}$SessionTokensValidFrom = $User.SignInSessionsValidFromDateTime$LastPasswordChange = $User.LastPasswordChangeDateTime[array]$PasswordPolicies = $User.passwordPoliciesIf ($PasswordPolicies) {$PasswordPoliciesOutput = $PasswordPolicies -join ", "}# Get MFA status for the user. If the privacy flag is set, we get the basic data reported for the user# If not, we get the full set of authentication methods registered for the user, including the details of each method$UserMFAStatus = $MFAData | Where-Object {$_.Id -eq $User.Id}If ($PrivacyFlag -eq $true) {$AuthenticationTypesOutput = $UserMFAStatus.MethodsRegistered -join ", "} Else {$AuthenticationTypesOutput = Get-AuthMethods -UserId $User.Id}# Get per-user MFA data if possibleTry {$Data = Get-MgBetaUserAuthenticationRequirement -UserId $User.Id -ErrorAction Stop} Catch {Write-Host ("Failed to retrieve MFA requirements for user {0}: {1}" -f $User.displayName, $_.Exception.Message) -ForegroundColor Red}# Make sure dates are all in a common formatIf ($LastSignIn) {$LastSignInOutput = (Get-Date $LastSignIn -format 'dd-MMM-yyyy HH:mm')}If ($LastPasswordChange) {$LastPasswordChangeOutput = (Get-Date $LastPasswordChange -format 'dd-MMM-yyyy HH:mm')}If ($LastSuccessfulSignIn) {$LastSuccessfulSignInOutput = (Get-Date $LastSuccessfulSignIn -format 'dd-MMM-yyyy HH:mm')}If ($SessionTokensValidFrom) {$SessionTokensValidFromOutput = (Get-Date $SessionTokensValidFrom -format 'dd-MMM-yyyy HH:mm')}Switch ($UserMFAStatus.UserPreferredMethodForSecondaryAuthentication) {"sms" {$PreferredMethod = "SMS"}"voice" {$PreferredMethod = "Voice call"}"mobileAppNotification" {$PreferredMethod = "Authenticator app notification"}"mobileAppCode" {$PreferredMethod = "Authenticator app code"}"email" {$PreferredMethod = "Email"}"push" {$PreferredMethod = "Push notification to device"}Default {$PreferredMethod = $UserMFAStatus.UserPreferredMethodForSecondaryAuthentication}}If ($MFADataAvailable) {$MFAVerifiedDate = $null$MFAVerifiedDate = $MFAUserData | Where-Object {$_.'User Id' -eq $User.Id} | Select-Object -ExpandProperty 'Date (UTC)'If ($MFAVerifiedDate) {$MFAVerifiedDate = (Get-Date $MFAVerifiedDate -format 'dd-MMM-yyyy HH:mm')}$DataLine = [PSCustomObject][Ordered]@{User = $User.displayNameUserId = $User.IdUPN = $User.userPrincipalNameUserType = $User.userType'Last password change' = $LastPasswordChangeOutput'Days since password change' = $DaysSincePasswordChange'Last successful sign in' = $LastSuccessfulSignInOutput'Last sign in' = $LastSignInOutput'Days since successful sign in' = $DaysSinceLastSuccessfulSignIn'Days since sign in' = $DaysSinceLastSignIn'Session tokens valid from' = $SessionTokensValidFromOutput'Password policies applied' = $PasswordPoliciesOutput'Authentication methods' = $AuthenticationTypesOutput'Admin flag' = $UserMFAStatus.isAdmin'MFA capable' = $UserMFAStatus.IsMfaCapable'MFA registered' = $UserMFAStatus.IsMfaRegistered'MFA last used' = $MFAVerifiedDate'Preferred MFA method' = $PreferredMethod'Per user MFA state' = $Data.perUserMfaState}} Else {$DataLine = [PSCustomObject][Ordered]@{User = $User.displayNameUserId = $User.IdUPN = $User.userPrincipalNameUserType = $User.userType'Last password change' = $LastPasswordChangeOutput'Days since password change' = $DaysSincePasswordChange'Last successful sign in' = $LastSuccessfulSignInOutput'Last sign in' = $LastSignInOutput'Days since successful sign in' = $DaysSinceLastSuccessfulSignIn'Days since sign in' = $DaysSinceLastSignIn'Session tokens valid from' = $SessionTokensValidFromOutput'Password policies applied' = $PasswordPoliciesOutput'Authentication methods' = $AuthenticationTypesOutput'Admin flag' = $UserMFAStatus.isAdmin'MFA capable' = $UserMFAStatus.IsMfaCapable'MFA registered' = $UserMFAStatus.IsMfaRegistered'Preferred MFA method' = $PreferredMethod'Per user MFA state' = $Data.perUserMfaState}}# Output report line$Report.Add($DataLine)}# Now to generate a HTML reportWrite-Host "Generating HTML report..."[array]$PerUserMFAStates = "enabled", "enforced"$OrgName = (Get-MgOrganization).DisplayName# First, define the header.$HTMLHead="<html><style>BODY{font-family: Arial; font-size: 8pt;}H1{font-size: 22px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}H2{font-size: 18px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}H3{font-size: 16px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}TABLE{border: 1px solid black; border-collapse: collapse; font-size: 8pt;}TH{border: 1px solid #969595; background: #dddddd; padding: 5px; color: #000000;}TD{border: 1px solid #969595; padding: 5px; }td.admin{background: #B7EB83;}td.mfacapable{background: #E3242B;}td.mfaperuserenabled{background: #FFFF00;}td.mfaregistered{background: #FF474C;}</style><body><div align=center><p><h1>User Password and Authentication Report</h1></p><p><h2><b>For the " + $Orgname + " tenant</b></h2></p><p><h3>Generated: " + $RunDate + "</h3></p></div>"# This section highlights whether a conditional access policy is enabled or disabled in the summary.# Idea from https://stackoverflow.com/questions/37662940/convertto-html-highlight-the-cells-with-special-values# First, convert the CA Policies report to HTML and then import it into an XML structure$HTMLTable = $Report | Select-Object User, UPN, 'Last password change', 'Days since password change', 'Last successful sign in', 'Days since sign in', 'Password policies applied', `'Authentication methods', 'Admin flag', 'MFA capable', 'MFA registered', 'MFA last used', 'Preferred MFA method', 'Per user MFA state' | ConvertTo-Html -Fragment[xml]$XML = $HTMLTable# Create an attribute class to use, name it, and append to the XML table attributes$TableClass = $XML.CreateAttribute("class")$TableClass.Value = "State"$XML.table.Attributes.Append($TableClass) | Out-Null# Conditional formatting for the table rows.ForEach ($TableRow in $XML.table.SelectNodes("tr")) {# each TR becomes a member of class "tablerow"$TableRow.SetAttribute("class","tablerow")# If row has the admin flag set to trueIf (($TableRow.td) -and ([string]$TableRow.td[8] -eq 'True')) {## tag the TD with the color for admin in the heading$TableRow.SelectNodes("td")[8].SetAttribute("class","admin")}# If MFA capableIf (($TableRow.td) -and ([string]$TableRow.td[9] -eq 'True')) {$TableRow.SelectNodes("td")[9].SetAttribute("class","mfacapable")}# If MFA registeredIf (($TableRow.td) -and ([string]$TableRow.td[10] -eq 'True')) {$TableRow.SelectNodes("td")[10].SetAttribute("class","mfaregistered")}# Per-user MFA statusIf (($TableRow.td) -and ([string]$TableRow.td[13] -in $PerUserMFAStates)) {$TableRow.SelectNodes("td")[13].SetAttribute("class","mfaperuserenabled")}}# Wrap the output table with a div tag$HTMLBody = [string]::Format('<div class="tablediv">{0}</div>',$XML.OuterXml)[array]$MFAUsers = $Report | Where-Object {$_.'MFA Registered' -eq $True}[array]$AdminUsers = $Report | Where-Object {$_.'Admin Flag' -eq $True}[array]$AdminNoMfA = $AdminUsers | Where-Object {$_.'MFA Registered' -eq $False}[string]$AdminNoMFANames = $AdminNoMFA.User -Join ", "[int]$NumberAdminNoMFA = $AdminNoMFA.Count[int]$NumberUsersNoMFA = ($Users.Count - $MFAUsers.count)$PercentMFAUsers = ($NumberUsersNoMFA/$Users.Count).ToString("P")If ($NumberofAdmins -eq 0) {$PercentMFAAdmins = "N/A"} Else {$PercentMFAAdmins = ($NumberAdminNoMFA/$AdminUsers.Count).ToString("P")}[array]$PerUserMFAEnabled = $Report | Where-Object {$_.'Per user MFA state' -eq 'enabled'}[array]$PerUserMFAEnforced = $Report | Where-Object {$_.'Per user MFA state' -eq 'enforced'}# If MFA data is available, find out how many MFA-capable users are actually connecting with MFAIf ($MFADataAvailable) {[array]$MFAActiveUsers = $Report | Where-Object {$_.'MFA last used' -ne $null}$PercentMFAActive = ($MFAActiveUsers.Count/$MFAUsers.Count).toString("P")}# End stuff to output$HTMLTail = "<p>Report created for the " + $OrgName + " tenant on " + $RunDate + "<p>" +"<p>-----------------------------------------------------------------------------------------------------------------------------</p>"+"<p>Number of user accouts analyzed: " + $Users.Count + "</p>" +"<p>Number of admin accounts found: " + $AdminUsers.Count + "</p>" +"<p>Number of accounts registered for MFA: " + $MFAUsers.Count + "</p>"IF ($PerUserMFAEnabled.Count -gt 0 -or $PerUserMFAEnforced.Count -gt 0) {$HTMLTail = $HTMLTail +"<p>Number of accounts with per-user MFA: " + $PerUserMFAEnabled.Count + "</p>" +"<p>Names of accounts enabled for per-user MFA: " + ($PerUserMFAEnabled.User -join ", ") + "</p>" +"<p>Number of accounts with per-user MFA enforced: " + $PerUserMFAEnforced.Count + "</p>" +"<p>Names of accounts with per-user MFA enforced: " + ($PerUserMFAEnforced.User -join ", ") + "</p>"}If ($MFADataAvailable) {$HTMLTail = $HTMLTail +"<p>Number of MFA accounts active: " + $MFAActiveUsers.count + "</p>" +"<p>Percentage of MFA accounts active: " + $PercentMFAActive + "</p>"}$HTMLTail = $HTMLTail +"<p>User accounts not registered for MFA: " + $NumberUsersNoMFA + " (" + $PercentMFAUsers + ")</p>" +"<p>Admin accounts not registered for MFA: " + $NumberAdminNoMFA + " (" + $PercentMFAAdmins + ")</p>" +"<p>Names of admin accounts not registrered: " + $AdminNoMFANames + "</p>" +"<p>-----------------------------------------------------------------------------------------------------------------------------</p>"+"<p>Entra ID User Passwords and Authentication Report<b> " + $Version + "</b>"$HTMLReport = $HTMLHead + $HTMLBody + $HTMLtail$HTMLReport | Out-File $ReportFile -Encoding UTF8$Report | Export-Csv -NoTypeInformation $CSVOutputFile -Encoding utf8Write-Host ("HTML format report is available in {0} and CSV file in {1}" -f $ReportFile, $CSVOutputFile)
Attribution
Author
Office365itpros