2017-10-22

New PowerShell Core Feature: Basic and OAuth Authentication for Invoke-WebRequest and Invoke-RestMethod

2017102201

Intro

PowerShell has always supported Basic authentication on Invoke-WebRequest and Invoke-RestMethod via the -Credential parameter. Those of you who have tried to use it on any modern APIs are probably scratching you head at what I just wrote. You probably know full well that the -Credential parameter is not sending Basic Authentication credentials when you expect it to. But it's true, Basic authentication has been there all along. The problem is that PowerShell will only send credentials if challenged.

This is a problem for accessing modern API's, especially for requesting OAuth Tokens from authentication endpoints. Modern APIs often do not issue 401 status codes with WWW-Authenticate headers. This results in the Web Cmdlets not issuing credentials to the remote server even though they were supplied. Modern API's expect you to present your credentials explicitly without being challenged first. This has resulted in a common frustration and feature request.

Additionally, the Web Cmdlets did not have any native support for OAuth bearer tokens. This means that all calls to APIs requiring OAuth tokens required passing an Authorization header. Since many OAuth grant flows require the client ID and client secret be sent as Basic authentication without a challenge, the entire OAuth process becomes a manual task.

I have run into this myself with the PSRAW and PSMSGraph projects. In both projects I have created wrapper functions around Invoke-WebRequest and Invoke-RestMethod that provide OAuth capabilities and handle the conversion of PSCredential to Basic Authentication. I have long wish this functionality was native to the cmdlets.

I'm pleased to announce that beginning with PowerShell Core 6.0.0-Beta.9, Invoke-WebRequest and Invoke-RestMethod  natively support explicit Basic and OAuth authentication.

If you want this functionality now, build the current master branch or pickup the nightly build. this was added in Pull Request #5052.


History

As mentioned, Invoke-WebRequest and Invoke-RestMethod have always support basic authentication. It just only works under certain conditions. What conditions exactly? Well, without getting too deep into HTTP, PowerShell 5.1 and older would only send basic credentials when the server responded with a 401 status code and a WWW-Authenticate header. The request flow looks like this:

  1. User invokes Invoke-WebRequest with -Credentials
  2. Invoke-WebRequest requests the URL from the remote server without supplying credentials.
  3. The remote server responds with status code 401 and a WWW-Authenticate response header like this one:
    WWW-Authenticate: Basic realm="Protected Site"
  4. Invoke-WebRequest requests the URL again, this time including the header
    Authorization: Basic dXNlcjpwYXNzd2Q=
    where dXNlcjpwYXNzd2Q= is the base64 encoded version of <username>:<password>
  5. The server then response with the requested data, assuming the username and password were accepted.

If the remote server doesn't respond that way, the web cmdlets will never send the credentials to prevent the risk of exposing your user name and password to a serer unintentionally.

To get around this in PowerShell 5.1 and older, you would need to supply an Authorization header in a -Header dictionary and run your request like so:

$uri = 'https://httpbin.org/hidden-basic-auth/user/passwd'
$Credential = Get-Credential
$bytes = [System.Text.Encoding]::UTF8.GetBytes(
    ('{0}:{1}' -f $Credential.UserName, $Credential.GetNetworkCredential().Password)
)
$Authorization = 'Basic {0}' -f ([Convert]::ToBase64String($bytes))
$Headers = @{ Authorization = $Authorization }
Invoke-WebRequest -Uri $uri -Headers $Headers

That is pretty painful and it's not obvious as PowerShell currently lacks a native Base64 conversation cmdlet. It was also a shame because the capability is there to do this in the web cmdlets, it just only works in certain conditions which don't often apply to modern APIs.

As for OAuth, you also had to manually supply the Authorization header like this:

$Token = 'ac3071b88fe547e6b43ff7ae75176cccdd79fc4f28b84935a51bbeff74c985ed7fa61eb180e4429fb8f3501778bbb2cb'
$Headers = @{ Authorization = 'Bearer {0}' -f $Token }
Invoke-WebRequest -Uri $uri -Headers $Headers

Not as bad as Basic, but still a bit painful.


The Future

There was already an issue request for this before I started my work on the web cmdlets. I'm not surprised being as it is a fairly common request. However, I couldn't work on this one right away even though I wanted this above everything else. I needed to first become more familiar with the code base and to plan for the future. You see, this is only the beginning. The way we have implemented the new -Authentication features allows us to plug in support for other authentication methods in the future. To an end user this may not be that important, but I wanted the right design choice behind the scenes to prevent us from expanding support in the future.

So enough talk. Here is how you can perform explicit basic Authentication in PowerShell core beginning with v6.0.0-beta.9:

$uri = 'https://httpbin.org/hidden-basic-auth/user/passwd'
$Credential = Get-Credential
Invoke-RestMethod -uri $uri -Authentication Basic -Credential $Credential

You still use the -Credential parameter but you also supply -Authentication Basic. Much easier!

And here is how you can send OAuth tokens:

$Token = Read-Host -AsSecureString -Prompt "Enter OAuth Access Token"
Invoke-RestMethod -uri $uri -Authentication OAuth -Token $Token

There is also an alias for the OAuth option Bearer if you want to be more accurate:

$Token = Read-Host -AsSecureString -Prompt "Enter Beaer Token"
Invoke-RestMethod -uri $uri -Authentication Bearer -Token $Token

OAuth and Bearer make use of a new -Token parameter that accepts a SecureString. The reason we chose a SecureString instead of a normal string is to discourage Access Tokens being hard coded in plain text. Some OAuth endpoints treat their access tokens like long-lived passwords instead of short-loved secrets.You should never hard code your password, access tokens, refresh tokens, or client secrets in plain text. This does mean that when you receive a access token from am OAuth authentication end point, you will need to transform it to a secure string.

$OAuthResponse = Invoke-RestMethod -Uri $OAuthAuthEndPoint -Authentication Basic -Credential $ClientCredentials

[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
    "PSAvoidUsingConvertToSecureStringWithPlainText",
    "",
    Justification = "Converting received plaintext token to SecureString"
)]
$Token = $OAuthResponse.access_token | ConvertTo-SecureString -AsPlainText -Force

$result = Invoke-RestMethod -uri $APIEndpointUri -Authentication OAuth -Token $Token

If you find -Authentication is too much to type or if you constantly mistype it, you can also use -Auth:

Invoke-RestMethod -uri $uri -Auth Basic -Credential $Credential

Security Matters

Another new feature is that if you attempt to use any -Authentication option over a URI that does not begin with https://, the cmdlets will error and the request will not be sent to the remote server.

2017102202

If you are working with a legacy system and need to send your secrets in plain text over unencrypted connections, you can bypass this error with the -AllowUnencryptedAuthentication parameter.

Invoke-RestMethod -uri $uri -Authentication Basic -Credential $Credential -AllowUnencryptedAuthentication

It is not recommended that you send secrets over unencrypted connections. It is recommended you work with the end point administrator to add support for HTTPS.

Note on -UseDefaultCredentials

One thing to note is that -Authentication will not work with -UseDefaultCredentials. There is no way to convert your system password to plaintext to be included in explicit authentication.If you need to use your system credentials you will need to supply them via a PSCredential object.


Legacy Behavior Untouched

The -Credential parameter is still available in a stand-alone usage just as it was in PowerShell 5.1 and older. That functionality has not change and older scripts using this method will be unaffected. -Credential is used for challenge/response authentication for Basic, Digest, and NTLM. It still has its uses. In future versions, how you authenticate for those methods may change (as yet undecided) to align with the new -Authentication parameter, but for now it's the same as it ever was.


Conclusion

I hope you find this new feature as useful as I do. I'm definitely happy that I can rip this out of my own code now that it lives natively in the Web Cmdlets,

Join the conversation on Reddit!

5 comments:

Oliver Lipkau said...

Hi Mark.

I have been using -UseDefaultCredentials for authentication with -Proxy.
I have always felt disappointed that the cmdlets from PowerShellGet don't have them.. but now I can't use them at all from what I understand you.
I will have to update my Psdefaultparametervalues Everytime I change my password. And will have to store the PSCredential in it :-(

Too bad that can't be changed

Mark Kraus said...

@Oliver -UseDefaultCredentials does not work for proxy credentials. -ProxyUseDefaultCredentials can be used to use the default credentials to authenticate to the proxy and that functionality has not been touched by this change.

Oliver Lipkau said...

Thank you @Mark.
Indeed I was using this incorrectly.

Since you mentioned how OAuth can be used in PowerShell, I would like to know if you have any tips for this issues for implementing oauth for jira I have.

inscara said...

Hi,

I still cannot get this working. I post in stackoverflow about it. I would really appreciate if you can take a look and tell me what I'm missing.

https://stackoverflow.com/questions/47454430/how-to-use-powershell-invoke-restmethod-to-post-a-file?noredirect=1#comment81865801_47454430

Mark Kraus said...

@Oliver I had a quick look but I don't have the time to dig deep enough to answer your OAuth + Jira questions.

@inscara Your PowerShell code looks ok on the Authorization part. I cannot speak to the PHP code. If you use the following with your code you will see it successfully authenticates:

invoke-webrequest -Headers $headers 'https://httpbin.org/basic-auth/someuser/somepasswd'

You should be warned that Windows PowerShell 5.1 and lower do not have multipart/form-data support built in, so even after you authenticate that will not work.

I don't have a high enough StackOverflow reputation to add comments there so if you need me, please reach out on poshcode slack http://slack.poshcode.org/ @markekraus.