Search code examples
httpwebrequestpowershell-2.0

How do you pass a SecureString from a PSCredential to a NetworkCredential?


NOTE: I cannot use PowerShell V3.0 here otherwise I'd be using Invoke-WebRequest and living a happy life.

I have a PowerShell V2.0 script that needs to POST data to a HTTP-Basic authenticated resource. For the purposes of the script I don't want or need to know the user's password, I just want to convert from a PSCredentials object (as returned from PromptForCredential) to a NetworkCredential for use with HttpWebRequest.

$uri = "https://example.com/some/resource/"

# Get our user's credentials...
$defaultUsername = "Some Username"
$caption = "Authentication required"
$message = "A username and password is required for ${uri}"
#$target = $uri #<<--NOTE: This prepends $uri+"\" to the username.
#$target = "" #<<--NOTE: This prepends "\" to the username.
$target = $null #<<--NOTE: This still prepends "\" to the username.
$psCredential = $Host.UI.PromptForCredential($caption, $message, $defaultUsername, $target)

# Construct a CredentialCache for HttpWebRequest...
# NOTE: We need to delete the "domain part" of the username from the PSCrential.Username, otherwise we get "Something\Username"
$username = ($psCredential.Username).Split('\')[1]
$networkCredential = New-Object System.Net.NetworkCredential($username, [System.Security.SecureString]$psCredential.Password)
$credentialCache = New-Object System.Net.CredentialCache
$credentialCache.Add( (New-Object Uri($uri)), "Basic", $networkCredential)

#...
$request = New-Object System.Net.HttpWebRequest($uri)
$request.Credentials = $credentialCache
#...
[System.Net.HttpWebResponse]$response = [System.Net.HttpWebResponse]$request.GetResponse()

This of course fails with the exception:

Exception calling "GetResponse" with "0" argument(s):
"The remote server returned an error: (401) Unauthorized."

Allegedly we have a NetworkCredential(String userName, SecureString password) constructor, but the user's credentials arrive the server as username:System.Security.SecureString.

Is there some little detail I'm missing? Do I need to decrypt the SecureString and pass that to the NetworkCredential(String userName, String password) constructor instead?


Solution

  • I've found the problem... the NetworkCredential(String userName, SecureString password) constructor is only available starting from .NET Framework 4.0. Of course PowerShell 2.0 is running in .NET 2.0.

    While there are ways and means of making PowerShell 2.0 run inside .NET 4.0 I'm not a liberty to alter the runtime environment's configuration.

    Instead I've gone down the "Unsecure String" path. Based on the article "How to properly convert SecureString to String" I've created this PowerShell function:

    function Convert-To-Unsecure-String {
            Param(
                [Parameter(HelpMessage="The SecureString object to make a very unsecure String")]
                [ValidateNotNull()]
                [System.Security.SecureString]
                $securePassword
            )
            $unmanagedString = [System.IntPtr]::Zero
            try {
                $unmanagedString = [Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($securePassword);
                return [Runtime.InteropServices.Marshal]::PtrToStringUni($unmanagedString);
            }
            finally {
                [Runtime.InteropServices.Marshal]::ZeroFreeGlobalAllocUnicode($unmanagedString);
            }
    }
    

    And replace the original example's NetworkCredential constructor with:

    $networkCredential = New-Object System.Net.NetworkCredential($username, (Convert-To-Unsecure-String($psCredential.Password)) )
    

    Now I'm getting the correct base64 encoded "username:password" string at the server.