Get-AzureADStaleUsers.ps1

<#PSScriptInfo
 
.VERSION 1.0
 
.GUID a585e812-3110-4775-a521-1e61df71dc24
 
.AUTHOR Aaron Guilmette
 
.COMPANYNAME Microsoft
 
.COPYRIGHT 2020
 
.TAGS Azure AzureAD stale accounts
 
.LICENSEURI
 
.PROJECTURI https://www.undocumented-features.com/2018/06/22/how-to-find-staleish-azure-b2b-guest-accounts/
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
#>

<#
 
.DESCRIPTION Report on Azure AD Stale Users
 
If you are utilizing external, guest, or B2B users in your Office 365 or Azure environments,
you may need a way to determine which objects haven't been logged in or used in a while. Azure
AD doesn't provide an easy way to view this information (really only having the refresh token
time avaiable). This script uses the RefreshTokensValidFromDateTime property from the user in
conjunction with one of the following:
 
- default token refresh lifetime in Azure AD (90 days)
- the actual token refresh lifetime if a policy has been configured and is able to be read
- a user-specified value
 
The additional value, specified in the the StaleAgeInDays parameter, is added to the one of
the three previous tenant token times. If the user's refresh token is older than that value,
the user is "stale."
 
This will help you idenify when users last logged on and determine if you need to perform
further actions on them. You can see some more on this script at
https://www.undocumented-features.com/2018/06/22/how-to-find-staleish-azure-b2b-guest-accounts/.
#>


<#
.SYNOPSIS
Check for stale Azure, Guest, or B2B accounts
 
.DESCRIPTION
If you are utilizing external, guest, or B2B users in your Office 365 or Azure environments,
you may need a way to determine which objects haven't been logged in or used in a while. Azure
AD doesn't provide an easy way to view this information (really only having the refresh token
time avaiable). This script uses the RefreshTokensValidFromDateTime property from the user in
conjunction with one of the following:
 
- default token refresh lifetime in Azure AD (90 days)
- the actual token refresh lifetime if a policy has been configured and is able to be read
- a user-specified value
 
The additional value, specified in the the StaleAgeInDays parameter, is added to the one of
the three previous tenant token times. If the user's refresh token is older than that value,
the user is "stale."
 
This will help you idenify when users last logged on and determine if you need to perform
further actions on them. You can see some more on this script at
https://www.undocumented-features.com/2018/06/22/how-to-find-staleish-azure-b2b-guest-accounts/.
 
.PARAMETER Credential
Specify a credential to use when connecting to Azure AD.
 
.PARAMETER InstallRequiredModules
This script requires the Get-AzureADPolicy cmdlet, which is only available in
the AzureADPreview module. If the module is not installed or not available,
you can use either the MaxInactiveTime parameter or use the default of 90 days
 
.PARAMETER Logfile
Log events for script execution.
 
.PARAMETER MaxInactiveTime
Use this parameter to specify the MaxInactiveTime value for your tenant. This is
token refresh value. The default value for Azure Active Directory is 90 days.
You cannot view, add, or modify an Azure AD policy without the AzureADPreview
module. If you do not want to install the module, you can use the default for
this parameter or specify your own value.
 
.PARAMETER Output
Specify the output file listing stale acccounts.
 
.PARAMETER StaleAgeInDays
Use this parameter to specify how many days past the refresh token an account
can be inactive before marking it stale.
 
.EXAMPLE
.\Get-AzureADStaleUsers.ps1 -MaxInactiveTime 30 -StaleAgeInDays 180
Return all objects that have not generated a refresh token in 210 days.
 
.NOTES
2018-06-22 Release.
 
.LINK
https://blogs.technet.microsoft.com/undocumentedfeatures/2018/06/22/how-to-find-staleish-azure-b2b-guest-accounts/
 
.LINK
https://gallery.technet.microsoft.com/Report-on-Azure-AD-Stale-8e64c1c5/edit?newSession=True
#>

param (
    [System.Management.Automation.PSCredential]$Credential,
    [switch]$InstallRequiredModules,
    [string]$Logfile = (Get-Date -Format yyyy-MM-dd) + "_GetAzureADStaleGuestAccountsLog.txt",
    [int]$MaxInactiveTime,
    [string]$Output = (Get-Date -Format yyyy-MM-dd) + "_GetAzureADStaleGuestAccounts.txt",
    [int]$StaleAgeInDays = 180
)

# Logging function
function Write-Log([string[]]$Message, [string]$LogFile = $Script:LogFile, [switch]$ConsoleOutput, [ValidateSet("SUCCESS", "INFO", "WARN", "ERROR", "DEBUG")][string]$LogLevel)
{
    $Message = $Message + $Input
    If (!$LogLevel) { $LogLevel = "INFO" }
    switch ($LogLevel)
    {
        SUCCESS { $Color = "Green" }
        INFO { $Color = "White" }
        WARN { $Color = "Yellow" }
        ERROR { $Color = "Red" }
        DEBUG { $Color = "Gray" }
    }
    if ($Message -ne $null -and $Message.Length -gt 0)
    {
        $TimeStamp = [System.DateTime]::Now.ToString("yyyy-MM-dd HH:mm:ss")
        if ($LogFile -ne $null -and $LogFile -ne [System.String]::Empty)
        {
            Out-File -Append -FilePath $LogFile -InputObject "[$TimeStamp] [$LogLevel] $Message"
        }
        if ($ConsoleOutput -eq $true)
        {
            Write-Host "[$TimeStamp] [$LogLevel] :: $Message" -ForegroundColor $Color
        }
    }
}

# Requires Azure AD Preview Module
If (!(Get-Module -ListAvailable "AzureADPreview" -ea SilentlyContinue))
{
    Write-Log -LogFile $Logfile -LogLevel WARN -Message "Azure AD Preview Module not detected." -ConsoleOutput
    If ($InstallRequiredModules)
    {
        Write-Log -LogFile $Logfile -LogLevel INFO -Message "Attempting to install module." -ConsoleOutput
        # Check if Elevated
        $wid = [system.security.principal.windowsidentity]::GetCurrent()
        $prp = New-Object System.Security.Principal.WindowsPrincipal($wid)
        $adm = [System.Security.Principal.WindowsBuiltInRole]::Administrator
        if ($prp.IsInRole($adm))
        {
            Write-Log -LogFile $Logfile -LogLevel SUCCESS -ConsoleOutput -Message "Elevated PowerShell session detected. Continuing."
        }
        else
        {
            Write-Log -LogFile $Logfile -LogLevel ERROR -ConsoleOutput -Message "InstallRequiredModules must be run in an elevated PowerShell window. Please launch an elevated session and try again."
            $ErrorCount++
            Break
        }
        
        Install-Module AzureADPreview -Force
        
        Try
        {
            Import-Module AzureADPreview -Force
        }
        Catch
        {
            Write-Log -LogFile $Logfile -LogLevel ERROR -Message "Unable to import module. Please verify that the module is installed and try again." -ConsoleOutput
            Write-Log -LogFile $Logfile -LogLevel WARN -Message "Continuing using defaults for Azure AD Policy settings ('90 days token refresh')."
        }
    }
    Else
    {
        Write-Log -LogFile $Logfile -LogLevel ERROR -Message "Unable to detect module and InstallRequiredModules switch not supplied. Please verify that the module has installed and try again." -ConsoleOutput
        Write-Log -LogFile $Logfile -LogLevel WARN -Message "Continuing using defaults for Azure AD Policy settings ('90 days token refresh')."
    }
}
Else
{
    Import-Module AzureADPreview -Force    
}

# VerifyMaxInactiveTime
If ($MaxInactiveTime -lt 1) { Write-Log -LogFile $Logfile -Message "The value specified for MaxInactiveTime must be greater than 0." -Console -LogLevel ERROR; Break }

# Check for existing Azure AD Connection
Try
{
    $TestAzureAD = Get-AzureADTenantDetail
}
Catch [Microsoft.Open.Azure.AD.CommonLibrary.AadNeedAuthenticationException]
{
    Write-Host "You're not connected.";
    Connect-AzureAD -credential $cred
}


If (!($MaxInactiveTime))
{
    $AzureADPolicy = Get-AzureADPolicy | ? { $_.Type -eq "TokenLifetimePolicy" }
    If ($AzureADPolicy)
    {
        $PolicyData = $AzureADPolicy.Definition | ConvertFrom-Json
        # Retrieve value for MaxInactiveTime
        [int]$MaxInactiveTime = $PolicyData.TokenLifetimePolicy.MaxInactiveTime.Split(":")[0].Split(".")[0]
        
        # Test MaxInactiveTime; if not exist, set to AAD default of 90 days,
        # per https://docs.microsoft.com/en-us/azure/active-directory/active-directory-configurable-token-lifetimes
        If (!$MaxInactiveTime)
        {
            $MaxInactiveTime = "90"
        }
    }
}

# Get guest users
$Filter = "UserType eq 'Guest'"
$Guests = Get-AzureADUser -All $true -Filter $Filter

# Calculate users whose last STS refresh token value is 'n' past expiration
# For example, if token expiration is 90 days, and StaleAgeInDays is 180, then
# return objects that have a token age 270 days ago
$Today = (Get-Date)
$Global:StaleUsers = $Guests | ForEach-Object {
    $TimeStamp = $_.RefreshTokensValidFromDateTime
    $TimeStampString = $TimeStamp.ToString()
    [int]$LogonAge = [math]::Round(($Today - $TimeStamp).TotalDays)
    [int]$StaleAge = $MaxInactiveTime + $StaleAgeInDays
    $User = $($_.Mail)
    If ($LogonAge -ge $StaleAge)
    {
        [pscustomobject]@{
            User        = $($User)
            ObjectID     = $_.ObjectID
            IsStale      = "True"
            LastLogon    = $TimeStamp
            DaysSinceLastLogon = $LogonAge
            UserIsStaleAfterThisManyDays = $StaleAge
        }
    }
}
$StaleUsers | Export-Csv -NoTypeInformation -Path $Output