Edit

Share via


Generate a password expiration report using Microsoft Entra PowerShell

This article shows you how to create a comprehensive password expiration report for licensed users in your Microsoft Entra ID tenant using Microsoft Entra PowerShell. The script generates a CSV report with detailed password status information, statistics, and alerts for users with expiring or expired passwords.

Prerequisites

The scenario outlined in this article assumes that you already have the following items:

Note

Sign-in activity data (AuditLog.Read.All) might not be available in all Microsoft Entra PowerShell environments. The script handles this and continues without sign-in data if unavailable.

Before running the script, establish a connection to your Microsoft Entra tenant:

Connect-Entra -Scopes "User.Read.All", "Domain.Read.All", "Organization.Read.All", "AuditLog.Read.All"

Understand the password expiration report

The script performs the following operations:

  • Retrieves tenant password policy by getting the default ___domain password validity period
  • Filters for active users with assigned licenses
  • Calculates password expiration dates using the last password change date and policy settings
  • Generates comprehensive reports by creating a CSV output file
  • Provides statistical analysis that shows averages and trends
  • Highlights urgent cases for passwords expiring within 30 days or already expired

Run the password expiration report script

The following script creates a complete password expiration report:


$Documents = [Environment]::GetFolderPath("MyDocuments")
$CSVOutputFile = Join-Path $Documents "PasswordExpirationReport-Entra.CSV"

# Check what the tenant password expiration policy is
try {
    $DefaultDomain = Get-EntraDomain | Where-Object {$_.IsDefault -eq $true}
    $PasswordLifetime = $DefaultDomain.PasswordValidityPeriodInDays
    
    If ($PasswordLifetime -eq 2147483647) {
        $TenantPasswordExpirationDisabled = $true
        # adjust the value otherwise the date calculation will fail
        $PasswordLifetime = 20000
    } Else {
        $TenantPasswordExpirationDisabled = $false
    }
} catch {
    $PasswordLifetime = 90
    $TenantPasswordExpirationDisabled = $false
}

# Get organization name
try {
    $OrgName = Get-EntraOrganization | Select-Object -ExpandProperty DisplayName
} catch {
    $OrgName = "Unknown Organization"
}

# Find licensed member accounts
try {
    [Array]$Users = Get-EntraUser -All -Property Id, DisplayName, UserPrincipalName, Country, Department, `
        AssignedLicenses, JobTitle, AccountEnabled, CreatedDateTime, PasswordPolicies, LastPasswordChangeDateTime, `
        UserType, CompanyName | 
        Where-Object { $_.AssignedLicenses.Count -gt 0 -and $_.UserType -eq 'Member' } | 
        Sort-Object DisplayName
} catch {
    throw "Failed to retrieve users: $($_.Exception.Message)"
}

# Extract Information about each user
$Report = [System.Collections.Generic.List[Object]]::new()

ForEach ($User in $Users) {
    $DisabledPasswordExpiry = $false
    $WarningMessage = $null
    $LastSignIn = $null
    $DaysSinceLastSignIn = "N/A"
    
    # Check if the user account password policy disables password expiration
    If ($User.PasswordPolicies -like "*DisablePasswordExpiration*") {
        $DisabledPasswordExpiry = $true
    }
    
    # Handle cases where LastPasswordChangeDateTime might be null
    if ($User.LastPasswordChangeDateTime) {
        # Calculate the password expiry date
        [datetime]$PasswordExpiryDate = $User.LastPasswordChangeDateTime.AddDays($PasswordLifetime)
        # Calculate the number of days to password expiration
        $DaystoExpiration = ($PasswordExpiryDate - (Get-Date)).Days
        $PasswordLastChanged = $User.LastPasswordChangeDateTime | Get-Date -Format 'dd-MMM-yyyy HH:mm:ss'
        $PasswordExpiryFormatted = $PasswordExpiryDate | Get-Date -Format 'dd-MMM-yyyy HH:mm:ss'
    } else {
        # Handle users who have never changed their password
        $DaystoExpiration = "Unknown"
        $PasswordLastChanged = "Never changed"
        $PasswordExpiryFormatted = "Unknown"
    }
    
    # Try to get sign-in activity (this may not be available in all Microsoft Entra PowerShell versions)
    try {
        $SignInActivity = Get-EntraUserSignInActivity -UserId $User.Id -ErrorAction SilentlyContinue
        if ($SignInActivity.LastSignInDateTime) {
            $LastSignIn = $SignInActivity.LastSignInDateTime | Get-Date -Format 'dd-MMM-yyyy HH:mm:ss'
            $DaysSinceLastSignIn = ((Get-Date) - $SignInActivity.LastSignInDateTime).Days
        }
    } catch {
        # Sign-in activity not available - this is common in some Microsoft Entra PowerShell versions
        $LastSignIn = "Not available"
        $DaysSinceLastSignIn = "N/A"
    }
    
    # Generate warning message for passwords expiring soon
    if ($DaystoExpiration -is [int] -and $DaystoExpiration -lt 30 -and $DaystoExpiration -gt 0) {
        $WarningMessage = ("Password expires in {0} days" -f $DaystoExpiration)
    } elseif ($DaystoExpiration -is [int] -and $DaystoExpiration -le 0) {
        $WarningMessage = "Password has expired"
    }
    
    $ReportLine = [PSCustomObject][Ordered]@{
        UserDisplayName                     = $User.DisplayName
        UserPrincipalName                   = $User.UserPrincipalName
        Department                          = $User.Department
        'Job title'                         = $User.JobTitle
        'Last sign in'                      = $LastSignIn
        'Days since sign in'                = $DaysSinceLastSignIn
        'Password last changed'             = $PasswordLastChanged
        PasswordExpiryDate                  = $PasswordExpiryFormatted
        DaysToExpiration                    = $DaystoExpiration
        'Account Password Expiry Disabled'  = $DisabledPasswordExpiry
        'Account enabled'                   = $User.AccountEnabled
        Status                              = $WarningMessage
    }
    $Report.Add($ReportLine)
}

# Export CSV report
$Report | Export-Csv -Path $CSVOutputFile -NoTypeInformation -Encoding UTF8

The script generates a CSV report with detailed data for all processed users.

Each user record includes:

  • User information - Display name, UPN, department, job title
  • Password data - Last change date, expiry date, days to expiration
  • Sign-in activity - Last sign-in date and days since last sign-in (when available)
  • Policy status - Whether password expiration is disabled
  • Account status - Whether the account is enabled

Customize the script

You can modify the script for your specific needs using the following customization options:

Customization Description Code example
Change the warning threshold Modify the number of days before password expiration to trigger warnings
# Set your desired warning threshold (e.g. 14 days)
if ($DaystoExpiration -is [int] -and
  $DaystoExpiration -lt 14 -and $DaystoExpiration -gt 0) {
# …
}
Filter by Department Process only users from a specific department
# Add department filtering
[Array]$Users = Get-EntraUser -All -Property … |
  Where-Object {
    $.AssignedLicenses.Count -gt 0 -and
    $
.UserType -eq 'Member' -and
    $_.Department -eq "IT Department"
  } |
  Sort-Object DisplayName

You can also schedule this script to run regularly to keep your password expiration data up-to-date.

To run this report regularly, consider the following options:

  • Create a scheduled task in Windows Task Scheduler
  • Use PowerShell scheduled jobs with Register-ScheduledJob
  • Implement in automation platforms like Azure Automation

Troubleshoot common issues

Use the following troubleshooting guidance to resolve common issues when running the password expiration report:

Issue Description Solution
Permission denied errors Access denied when running the script due to insufficient permissions Ensure your account has the required roles and permissions. Reconnect with all necessary scopes:
Connect-Entra -Scopes "User.Read.All", "Domain.Read.All", "Organization.Read.All", "AuditLog.Read.All"
Missing sign-in data Sign-in activity data is not available or cannot be retrieved Sign-in activity may not be available in all environments. The script handles this issue and continues without this data.
Large tenant performance Script runs slowly or times out when processing large numbers of users For tenants with many users, consider:
- Running during off-peak hours
- Implementing progress tracking
- Adding error retry logic