Condividi tramite


Confirming/Validating PowerShell Get-Credential Input Before Use

Have you ever used Get-Credential to generate credentials in PowerShell, only to find out when you go to use those credentials that they were invalid? It’s even worse when you are doing this in a script where the script throws errors every time the invalid credentials are used, and you have to restart the script (and re-enter credentials) to correct it. Wouldn’t it have been nice to know right away if you used a bad user name or password, and been given a chance (or multiple chances) to fix it? If so, then this blog post is for you.

NOTE: The process described in this blog post is focused on authenticating accounts against an Active Directory (AD) domain. If you want to authenticate accounts via Get-Credential against another source, this process can still be used but a couple of components such as the .NET assembly might need to be changed.

For ease of reference/testing, I am providing this sample script that was used in creation of this blog post. Please read through this blog post entirely if you plan to download and test it out, otherwise you might not understand something important.

This all started a couple of years ago when I was asked to take over some scripts that automate the deployment of Exchange 2013 (we have since progressed on to Exchange 2016). One of the automation components was having the scripts prompt the admin for their user name and password, so the combination could be used (via the Registry) to automatically log the admin account back in after a reboot. This was used in conjunction to having the script immediately relaunch itself after logon (similar to the relaunching process in my PowerShell Version Control Process (PSVCP) blog), and pick up where it left off (see my other post about tracking and controlling a script’s execution progress via an XML file for more on that second part).

Sometimes one of the admins running the script, myself included, would “fat finger” their password because admin account passwords are generally long and complex and subsequently prone to type-o’s (that’s my excuse anyway 😊). The problem is Get-Credential doesn’t care what you provide it, it just assumes you put in exactly what you meant to put in and goes on its merry way. This bad password resulted is a failed logon attempt against AD when the server tried to automatically log on, subsequently the script didn’t automatically restart following the reboot, which resulted in admin confusion and wasted time.

I knew there had to be a way to validate the user name and password being into Get-Credential, even though it had no native way of doing that via the cmdlet itself. After doing some research I found that you could test a user name and password combination against AD via a .NET assembly (I *think* it was Shay Levy's post but it was some time ago). However, testing just once sufficient if the test failed, so I built a process around gathering credentials, validating them against AD, and providing multiple attempts to confirm the credentials were entered in correctly. At a high level, the are 3 parts to this entire process:

  1. Initializing the perquisites such as loading the AD Directory Services Account Management .NET assembly and establishing the initial parameters for performing the credential validation.
  2. A PowerShell Do Until loop construct that gathers and then validates the credentials until they are confirmed, or the maximum number of attempts are exceeded.
  3. The part of the process that uses those confirmed credentials, such as the rest of the script, or if the credentials can’t be confirmed then exiting out to avoid unnecessary errors and wasted time.

Initializing the Credential Confirmation Process Prerequisites

The first component needed is the AD Directory Services Account Management .NET assembly which is used later to confirm whether or not the provided user name and password is valid. Then there are additional variables used to initialize the process such as what goes into the credential prompt and the number of times to try prompting for credentials before giving up, as illustrated in the following example code:

 
# Prompt for Credentials and verify them using the DirectoryServices.AccountManagement assembly.
Write-Host "Please provide your credentials so the script can continue."
Add-Type -AssemblyName System.DirectoryServices.AccountManagement
# Extract the current user's domain and also pre-format the user name to be used in the credential prompt.
$UserDomain = $env:USERDOMAIN
$UserName = "$UserDomain\$env:USERNAME"
# Define the starting number (always #1) and the desired maximum number of attempts, and the initial credential prompt message to use.
$Attempt = 1
$MaxAttempts = 5
$CredentialPrompt = "Enter your Domain account password (attempt #$Attempt out of $MaxAttempts):"
# Set ValidAccount to false so it can be used to exit the loop when a valid account is found (and the value is changed to $True).
$ValidAccount = $False

While the current user’s Domain and user name are used as the default for the first credential prompt, they can be changed in the prompt and the code below will remember the change for future credential prompts. Also note that while the $Attempt variable must always be initially set to 1 for the logic in the loop construct to function properly, the number of times you want to try confirming the credentials before giving up is completely up to you by changing the $MaxAttempts variable.

Creating a Do Until Loop that Confirms the Credentials

Now that the .NET assembly is loaded, the information for the initial credential prompt is prepared, and the maximum number of times to try confirming the credentials is set, it’s time to initiate the loop to perform the actual work. To that end a Do Until loop was chosen because we always want to try a credential prompt at least once, and then we want to keep doing it until either it succeeds or hits the maximum number of attempts. Here is some additional example code showing just that:

 
# Loop through prompting for and validating credentials, until the credentials are confirmed, or the maximum number of attempts is reached.
Do {
    # Blank any previous failure messages and then prompt for credentials with the custom message and the pre-populated domain\user name.
    $FailureMessage = $Null
    $Credentials = Get-Credential -UserName $UserName -Message $CredentialPrompt
    # Verify the credentials prompt wasn't bypassed.
    If ($Credentials) {
        # If the user name was changed, then switch to using it for this and future credential prompt validations.
        If ($Credentials.UserName -ne $UserName) {
            $UserName = $Credentials.UserName
        }
        # Test the user name (even if it was changed in the credential prompt) and password.
        $ContextType = [System.DirectoryServices.AccountManagement.ContextType]::Domain
        Try {
            $PrincipalContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext $ContextType,$UserDomain
        } Catch {
            If ($_.Exception.InnerException -like "*The server could not be contacted*") {
                $FailureMessage = "Could not contact a server for the specified domain on attempt #$Attempt out of $MaxAttempts."
            } Else {
                $FailureMessage = "Unpredicted failure: `"$($_.Exception.Message)`" on attempt #$Attempt out of $MaxAttempts."
            }
        }
        # If there wasn't a failure talking to the domain test the validation of the credentials, and if it fails record a failure message.
        If (-not($FailureMessage)) {
            $ValidAccount = $PrincipalContext.ValidateCredentials($UserName,$Credentials.GetNetworkCredential().Password)
            If (-not($ValidAccount)) {
                $FailureMessage = "Bad user name or password used on credential prompt attempt #$Attempt out of $MaxAttempts."
            }
        }
    # Otherwise the credential prompt was (most likely accidentally) bypassed so record a failure message.
    } Else {
        $FailureMessage = "Credential prompt closed/skipped on attempt #$Attempt out of $MaxAttempts."
    }

    # If there was a failure message recorded above, display it, and update credential prompt message.
    If ($FailureMessage) {
        Write-Warning "$FailureMessage"
        $Attempt++
        If ($Attempt -lt $MaxAttempts) {
            $CredentialPrompt = "Authentication error. Please try again (attempt #$Attempt out of $MaxAttempts):"
        } ElseIf ($Attempt -eq $MaxAttempts) {
            $CredentialPrompt = "Authentication error. THIS IS YOUR LAST CHANCE (attempt #$Attempt out of $MaxAttempts):"
        }
    }
} Until (($ValidAccount) -or ($Attempt -gt $MaxAttempts))

As you can see the first thing the loop tries to do is execute the Get-Credentials cmdlet with the supplied user information and message which looks like this:

The user is free to change the “User name” before entering a password, and the user name can be in “Domain\Username” or username@domain.com UPN formats. As mentioned previously, whatever changes are made remain for the next try if there is a failure.

NOTE: The “User name” field will auto-suggest known credentials if you start to change it. If you highlight one you want to use, use the TAB key to select it and move to the Password field. If you use the ENTER key to try and select it, the credential prompt will think you are hitting the OK button and exhaust one of your attempts.

In this example you have 5 tries (starting with the one above) to try and enter your user name and password correctly. After the first try, the message gets modified to “Authentication error. Please try again”, and stays that way until the last try where it gets modified to the following:

All the while the failed attempts (and what they due to) are logged as Warnings in the main PowerShell console:

NOTE: If you happen to run into this error:

 Exception calling "ValidateCredentials" with "2" argument(s): "The server cannot handle directory requests."

Then you can try changing the security method the .NET assembly uses to talk to your DCs by changing the one line of code that starts with " $ValidAccount = " by adding [System.DirectoryServices.AccountManagement.ContextOptions]::Negotiate at the end like this:

 
$ValidAccount = $PrincipalContext.ValidateCredentials($UserName,$Credentials.GetNetworkCredential().Password,[System.DirectoryServices.AccountManagement.ContextOptions]::Negotiate)

Although doing so seems to break the use of the "Domain\Username" user name format, and only the UPN User name format works in that scenario. I will try to research how to work around that error without handicapping the "Domain\Username" User name format.

Continuing After the Do Until Loop

Hopefully a valid user name and password was used within the defined number of maximum attempts to generate confirmed PowerShell credentials. If they were then the script or whatever you are trying to do should continue on, otherwise things should probably stop - such as exiting the script. This next bit of sample code shows exiting the script if there were no valid credentials after all the attempts. Otherwise it outputs the resulting credentials to the screen, but you would probably drop the Else {…} statement and just have the rest of your script carry on:

 
# If the credentials weren't successfully verified, then exit the script.
If (-not($ValidAccount)) {
    Write-Host -ForegroundColor Red "You failed $MaxAttempts attempts at providing a valid user credentials. Exiting the script now... "
    EXIT
} Else {
    Write-Host "The authenticated credentials were as follows:"
    $Credentials
}

This is merely a suggestion on how to handle whether the user name and password was validated in the credential confirmation process, as you could prompt the user whether to continue the script or not versus just exiting. Either way the core code is there for you to decide what actions to take.

Closing Thoughts

So there you go, you can now add credential confirmation to your scripts/processes to ensure the user name and password entered are valid before continuing on (hopefully saving time and frustration). I hope this helps you in your PowerShell coding endeavors.

FYI – I use a similar Do Until loop process for requesting a password for a certificate (via Read-Host), and then trying to import a certificate (via Import-PfxCertificate) on a machine. This has also saved time and frustration in the automation process. 😊

Please feel free to leave me comments here if you wish, I promise I will try to respond to each in kind.

Thanks!

Dan Sheehan
Senior Premier Field Engineer