Skip to content

Export-AuthenticationMethodReport

Exports user authentication methods to Excel.

Example Output

Export-AuthenticationMethodReport output

Syntax

.\Export-AuthenticationMethodReport.ps1 [-Tenant <String>] [-Cloud <String>] [-Out <String>]

Description

Retrieves all users from Microsoft Entra ID along with their configured authentication methods, default method preferences, and tenant-level authentication method policies. Exports to Excel with Summary and Details worksheets.

Examples

Example 1

.\Export-AuthenticationMethodReport.ps1
# Exports authentication methods to an auto-named Excel file.

Example 2

.\Export-AuthenticationMethodReport.ps1 -Tenant 'contoso.onmicrosoft.com' -Out 'C:\Reports\AuthMethods.xlsx'
# Exports authentication methods for the specified tenant.

Parameters

-Tenant

Optional tenant ID or domain for specific tenant connection.

  • Type: String
  • Required: No

-Cloud

Cloud environment to connect to. Defaults to 'Global'. Valid values: Global, USGov, USGovDoD, Germany, China

  • Type: String
  • Required: No
  • Valid values: Global, GCC, GCCH, DoD, Germany, China

-Out

Output file path for the Excel export. If not specified, generates a filename based on tenant name and current timestamp.

  • Type: String
  • Required: No

Notes

Requires Microsoft.Graph and ImportExcel modules.

Full Script

Copy this complete standalone script (includes all helper functions):

#Requires -Version 7.0
#Requires -Modules ImportExcel, Microsoft.Graph.Authentication

<#
.SYNOPSIS
    Exports user authentication methods to Excel.
.DESCRIPTION
    Retrieves all users from Microsoft Entra ID along with their configured
    authentication methods, default method preferences, and tenant-level
    authentication method policies. Exports to Excel with Summary and Details
    worksheets.
.PARAMETER Tenant
    Optional tenant ID or domain for specific tenant connection.
.PARAMETER Cloud
    Cloud environment to connect to. Defaults to 'Global'.
    Valid values: Global, USGov, USGovDoD, Germany, China
.PARAMETER Out
    Output file path for the Excel export. If not specified, generates
    a filename based on tenant name and current timestamp.
.EXAMPLE
    .\Export-AuthenticationMethodReport.ps1
    # Exports authentication methods to an auto-named Excel file.
.EXAMPLE
    .\Export-AuthenticationMethodReport.ps1 -Tenant 'contoso.onmicrosoft.com' -Out 'C:\Reports\AuthMethods.xlsx'
    # Exports authentication methods for the specified tenant.
.NOTES
    Requires Microsoft.Graph and ImportExcel modules.
#>

[CmdletBinding()]
param(
    [string]$Tenant,

    [ValidateSet('Global', 'GCC', 'GCCH', 'DoD', 'Germany', 'China')

#region Helper Functions

function Write-TimeLog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [string]$Message,

        [Parameter()]
        [ConsoleColor]$Color = 'White'
    )

    Write-Host "[$(Get-Date -Format 'HH:mm')] $Message" -ForegroundColor $Color
}

function Connect-MgGraphIfNeeded {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]$Scopes,

        [Parameter()]
        [string]$Tenant,

        [Parameter()]
        [ValidateSet('Global', 'GCC', 'GCCH', 'DoD', 'Germany', 'China')]
        [string]$Cloud = 'Global'
    )

    $envMap = @{
        'Global'  = 'Global'
        'GCC'     = 'Global'
        'GCCH'    = 'USGov'
        'DoD'     = 'USGovDoD'
        'Germany' = 'Germany'
        'China'   = 'China'
    }

    try {
        $context = Get-MgContext

        if ($context) {
            $missingScopes = $Scopes | Where-Object { $_ -notin $context.Scopes }
            $tenantMismatch = $false

            if ($Tenant) {
                $currentTenantId = $context.TenantId
                if ($currentTenantId -ne $Tenant) {
                    $org = Get-MgOrganization -ErrorAction SilentlyContinue
                    $tenantDomain = $org.VerifiedDomains | Where-Object { $_.IsDefault } | Select-Object -ExpandProperty Name
                    if ($Tenant -ne $tenantDomain) {
                        $tenantMismatch = $true
                        Write-TimeLog "Current connection is to '$($org.DisplayName)' but '$Tenant' was requested" -Color Yellow
                    }
                }
            }

            if (-not $missingScopes -and -not $tenantMismatch) {
                $org = Get-MgOrganization
                Write-TimeLog "Using existing Graph connection to $($org.DisplayName)" -Color Cyan
                return $org
            }

            if ($tenantMismatch) {
                Write-TimeLog "Reconnecting to switch to tenant: $Tenant" -Color Yellow
            }
            elseif ($missingScopes) {
                Write-TimeLog "Reconnecting to add scopes: $($missingScopes -join ', ')" -Color Yellow
            }
        }

        $connectParams = @{
            Scopes      = $Scopes
            Environment = $envMap[$Cloud]
            NoWelcome   = $true
            ErrorAction = 'Stop'
        }

        if ($Tenant) {
            $connectParams['TenantId'] = $Tenant
        }

        Connect-MgGraph @connectParams
        $org = Get-MgOrganization
        Write-TimeLog "Connected to Graph: $($org.DisplayName)" -Color Green
        $org
    }
    catch {
        Write-TimeLog "Failed to connect to Microsoft Graph: $($_.Exception.Message)" -Color Red
        throw
    }
}

function Get-DefaultExcelParams {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    @{
        Path         = $Path
        TableStyle   = 'Medium2'
        AutoSize     = $true
        FreezeTopRow = $true
    }
}

function New-ReportPath {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$Out,

        [Parameter(Mandatory)]
        [string]$FriendlyName,

        [Parameter(Mandatory)]
        [string]$OrgDisplayName,

        [Parameter()]
        [string]$Extension = 'xlsx'
    )

    if (!$Out) {
        "$OrgDisplayName-$FriendlyName-$(Get-Date -Format 'yyyy-MM-dd_HH-mm').$Extension"
    }
    elseif ($Out -notlike "*.$Extension") {
        "$Out.$Extension"
    }
    else {
        $Out
    }
}

#endregion Helper Functions
]
    [string]$Cloud = 'Global',

    [string]$Out
)

$scopes = @(
    'User.Read.All',
    'Organization.Read.All',
    'UserAuthenticationMethod.Read.All',
    'Policy.Read.All'
)

$org = Connect-MgGraphIfNeeded -Scopes $scopes -Tenant $Tenant -Cloud $Cloud
$Out = New-ReportPath -Out $Out -FriendlyName 'UserAuthMethods' -OrgDisplayName $org.DisplayName

Write-TimeLog "Fetching all users..."

$users = Get-MgUser -All -Property Id, DisplayName, UserPrincipalName, AccountEnabled, CreatedDateTime

# Helper function to convert OData type to friendly method name
function Convert-MethodType {
    param([string]$OdataType)
    if (-not $OdataType) { return 'Unknown' }
    $t = $OdataType -replace '#microsoft.graph.', ''
    switch -Regex ($t) {
        '^emailAuthenticationMethod$' { 'Email' ; break }
        '^fido2AuthenticationMethod$' { 'FIDO2' ; break }
        '^microsoftAuthenticatorAuthenticationMethod$' { 'MSAuthenticator' ; break }
        '^passwordAuthenticationMethod$' { 'Password' ; break }
        '^phoneAuthenticationMethod$' { 'Phone' ; break }
        '^softwareOathAuthenticationMethod$' { 'SoftwareOATH' ; break }
        '^temporaryAccessPassAuthenticationMethod$' { 'TemporaryAccessPass' ; break }
        '^windowsHelloForBusinessAuthenticationMethod$' { 'WindowsHelloForBusiness' ; break }
        '^x509CertificateAuthenticationMethod$' { 'Certificate' ; break }
        default { $t }
    }
}

# Collect all method types present across the directory
$allMethodTypes = New-Object System.Collections.Generic.HashSet[string]
$userMethodMap = @{}

Write-TimeLog "Fetching authentication methods for $($users.Count) users..."

foreach ($u in $users) {
    try {
        $methods = Get-MgUserAuthenticationMethod -UserId $u.Id -ErrorAction Stop
        $userMethodMap[$u.Id] = $methods
        foreach ($m in $methods) {
            $null = $allMethodTypes.Add( (Convert-MethodType -OdataType $m.AdditionalProperties['@odata.type']) )
        }
    }
    catch {
        Write-Warning "Failed to retrieve authentication methods for $($u.UserPrincipalName): $_"
        $userMethodMap[$u.Id] = @()
    }
}

# Create a stable, readable order for the dynamic columns
$preferredOrder = @(
    'Password', 'MSAuthenticator', 'Phone', 'Email', 'FIDO2', 'WindowsHelloForBusiness', 'SoftwareOATH', 'TemporaryAccessPass', 'Certificate', 'Unknown'
)
$dynamicColumns = @()
foreach ($name in $preferredOrder) {
    if ($allMethodTypes.Contains($name)) { $dynamicColumns += $name }
}
# Append any remaining uncommon types that were discovered
$remaining = $allMethodTypes | Where-Object { $_ -notin $dynamicColumns }
$dynamicColumns += ($remaining | Sort-Object)

# Build rows with one Boolean column per discovered method + default & system preferred status
Write-TimeLog "Fetching sign-in preferences..."

$rows = foreach ($u in $users) {
    $row = [ordered]@{
        DisplayName       = $u.DisplayName
        UserPrincipalName = $u.UserPrincipalName
        AccountEnabled    = $u.AccountEnabled
        CreatedDate       = $u.CreatedDateTime
        MethodCount       = 0
        DefaultAuthMethod = $null
        IsSystemPreferred = $null
    }
    foreach ($col in $dynamicColumns) { $row[$col] = $false }

    $count = 0

    foreach ($m in $userMethodMap[$u.Id]) {
        $typeName = Convert-MethodType -OdataType $m.AdditionalProperties['@odata.type']
        if ($row.Contains($typeName)) { $row[$typeName] = $true }
        $count++
    }

    # Beta endpoint: get default authentication method or the system preferred method
    try {
        $pref = Invoke-MgGraphRequest -Method GET -Uri "/beta/users/$($u.Id)/authentication/signInPreferences" -ErrorAction Stop
        $row['IsSystemPreferred'] = [bool]$pref.isSystemPreferredAuthenticationMethodEnabled
        $row['PreferredMethod'] = $pref.userPreferredMethodForSecondaryAuthentication

        if ($pref.systemPreferredAuthenticationMethod) {
            $row['SystemPreferredMethod'] = $pref.systemPreferredAuthenticationMethod
        }

        # System preferred wins if enabled; otherwise, user's preferred selection
        if ($row['IsSystemPreferred']) { $row['DefaultAuthMethod'] = $row['SystemPreferredMethod'] }
        else { $row['DefaultAuthMethod'] = $row['PreferredMethod'] }
    }
    catch {
        # Leave preference fields null if not available
    }

    $row['MethodCount'] = $count
    [PSCustomObject]$row
}

# Summary data
$summaryCounts = foreach ($name in $dynamicColumns) {
    [PSCustomObject]@{
        Method          = $name
        UsersConfigured = ($rows | Where-Object { $_.$name -eq $true }).Count
    }
}

# Tenant authentication method configurations (enabled/disabled)
Write-TimeLog "Fetching tenant authentication settings..."

$summaryConfig = @()
try {
    $policy = Get-MgPolicyAuthenticationMethodPolicy -ErrorAction Stop
    $configs = $policy.AuthenticationMethodConfigurations

    foreach ($c in $configs) {
        # Prefer AdditionalProperties['@odata.type'] when present; else fall back to Id mapping
        $odata = $null
        if ($c.AdditionalProperties -and $c.AdditionalProperties.ContainsKey('@odata.type')) {
            $odata = [string]$c.AdditionalProperties['@odata.type']
            $odata = $odata -replace '#microsoft.graph.', ''
        }

        $name = switch ($odata) {
            'emailAuthenticationMethodConfiguration' { 'Email' }
            'fido2AuthenticationMethodConfiguration' { 'FIDO2' }
            'microsoftAuthenticatorAuthenticationMethodConfiguration' { 'MSAuthenticator' }
            'passwordAuthenticationMethodConfiguration' { 'Password' }
            'phoneAuthenticationMethodConfiguration' { 'Phone' }
            'softwareOathAuthenticationMethodConfiguration' { 'SoftwareOATH' }
            'temporaryAccessPassAuthenticationMethodConfiguration' { 'TemporaryAccessPass' }
            'windowsHelloForBusinessAuthenticationMethodConfiguration' { 'WindowsHelloForBusiness' }
            'x509CertificateAuthenticationMethodConfiguration' { 'Certificate' }
            'smsAuthenticationMethodConfiguration' { 'SMS' }
            'voiceAuthenticationMethodConfiguration' { 'Voice' }
            Default {
                switch ($c.Id) {
                    'Fido2' { 'FIDO2' }
                    'MicrosoftAuthenticator' { 'MSAuthenticator' }
                    'Password' { 'Password' }
                    'TemporaryAccessPass' { 'TemporaryAccessPass' }
                    'SoftwareOath' { 'SoftwareOATH' }
                    'Email' { 'Email' }
                    'Voice' { 'Voice' }
                    'Sms' { 'SMS' }
                    'Phone' { 'Phone' }
                    'WindowsHelloForBusiness' { 'WindowsHelloForBusiness' }
                    'X509Certificate' { 'Certificate' }
                    Default { if ($c.DisplayName) { $c.DisplayName } else { $c.Id } }
                }
            }
        }

        $enabled = $null
        if ($c.PSObject.Properties.Name -contains 'State') { $enabled = ($c.State -eq 'enabled') }
        elseif ($c.PSObject.Properties.Name -contains 'IsEnabled') { $enabled = [bool]$c.IsEnabled }

        $summaryConfig += [PSCustomObject]@{
            Method  = $name
            Enabled = $enabled
        }
    }
}
catch {
    Write-Warning "Failed to retrieve authentication method policy: $_"
}

# Export to Excel
$excelParams = Get-DefaultExcelParams -Path $Out

$summaryCounts | Export-Excel @excelParams -WorksheetName 'Summary' -TableName 'AuthMethodCounts'

# Tenant auth methods summary - add below the counts table
$startRow = ($summaryCounts.Count + 3)
$summaryConfig | Export-Excel -Path $Out -WorksheetName 'Summary' -TableName 'AuthMethodConfig' -StartRow $startRow -AutoSize

$rows | Export-Excel @excelParams -WorksheetName 'Details' -TableName 'UserAuthDetails'

Write-TimeLog "Exported to: $Out" -Color Green