Skip to main content

This function validates Active Directory credentials by attempting to authenticate against the Active Directory domain controller. It performs actual directory queries to ensure credentials are properly validated, not just directory binding.

function Test-ADCredential
{
    <#
    .SYNOPSIS
        Test the username and password of Active Directory credentials.

    .DESCRIPTION
        This function validates Active Directory credentials by attempting to authenticate
        against the Active Directory domain controller. It supports both simple name-based
        validation and thorough directory queries to ensure credentials are properly
        authenticated.

        REQUIREMENTS:
        - Windows platform (relies on Windows-specific .NET DirectoryServices classes)
        - Active connection to an Active Directory domain controller
        - Network access to query the domain

        NOTE: This function only works on Windows platforms. On macOS and Linux, use
        alternative LDAP authentication methods or PowerShell modules like 'Microsoft.Graph'
        for Azure AD authentication.

    .PARAMETER Credential
        The PSCredential object containing username and password to test.
        This parameter is mandatory and accepts pipeline input.

    .EXAMPLE
        PS> $cred = Get-Credential
        PS> Test-ADCredential -Credential $cred
        Successfully authenticated with 'username'
        True

        Tests the credentials provided and returns $true if they are valid.

    .EXAMPLE
        PS> Get-Credential | Test-ADCredential -Verbose
        VERBOSE: Successfully authenticated 'username'
        Successfully authenticated with 'username'
        True

        Tests credentials from the pipeline with verbose output.

    .EXAMPLE
        PS> Test-ADCredential -Credential $invalidCred -Verbose
        VERBOSE: Authentication failed for 'username': Unable to bind to directory
        Authentication failed for 'username'
        False

        Tests invalid credentials and shows the detailed error message with verbose output.

    .OUTPUTS
        System.Boolean
        Returns $true if authentication succeeds, otherwise $false.

    .NOTES
        This function validates credentials by first attempting a simple directory binding
        test, then performing a directory search to ensure comprehensive validation.
        It includes proper error handling and resource cleanup for all DirectoryServices
        objects.

    .LINK
        https://jonlabelle.com/snippets/view/powershell/test-windows-credential
    #>
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')]
    [CmdletBinding()]
    [OutputType([bool])]
    param (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [PSCredential]
        $Credential
    )

    begin
    {
        # Load required assemblies for DirectoryServices
        try
        {
            Add-Type -AssemblyName System.DirectoryServices.AccountManagement -ErrorAction Stop
            Add-Type -AssemblyName System.DirectoryServices -ErrorAction Stop
        }
        catch
        {
            Write-Warning "Failed to load DirectoryServices assemblies: $($_.Exception.Message)"
        }
    }

    process
    {
        # Platform detection
        if ($PSVersionTable.PSVersion.Major -lt 6)
        {
            # PowerShell 5.1 - Windows only
            $script:IsWindowsPlatform = $true
            $script:IsMacOSPlatform = $false
            $script:IsLinuxPlatform = $false
        }
        else
        {
            # PowerShell Core - cross-platform
            $script:IsWindowsPlatform = $IsWindows
            $script:IsMacOSPlatform = $IsMacOS
            $script:IsLinuxPlatform = $IsLinux
        }

        # Check if running on Windows
        if (-not $script:IsWindowsPlatform)
        {
            $platformName = if ($script:IsMacOSPlatform) { 'macOS' } elseif ($script:IsLinuxPlatform) { 'Linux' } else { 'this platform' }
            throw "Test-ADCredential is only supported on Windows. On $platformName, use alternative LDAP authentication methods or consider using PowerShell modules like 'Microsoft.Graph' for Azure AD authentication."
        }

        $username = $Credential.UserName
        $password = $Credential.GetNetworkCredential().Password

        try
        {
            # Get the domain distinguished name more reliably
            $rootDSE = New-Object System.DirectoryServices.DirectoryEntry('LDAP://RootDSE')
            $defaultNamingContext = $rootDSE.Properties['defaultNamingContext'][0]
            $domain = "LDAP://$defaultNamingContext"
            $rootDSE.Dispose()
        }
        catch
        {
            Write-Error 'Unable to connect to Active Directory. This function requires a connection to an Active Directory domain controller. Ensure you are on a domain-joined machine or have network access to a domain controller.'
            return $false
        }

        $de = $null
        try
        {
            $de = New-Object System.DirectoryServices.DirectoryEntry($domain, $username, $password)

            # Test authentication using the original simpler approach first
            if ($null -eq $de.Name)
            {
                Write-Verbose "Authentication failed for '$username': Unable to bind to directory"
                Write-Host "Authentication failed for '$username'" -ForegroundColor Red
                return $false
            }

            # If simple test passes, perform more thorough validation
            # Just accessing .Name isn't sufficient - we need to actually query the directory
            $searcher = New-Object System.DirectoryServices.DirectorySearcher($de)
            $searcher.Filter = '(objectClass=*)'
            $searcher.SizeLimit = 1
            $null = $searcher.FindOne()
            $searcher.Dispose()

            Write-Verbose "Successfully authenticated '$username'"
            Write-Host "Successfully authenticated with '$username'" -ForegroundColor Green
            $true
        }
        catch [System.Runtime.InteropServices.COMException]
        {
            # Handle COM exceptions from DirectoryServices operations
            Write-Verbose "Authentication failed for '$username': $($_.Exception.Message)"
            Write-Host "Authentication failed for '$username'" -ForegroundColor Red
            $false
        }
        catch [System.DirectoryServices.DirectoryServiceCOMException]
        {
            # Handle DirectoryServices specific exceptions if available
            Write-Verbose "Authentication failed for '$username': $($_.Exception.Message)"
            Write-Host "Authentication failed for '$username'" -ForegroundColor Red
            $false
        }
        catch
        {
            Write-Verbose "Authentication failed for '$username': $($_.Exception.Message)"
            Write-Host "Authentication failed for '$username'" -ForegroundColor Red
            $false
        }
        finally
        {
            if ($null -ne $de)
            {
                try
                {
                    $de.Dispose()
                }
                catch
                {
                    # Silently ignore disposal errors as the object may already be disposed
                    Write-Debug "DirectoryEntry disposal failed: $($_.Exception.Message)"
                }
            }
        }
    }
}