Download file with SmartCard Authentication

I need to download a PDF file from a SharePoint Server that requires SmartCard Authentication. This powershell file will need to be run multiple times a day and by different users. The only method of authentication is via SmartCard. How can I do this?

I have tried the following method, where I first tried to retrieve credentials from:

$object = new-object Microsoft.VisualBasic.Devices.Network
Write-Host “Please Select SmartCard from dropdown and enter PIN.”;
$credential = $host.ui.PromptForCredential(“Need credentials”, “Please enter your user name and password.”, “”, “NetBiosUserName”)

then did the following:

  1. $web.Credentials = $credential
    $web.DownloadFile($url, $path)

  2. $object.DownloadFile($url, $path, $credential.username, $credential.password , $true, 500, $true, ‘DoNothing’)

Neither method worked.

If I create an instance of internet explorer I can open the PDF file, but I cannot get it to download automatically. Ideally, besides opening the powershell file and clicking authenticate on the SmartCard popup, everything should be invisible to the user. If not invisible, then everything would close down.

Smart card authentication for a website can normally be treated like a website that requires a certificate. Windows will handle any operations that require the private key, and it will prompt you for your PIN when needed. If you can use Invoke-WebRequest, it has a -Certificate parameter and an -OutFile parameter for saving a file. Try this (it depends on $Url and $Path already being defined):

Add-Type -AssemblyName System.Security

# You can do more filtering here if there are other cert requirements...
$ValidCerts = [System.Security.Cryptography.X509Certificates.X509Certificate2[]](dir Cert:\CurrentUser\My | where { $_.NotAfter -gt (Get-Date) })

# You could check $ValidCerts, and not do this prompt if it only contains 1...
$Cert = [System.Security.Cryptography.X509Certificates.X509Certificate2UI]::SelectFromCollection(
    $ValidCerts,
    'Choose a certificate',
    'Choose a certificate',
    'SingleSelection'
) | select -First 1

$IwrParams = @{
    Uri = $Url       # Uri to file to download
    OutFile = $Path  # Path to where file should be downloaded (include filename)
    Certificate = $Cert
}
Invoke-WebRequest @IwrParams

There are some scenarios where that still won’t work, though, so let me know what happens when you try it…

Rohn,

Thanks for the reply. Amateur hour here, sorry, I should have mentioned I am stuck with powershell v2. I was under the impression from earlier research that Invoke-webrequest is for version 3 and up; please correct me if I am wrong as I could not get it to work earlier. I have not had a chance to try your code. Any ideas for powershell v2?

Derek,

You’re right, Invoke-WebRequest isn’t available in v2. I think it just uses the .NET WebRequest and WebResponse objects, though, which should be available. Try this (don’t forget to fill in the first three variables):

$Url = 'URL HERE'
$OutFilePath = 'OUTPUT FILE NAME HERE'
$Cert = 'USER CERT HERE'

if (Test-Path $OutFilePath) {
    throw "'$OutFilePath' already exists!"
}

# Create webrequest that contains the selected certificate, and try to get a response
$Request = [System.Net.WebRequest]::Create($Url)
$Request.ClientCertificates.Add($Cert)

try {
    $Response = $Request.GetResponse()
}
catch {
    # You could present a nicer message here
    Write-Error $_
}

if ($Response) {

    # You'll probably want to check out the $Response object before doing anything with
    # it (probably at least check $Response.StatusCode)

    # There's probably a shorter/cleaner/better way to do this, but this will create a buffer and a filestream,
    # then transfer the binary data from the $Response's stream to the filestream using the buffer...
    $Buffer = New-Object byte[] 1024  # You can adjust the buffer size
    $OutFileStream = [System.IO.File]::Create($OutFilePath)   # This will overwrite an existing file!
    $ResponseStream = $Response.GetResponseStream()

    while (($BytesRead = $ResponseStream.Read($Buffer, 0, $Buffer.Length))) {
        $OutFileStream.Write($Buffer, 0, $BytesRead)
    }

    # Cleanup
    $OutFileStream.Flush()
    $OutFileStream.Dispose()
    $ResponseStream.Dispose()
    $Response.Close()
}

Rohn,

You are a PowerShell Genius! Combining your two threads worked perfect.