2017-12-24

PowerShell Core Web Cmdlets in Depth (Part 3)

2017122401

Part 3 Intro

In Part 1, I covered the primary changes in the actual code base of the PowerShell Core Web Cmdlets Invoke-RestMethod and Invoke-WebRequest and how those changes manifest themselves in the PowerShell user experience.

In Part 2, I covered outstanding issues as well missing and/or deprecated features.

In Part 3, I will cover new features available in PowerShell Core 6.0.0 Invoke-RestMethod & Invoke-WebRequest. I will also cover future plans for the cmdlets.

If you have not read Part 1 and Part 2, please do so before reading Part 3. This blog series goes in depth and requires a great many words to do so. To save space I will not repeat some information and will assume the reader as read Part 1 and Part 2.

A quick bit of news: PowerShell Core v6.0.0-rc.2 was released. Unless any blocking issues are discovered, this will be the final RC release and the next release will be GA in January 2018.


Table of Contents


New Web Cmdlet Features and Fixes in PowerShell Core 6.0.0

It's Christmas time and Santa Claus has delivered a sleigh-load of presents to the good little boys, girls, and other genders of the PowerShell community. I am very excited about many new of the features in PowerShell Core 6.0.0. So many breaking changes were made for the better and so many new features have been added. If I could cover them all it would take a year and could fill a book.

This blog series is focused on just the Web Cmdlets. If you have read parts 1 and 2 you can see just how radically they have changed, and they are only a small (but important) part of PowerShell. In this part of the series I will pile on all the goodies we are delivering to the Web Cmdlets in PowerShell Core 6.0.0.


New Parameters

Most of the new features of Invoke-RestMethod and Invoke-WebRequest  reveal themselves as new parameters available on the cmdlets. I will go into all of these in depth, but I want to show them all together so you can see just how impressive this list is:

New parameters in both Invoke-RestMethod and Invoke-WebRequest:

-AllowUnencryptedAuthentication
-Authentication
-CustomMethod
-NoProxy
-PreserveAuthorizationOnRedirect
-SkipCertificateCheck
-SkipHeaderValidation
-SslProtocol
-Token

In addition to the new parameters available to both cmdlets, Invoke-RestMethod  has theese additional new parameters:

-FollowRelLink
-MaximumFollowRelLink
-ResponseHeadersVariable

That makes for 12 total new parameters!

You may be wondering if this is too much or if the web cmdlets are being overcomplicated. Those of us who have worked in the Linux/Unix side of the world have had curl and wget. Those 2 binaries are extremely flexible and can be used for wide variety web scenarios. By contrast, Invoke-RestMethod and Invoke-WebRequest are severely lacking, or at least they have been in my opinion. Their simplicity has made them rigid and in some cases useless.

But, do we need all these new features? I believe we really do. This is not just to play "catch up" with curl and wget. I'm not seeking 100% parity with them. However, the modern IT paradigm has been shifting to more and more REST based services used to manage everything from internal on-premise services to cloud based SaaS, PaaS and IaaS. If PowerShell is to survive as a modern scripting language, it needs immaculate support for web requests and it needs to be flexible enough to work with the various web API implementations. These new features will enable module authors to more easily abstract REST services as native PowerShell commands.

That has been my focus in contributing to the Web Cmdlets as well as why I got involved to begin with. I am not a developer and I am very new to C# (less than a year), but I use PowerShell heavily to work with many web based APIs both personally and professionally. For work, I am using Microsoft Graph and many Office 365 & Azure REST APIs to manage and audit our Office 365 and Azure environments. Outside of work, I work closely with with Reddit API. My desire to contribute to the PowerShell Core Web Cmdlets was driven by frustration and disappointment with the state of Invoke-RestMethod and Invoke-WebRequest in Windows PowerShell 5.1.

So, I do believe these new features are needed. I understand that this raises the complexity of the cmdlets and in some cases breaks the "do one thing" principle, but, rather than have 100's of separate tiny tools to do related tasks, having a small number that are flexible enough to deal with the majority of API requirements in the wild seems to be the better option.


Updated Default User-Agents

Now that PowerShell Core is cross-platform, it was time to make some changes to the User-Agent HTTP request header that Invoke-RestMethod and Invoke-WebRequest sends by default. In Windows PowerShell 5.1 this is what is being sent to servers from my Windows 10 system:

Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US)  WindowsPowerShell/5.1.15063.674

Some things to note: The Windows NT and WindowsPowerShell where hard coded values in the Microsoft.PowerShell.Commands.PSUserAgent class. Those obviously needed to change. Here is what is sent now:

Windows:

Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.15063; en-US) PowerShell/6.0.0

Linux (line breaks added to readability):

Mozilla/5.0 (Linux;
    Linux 4.4.0-96-generic  #119-Ubuntu SMP Tue Sep 12 14:59:54 UTC 2017;
    en-US) PowerShell/6.0.0

macOS (line breaks added to readability)::

Mozilla/5.0 (Macintosh;
    Darwin 17.0.0 Darwin Kernel Version 17.0.0: Thu Aug 24 21:48:19  PDT 2017;
    root:xnu-4570.1.46~2/RELEASE_X86_64; ) PowerShell/6.0.0

You can see that the details are a bit more dynamic. The Platform identifier for Windows now includes the major and minor version. For all Linux and Unix distributions, Linux is the Platform Identifier. On macOS the Platform Identifier is Macintosh.The Platform Version Info now includes more detailed information for all platforms. Finally, it now says just PowerShell instead of WindowsPowerShell.

A change in User-Agent headers means that web servers may potentially serve different content to Invoke-RestMethod and Invoke-WebRequest. You may see different responses on Windows PowerShell 5.1 and PowerShell Core 6.0.0 on Windows as well as on Linux and macOS. Using the User-Agent to determine what kind of content to serve (for example, a desktop vs mobile version of a site) is not an exact science. While there is an RFC for User-Agent headers, it is very much irrelevant to what is used in the wild. This shouldn't be a problem for APIs, but may be for web scraping from normal web sites. My advice is to find a bowser that renders the page in a way you like, visit https://httpbin.org/headers, then use that User-Agent with the -UserAgent parameter.

If you want to see what User-Agent your Invoke-RestMethod and Invoke-WebRequest will send by default you can do one of 2 things.

1) Use  https://httpbin.org/headers:

(Invoke-RestMethod  https://httpbin.org/headers).Headers."User-Agent"

2) Use reflection

[Microsoft.PowerShell.Commands.PSUserAgent].
    GetMembers('Static, NonPublic').
    Where({$_.Name -eq 'UserAgent'})[0].
    GetValue($null, $null)

Also, don't forget that the Microsoft.PowerShell.Commands.PSUserAgent class provides several options as well:

Chrome

[Microsoft.PowerShell.Commands.PSUserAgent]::Chrome
Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.16299; en-US) AppleWebKit/534.6 (KHTML, like Gecko) Chrome/7.0.500.0 Safari/534.6

Firefox

[Microsoft.PowerShell.Commands.PSUserAgent]::FireFox
Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.16299; en-US) Gecko/20100401 Firefox/4.0

Internet Explorer

[Microsoft.PowerShell.Commands.PSUserAgent]::InternetExplorer
Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 10.0; Microsoft Windows 10.0.16299; en-US)

Opera

[Microsoft.PowerShell.Commands.PSUserAgent]::Opera
Opera/9.70 (Windows NT 10.0; Microsoft Windows 10.0.16299; en-US) Presto/2.2.1

Safari

[Microsoft.PowerShell.Commands.PSUserAgent]::Safari
Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.16299; en-US) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16

For example:

$Params = @{
    Uri       = 'https://httpbin.org/headers'
    UserAgent = [Microsoft.PowerShell.Commands.PSUserAgent]::Chrome
}
Invoke-RestMethod @Params

For more information, see Pull Requests #4914, #4937, and #5256.


Authentication

In PowerShell Core 6.0.0 we have added an -Authentication parameter that will perform explicit authentication requests. What I mean by explicit is that the authentication is sent without being challenged. In Windows PowerShell 5.1 you could use the -Credential parameter for Basic Authentication, however, it would only send the Authorization: Basic header only if it had first received a WWW-Authenticate: Basic response header from the remote endpoint.

I already have an in-depth blog on this new feature at https://get-powershellblog.blogspot.com/2017/10/new-powershell-core-feature-basic-and.html so I suggest reading that. I will provide a brief review and some examples here of the new options, but for in-depth coverage please read the previous entry. Also, there is one addendum to the previous blog: The legacy -Credential parameter will now produce an error when you attempt to use it over HTTP to be consistent with the newer authentication options. The relevant Pull requests are #5052 & #5402.

Another quick note, if you find that -Authentication is too long, you can use the shorter -Auth.


No Authentication

This is the default behavior of the cmdlets when no -Authentication option is supplied, but it is also included as an explicit option should you need it.

$uri = 'https://httpbin.org/get'
Invoke-RestMethod -uri $uri -Authentication None


Basic Authentication

The Basic option for the -Authentication parameter provides RFC-7617 Basic Authentication. This was available in Windows PowerShell 5.1, but only for challenge based authentication. Many modern APIs do not send challenge responses and expect you to provide your Basic Authentication up front. -Authentication Basic requires the -Credential parameter and does not support the -UseDefaultCredentials parameter.

For the below examples, the username is user and the password is passwd.

How to do this in Windows 5.1:

$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

How to do it in PowerShell 6.0.0

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

Much easier!


Bearer/OAuth Authentication

Welcome to the modern Internet where an ever increasing majority of APIs and websites function on OAuth Authentication! Going into how OAuth works is out of scope for this series, but you can read up on RFC-6749. To accommodate this, we have added the Bearer and OAuth options to the -Authentication parameter. Both options do the exact same thing but are included as separate options for convenience. They both implement the RFC- 6750 Authorization: Bearer header.

The Bearer and OAuth options require the new -Token parameter.  The -Token parameter requires a SecureString. OAuth tokens are supposed to be short-lived secrets. Like other secrets (such as passwords) they should not be in plain-text in your code. Also, some API's can have up to a 6 month expiration on OAuth tokens making them closer to traditional passwords instead of short-lived secrets.

For the following examples you can use any text you want for the OAuth token.

In Windows PowerShell 5.1

$uri = 'https://httpbin.org/headers'
$Token = Read-Host 'Enter OAuth Token'
$Headers = @{ Authorization = 'Bearer {0}' -f $Token }
Invoke-WebRequest -Uri $uri -Headers $Headers

In PowerShell Core 6.0.0

$uri = 'https://httpbin.org/headers'
$Token = Read-Host -AsSecureString -Prompt "Enter OAuth Token"
Invoke-RestMethod -uri $uri -Authentication OAuth -Token $Token

The SecureString does complicate things somewhat, but, you can now send it as a parameter instead of as a header.

Please note that this does not manage your OAuth access token lifecycle. You will still need to request tokens yourself as well as refresh them when they expire.


Authentication Over HTTP vs HTTPS

Attempts to send secrets over HTTP instead of HTTPS will now result in an error. Here is an example using the previous Basic Authentication example but changing the URI to http instead of https.

$Params = @{
    Uri            = 'http://httpbin.org/hidden-basic-auth/user/passwd'
    Authentication = 'Basic'
    Credential     = Get-Credential
}
Invoke-RestMethod @params

This results in the following error:

Invoke-RestMethod : The cmdlet cannot protect plain text secrets sent over unencrypted connections. To suppress this warning and send plain text secrets over unencrypted networks, reissue the command specifying the AllowUnencryptedAuthentication parameter.
At line:3 char:1
+ Invoke-RestMethod -uri $uri -Authentication Basic -Credential $Creden ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : InvalidArgument: (Microsoft.Power...stMethodCommand:InvokeRestMethodCommand) [Invoke-RestMethod], ValidationMetadataException
+ FullyQualifiedErrorId : WebCmdletAllowUnencryptedAuthenticationRequiredException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand

There are 2 ways around this. The first and recommended solution is to switch to using HTTPS. There is really no excuse to not use HTTPS any more. If you have a legacy system that only supports HTTP or only supports older deprecated SSL protocols, you should place those behind reverse proxies that can support HTTPS and TLS (preferably 1.2).

But, we understand that some times that is just not an option or you are testing something. The second and unsafe option is to use the new -AllowUnencryptedAuthentication parameter.

$Params = @{
    Uri            = 'http://httpbin.org/hidden-basic-auth/user/passwd'
    Authentication = 'Basic'
    Credential     = Get-Credential
}
Invoke-RestMethod @Params -AllowUnencryptedAuthentication

Again, you should avoid doing this unless necessary, as you will be sending secrets over an unencrypted protocol.


Improved Support for Multiple Response Headers With the Same Name

As mentioned in part 1, HttpClient treats multiple response headers with the same name differently and response headers are now an array of strings. This means that we can now properly reconstitute them in BasicHtmlWebResponseObject.RawContent. Relevant Pull Request: #4494

The following example has a URI that causes HttpBin.org to return the following response headers:

X-Header: Value1
X-Header: Value2

Example:

$Uri = 'https://httpbin.org/response-headers?X-Header=Value1&X-Header=Value2'
$Response = Invoke-WebRequest -Uri $uri
$Response.RawContent

Result on PS Core 6.0.0:

HTTP/1.1 200 OK
Connection: keep-alive
Date: Fri, 22 Dec 2017 14:58:49 GMT
Via: 1.1 vegur
Server: meinheld/0.6.1
X-Header: Value1
X-Header: Value2
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
X-Powered-By: Flask
X-Processed-Time: 0.000851154327393
Content-Length: 91
Content-Type: application/json

{
  "Content-Type": "application/json",
  "X-Header": [
    "Value1",
    "Value2"
  ]
}

Result on Windows PowerShell 5.1:

HTTP/1.1 200 OK
Connection: keep-alive
X-Header: Value1,Value2
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
X-Processed-Time: 0.000999212265015
Content-Length: 91
Content-Type: application/json
Date: Fri, 22 Dec 2017 15:01:42 GMT
Server: meinheld/0.6.1
Via: 1.1 vegur
X-Powered-By: Flask

{
  "Content-Type": "application/json",
  "X-Header": [
    "Value1",
    "Value2"
  ]
}

You can see that PowerShell Core 6.0.0 properly shows the separate headers but Windows PowerShell 5.1 concatenates the separate headers into one line.

As a refresher, this improvement also applies to the Headers property:

$Uri = 'https://httpbin.org/response-headers?X-Header=Value1&X-Header=Value2'
$Response = Invoke-WebRequest -Uri $uri
$Response.Headers."X-Header".GetType().Name
$Response.Headers."X-Header"

On PowerShell Core 6.0.0

String[]
Value1
Value2

On Windows PowerShell 5.1

String
Value1,Value2

This should significantly improve parsing response headers from APIs. Please note that APIs and websites may still chose to send single delimiter separated header instead of multiple headers with the same name. In those cases, there will be a single array element with the delimiter separated string.


Improved BasicHtmlWebResponseObject.Headers Performance

The BasicHtmlWebResponseObject.Headers returned from Invoke-WebRequest transforms the headers object returned from the .NET API into a more PowerShell friendly Dictionary. This is the case for both Windows PowerShell 5.1 and PowerShell Core 6.0.0 even though the type of objects it parses is different. In Windows PowerShell 5.1 there was a bit of inefficient code for generating the Dictionary. Every time you accessed the property, a new Dictionary would be generated. This means a small excess of memory and CPU usage as well as some confusion.

Take the following code in Windows PowerShell 5.1:

$Response = Invoke-WebRequest -Uri 'https://httpbin.org/get'
$Response.Headers['test'] = '123'
$Response.Headers['test']

The result is null and there are no errors. However this works as expected:

$Response = Invoke-WebRequest -Uri 'https://httpbin.org/get'
$Headers = $Response.Headers
$Headers['test'] = '123'
$Headers['test']

Result:

123

The reason the first block of code doesn't work is that the second time you access the Dictionary, a completely new Dictionary was created from the original response object.

In Pull Request #4853 this was fixed so that the Dictionary is only created once on the first access and then the same Dictionary is returned on all subsequent accesses.

The equivalent code in PowerShell Core 6.0.0:

$Response = Invoke-WebRequest -Uri 'https://httpbin.org/get'
$Response.Headers['test'] = [string[]]@('123')
$Response.Headers['test']

Result:

123

This isn't going to result in a serious performance gain and whether or not users actually modify the headers dictionary is debatable. But, every little bit counts in my book!


Response Header Support for Invoke-RestMethod

Modern API's return response headers that range from comical, to useful, to absolutely critical. For an example of a critical response header, several APIs return the X-RateLimit-Remaining header which determines the number of API calls remaining for the current reset period before the client will be blocked from making API requests.  This is to prevent the client from making too many API calls in too short a period of time. Other APIs use response headers to return information about where you are at in paged collection or when the current OAuth token will expire.

In Windows PowerShell 5.1, you have no way of retrieving the response headers on a successful call to Invoke-RestMethod. If you are working with an API in Windows PowerShell 5.1 and need to parse response headers, you have to fall back to Invoke-WebRequest, parse the headers, determine the content type, and then convert the content to a PowerShell object yourself. Basically, you are performing all the tasks that Invoke-RestMethod does for you just so you can inspect the Response Headers.

In Pull Request #4888 we added the -ResponseHeadersVariable parameter to address this problem. This features is covered in depth in my New PowerShell Core Feature: Invoke-RestMethod -ResponseHeadersVariable blog entry, so I will cover it only briefly here. This parameter works similar to the -SessionVariable parameter in that you supply a string for and a variable with that name will be created in the calling scope containing the same Response Headers Dictionary you would see on a similar Invoke-WebRequest call. If -ResponseHeadersVariable is too long for your tastes, there is also the -RHV alias you can use.

Here is an example:

$uri = 'https://httpbin.org/get'
$Headers = $null
$Result = Invoke-RestMethod -Uri $uri -ResponseHeadersVariable 'Headers'
$Headers['Content-Type']

Result:

application/json

Note that setting $Headers to null is not required and is done only to demonstrate that variable is empty before the call to Invoke-RestMethod. Also note that this parameter is only available on Invoke-RestMethod and is not available on Invoke-WebRequest. Invoke-WebRequest already returns the same headers as a property on the BasicHtmlWebResponseObject object.


-SslProtocol Parameter

Sometimes you find yourself in a position where you need to deliver code that meets certain security requirements. One increasingly common requirement is that network calls be made only over certain TLS versions such as TLS 1.1 and higher. To accommodate that need, Pull Request #5329 added the -SslProtocol parameter for both Invoke-RestMethod and Invoke-WebRequest.

The -SslProtocol parameter accepts a Microsoft.PowerShell.Commands.WebSslProtocol, which is a newly added Flag Enum. This is, admittedly, an odd choice for a PowerShell cmdlet parameter type, but the reason is that it can also accept a System.Security.Authentication.SslProtocols which is commonly used for this kind of setting. This allows for a single configuration setting to be used against multiple APIs. We created a separate Flags Enum, for this to ensure only the supported protocols can be supplied. However, you do not need to use this advanced feature. You can simply supply a string representation of the option or options.

The available options are as follows:

  • Default - This is the default setting used when -SslProtocol is not supplied. It uses the underlying defaults for the system which tend to be Tls, Tls11, and Tls12 all being allowed.
  • Tls - Use TLS 1.0
  • Tls11 - Use TLS 1.1
  • Tls12 -  Use TLS 1.2

Here is an example of this in action:

$uri = 'https://httpbin.org/get'
Invoke-RestMethod -Uri $Uri -SslProtocol 'Tls12'

You can specify multiple flags on some platforms. Currently this is not supported on macOS due to some underlying .NET Core limitations. We have also seen that some combinations are not supported on some Linux distributions. You can specify multiple options in two ways: using a string or using a binary-or:

$uri = 'https://httpbin.org/get'

# Multiple options as a string:
Invoke-RestMethod -Uri $Uri -SslProtocol 'Tls11, Tls12'

#Multiple options with -bor:
$Protocols =
    [Microsoft.PowerShell.Commands.WebSslProtocol]::Tls11 -bor
    [Microsoft.PowerShell.Commands.WebSslProtocol]::Tls12
Invoke-RestMethod -Uri $Uri -SslProtocol $Protocols

Note that when using a string representation of multiple options, you place the comma and space inside the string. This is not an array of strings. As stated in Part 2 SSL 1.0, 2.0 and 3.0 are not supported.


Custom HTTP Method Support

Some APIs  (IoT devices in particular) have a requirement for custom HTTP request methods. These are methods that fall outside the ones allowed by Microsoft.PowerShell.Commands.WebRequestMethod (GET, POST, HEAD, DELETE, MERGE, OPTION, PUT, and TRACE). Let's say you have an IoT water filtration device that has a reservoir tank that is managed through the /api/reservoir/ endpoint. In order to purge (dump) water from the reservoir, the IoT device expects the PURGE verb to be sent to the api/reservoir/ endpoint. In Windows PowerShell 5.1 this was not possible with the Web Cmdlets. However, in Pull Request #3142 the -CustomMethod parameter was added to Invoke-RestMethod and Invoke-WebRequest for PowerShell Core 6.0.0.

Example:

$Uri = 'https://http.lee.io/method'
$result = Invoke-RestMethod -Uri $uri -CustomMethod 'PURGE'
$result.output

Result:

method
------
PURGE


Proxy Bypass Support

There are times where you have a system-configured web proxy, but that proxy is not required or desirable for your Web Cmdlets calls. In Windows PowerShell 5.1 you were at the mercy of the system proxy settings. In Pull request #3447 the -NoProxy parameter was added for Invoke-RestMethod and Invoke-WebRequest.

Showing a demo of this is somewhat difficult as there are no publicly available proxies to test with that I'm aware of. But, I will walk you through briefly setting up a local proxy if you really want to see this in action. First, this assumes that you have installed node.js and npm which I will not cover here.

Prep and start the proxy server:

New-Item -ItemType Directory -Force c:\temp\proxy\
Set-Location c:\temp\proxy
npm install proxy-test-server
Set-Location .\node_modules\proxy-test-server\examples
node .\proxy.js

Now you will need to set your proxy in Internet Explorer to 127.0.0.1 port 9999 and enable it.  In another PowerShell Session test a normal web request across the proxy:

$uri = 'https://httpbin.org/get'
Invoke-RestMethod -Uri $Uri

On the node session you should see something like this:

PROXY Server Listening
[2017-12-2317:31:06] From ::ffff:127.0.0.1 to httpbin.org:443

and in the PowerShell Session you should see something like this:

args headers
---- -------
     @{Connection=close; Host=httpbin.org; User-Agent=Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.16299; en-US) Po...

Now that we know the proxy is working we can bypass it with the -NoProxy parameter:

$uri = 'https://httpbin.org/get'
Invoke-RestMethod -Uri $Uri -NoProxy

You should not see any new entries in the proxy session window because the cmdlet has bypassed the proxy. Now that you are done testing you can close the console you were running the proxy in and revert your proxy setting in Internet Explorer.

If you are thinking this is a security hole, it really isn't. If the user has the ability to bypass an administrator configured proxy in the .NET API (either directly or through an application configured to do so), then this is no different. It's somewhat difficult to keep a user from bypassing a proxy on their system unless you lock it down so they cannot use anything but Internet Explorer. There are just too many ways around the settings and not every program even supports or respects the system proxy settings. Usually, you effectively block this at the network level making it impossible to make web requests against anything but the proxy.


Support for Authorization on Redirect

Some websites and APIs provide generalized login endpoints that will redirect the client to the correct authentication endpoint that will ultimately process the request. This isn't a common scenario in my experience and it is often discouraged. It's discouraged because the potential exists for abusing the client's trust in an endpoint to redirect the client to the endpoint of a bad actor which then collects the authorization details from the client. To prevent this abuse, Invoke-RestMethod and Invoke-WebRequest do not allow the Authorization request header to be sent when the endpoint responds with a redirect response.

However unsafe and discouraged this may be, there are websites and APIs out there that require this. To make it possible to use those resources, Pull Request #3885 added the -PreserveAuthorizationOnRedirect parameter to Invoke-RestMethod and Invoke-WebRequest.

To see this in action, let's first do a normal redirect with an Authorization request header. You can do this in both PowerShell Core 6.0.0 and Windows PowerShell 5.1.

$Params = @{
Uri = 'https://httpbin.org/redirect-to?url=https://httpbin.org/get'
    Headers = @{Authorization = 'Test'}
}
$result = Invoke-RestMethod @Params
$result.headers.Authorization

The result is null because the redirected endpoint did not receive the Authorization header as it was stripped by Invoke-RestMethod. On PowerShell Core 6.0.0 do the following:

$Params = @{
Uri = 'https://httpbin.org/redirect-to?url=https://httpbin.org/get'
    Headers = @{Authorization = 'Test'}
}
$result = Invoke-RestMethod @Params -PreserveAuthorizationOnRedirect
$result.headers.Authorization

Result:

Test

The -PreserveAuthorizationOnRedirect parameter bypasses the default behavior and the redirected endpoint receives the Authorization header. Again, you should use this with caution because it has the potential to be abused.


Support for Accessing Sites with Expired, Self-Signed, Malformed, or Untrusted Certificates

This feature has been deeply needed for a long time and is still a common question on PowerShell forums and slack channels. I have heard of users switching their code to PowerShell Core just to take advantage of this new feature. So, what is this awesome new feature? It's the ability to work with sites that have self-signed certificates.

This is a common scenario: you have a network appliance, IoT device, or test site setup and it is using a self-signed certificate to provide HTTPS access. You implicitly trust the endpoint because it is yours so you don't necessarily care that the certificate is not issued by a trusted CA. You are either testing something out or in a chicken-and-egg scenario where you need to initially trust the certificate before you can apply a certificate issued by a trusted CA. In a web browser you can chose to ignore the certificate warnings and add a temporary trust. But in Windows PowerShell 5.1 there was no easy way to do this.

This not to say that there is no way to do thisin Windows PowerShell 5.1. One option is to add the certificate to the Trusted Root Authorities cert store. That's not really a good idea, so there is a programmatic solution. Since Windows PowerShell 5.1 uses WebRequest and WebRequest honors settings in System.Net.ServicePointManager, you can do something like this:

# In PowerShell 5.1
[System.Net.ServicePointManager]::ServerCertificateValidationCallback =
[System.Linq.Expressions.Expression]::Lambda(
    [System.Net.Security.RemoteCertificateValidationCallback],
    [System.Linq.Expressions.Expression]::Constant($true),
    [System.Linq.Expressions.ParameterExpression[]](
        [System.Linq.Expressions.Expression]::Parameter(
            [object], 'sender'),
        [System.Linq.Expressions.Expression]::Parameter(
            [X509Certificate], 'certificate'),
        [System.Linq.Expressions.Expression]::Parameter(
            [System.Security.Cryptography.X509Certificates.X509Chain], 'chain'),
        [System.Linq.Expressions.Expression]::Parameter(
            [System.Net.Security.SslPolicyErrors], 'sslPolicyErrors'))).
    Compile()

That's ugly and not at all apparent what is going on. There are other alternatives such as creating a small .NET class with a delegate that returns true. There is incorrect documentation out there that says you can assign a ScriptBlock that returns true, but that doesn't seem to work in recent versions.

The solution in PowerShell Core 6.0.0 is the -SkipCertificateCheck parameter added in Pull request #2006 for both Invoke-RestMethod and Invoke-WebRequest.

First, let's try a site with a self-signed certificate and see what happens:

$Uri = 'https://self-signed.badssl.com'
Invoke-RestMethod -Uri $Uri

Result:

Invoke-RestMethod : A security error occurred
At line:2 char:1
+ Invoke-RestMethod -Uri $Uri
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : InvalidOperation: (Method: GET, Re...rShell/6.0.0
}:HttpRequestMessage) [Invoke-RestMethod], HttpRequestException
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand

A Security error means the certificate used by the HTTPS endpoint is not trusted. In this case it is because the certificate is self-signed, but the same error occurs on sites with expired, malformed, and untrusted certificates. Now, let's see this new feature in action on PowerShell Core 6.0.0:

$Uri = 'https://self-signed.badssl.com'
Invoke-RestMethod -Uri $Uri -SkipCertificateCheck

Result:

<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="shortcut icon" href="/icons/favicon-red.ico"/>
  <link rel="apple-touch-icon" href="/icons/icon-red.png"/>
  <title>self-signed.badssl.com</title>
  <link rel="stylesheet" href="/style.css">
  <style>body { background: red; }</style>
</head>
<body>
<div id="content">
  <h1 style="font-size: 12vw;">
    self-signed.<br>badssl.com
  </h1>
</div>

</body>
</html>

This is a feature you really shouldn't use unless you have to and you should definitely not be setting this as a default parameter value. This could allow a bad actor to impersonate an endpoint and steal credentials or confidential data. Trust is an essential part of encryption, not just an unnecessary annoyance.

Instead of relying on this feature, you should work on getting a trusted certificate on to or in front of the endpoint. If the endpoint doesn't allow you to set a trusted certificate, you can always put the endpoint in front of an HTTPS reverse proxy. Let the proxy handle the trust issues. If you are testing, that is all the more reason to use properly trusted certs. You should validate how things will work in production where hopefully you are using properly trusted certs. Even if this means setting up a development PKI, you should not be relying on self-signed certs.

But, those are "perfect world" scenarios. So, please just use this feature sparingly and with caution.


Automated Pagination With Invoke-RestMethod

This feature is one of my favorites from all of PowerShell Core 6.0.0. Some APIs take advantage of RFC-5988 Link response headers when returning pages of results. Take the GitHub issues API as an example. If you go to https://api.github.com/repos/powershell/powershell/issues you will see it returns 30 open issues from the PowerShell Gallery. If you go to https://github.com/PowerShell/PowerShell/issues you can see that there are over 1,000 open issues, definitely more than 30. The API is returning paged results.

If you look at the response headers from https://api.github.com/repos/powershell/powershell/issues you will see the following (I have added line breaks for readability):

Link: <https://api.github.com/repositories/49609581/issues?page=2>; rel="next",
      <https://api.github.com/repositories/49609581/issues?page=38>; rel="last"

The API provides a URI to the next page and last page of results. if you go to https://api.github.com/repositories/49609581/issues?page=2 and inspect the response headers, you will see this:

Link: <https://api.github.com/repositories/49609581/issues?page=3>; rel="next",
<https://api.github.com/repositories/49609581/issues?page=38>; rel="last",
      <https://api.github.com/repositories/49609581/issues?page=1>; rel="first",
      <https://api.github.com/repositories/49609581/issues?page=1>; rel="prev"

In addition to the next page and last page links, it provides the previous page and first page links. These link headers are what API clients use to parse and create next, back, first, and last links or to pull in all results from all pages.

In Windows PowerShell 5.1, in order to leverage an API's Link header pagination, you had to use Invoke-WebRequest. As mentioned before, Windows PowerShell 5.1's Invoke-RestMethod does not provide a method for inspecting response headers. That means the process in Windows PowerShell 5.1 went something like this:

  1. Use Invoke-WebRequest to grab a page of results
  2. Perform error detection
  3. Detect the Content-Type (JSON/XML/Text)
  4. Serialize a PowerShell object from the content (e.g. ConvertFrom-Json).
  5. Check for the existence of Link header
  6. Use Regex to parse the Link headers to find the URI for the next page
  7. Use the parsed URI and repeat the above

That is bunch of work and large part of it would normally be handled by Invoke-RestMethod.

In Pull Request #3828 the -FollowRelLink and -MaximumFollowRelLink parameters were added to Invoke-RestMethod. This allows for Invoke-RestMethod to automatically follow the next page links. -FollowRelLink enables the behavior and -MaximumFollowRelLink sets the maximum number of links to follow.

Here is an example of it in action:

$FirstPage = 'https://api.github.com/repos/powershell/powershell/issues'
$Pages = Invoke-RestMethod -Uri $FirstPage -FollowRelLink -MaximumFollowRelLink 2
$Pages[0].Count
$Pages[1].Count

Result:

30
30

This may seem a bit counter-intuitive at first. You might expect that instead of returning an array of arrays, Invoke-RestMethod would just return the members of each page as a single array. There is a good reason why this is not done. A cardinal rule for any API client is that is should maintain parity with the remote API. This means that if the remote endpoint returns an array, the API client should also return an array and not a string, dictionary or other object type. In this example, the GitHub API returns an array of issues for each page. Thus Invoke-RestMethod maintains parity and returns an array for each page.

If you want to normalize the data so it is not an array of arrays but instead a single array containing all issues you can do the following:

$FirstPage = 'https://api.github.com/repos/powershell/powershell/issues'
$Params = @{
    Uri                  = $FirstPage
    FollowRelLink        = $true
    MaximumFollowRelLink = 2
}
$Results = Invoke-RestMethod @Params | ForEach-Object {$_}
$Results.Count

Result:

60

At this time I don't think we should bake a normalization capability into the Invoke-RestMethod. It would add some needless complication to the code and working with it in its current form is simple enough. To me, it is far more important to maintain parity.

There is an open issue on this that will be fixed in a future version. Issue #5667 points out a white space parsing issue in the current regex used to parse the link headers. One issue we did get fixed before 6.0.0 was support for multiple Link response headers being returned by an API (Pull request #5265). One possible feature addition for the future is to add the ability choose what link to follow. For example, supply the last page link and follow the previous page links to grab the last 3 pages.


Relation Links Parsed by Invoke-WebRequest

Invoke-WebRequest also has a new feature for the RFC-5988 Link response headers discussed in the previous section. The BasicHtmlWebResponseObject object returned by Invoke-WebRequest now includes a RelationLink Dictionary property. This is a Dictionary where the keys are the relation link type (next, first, last, prev, etc.) and the values are the URIs.

As an example, we can use the https://api.github.com/repositories/49609581/issues?page=2 page from the previous section:

$uri = 'https://api.github.com/repositories/49609581/issues?page=2'
$Response = Invoke-WebRequest -Uri $Uri
$Response.RelationLink

Result:

Key   Value
---   -----
next  https://api.github.com/repositories/49609581/issues?page=3
last https://api.github.com/repositories/49609581/issues?page=38
first https://api.github.com/repositories/49609581/issues?page=1
prev  https://api.github.com/repositories/49609581/issues?page=1

Even though Invoke-WebRequest  does not support automatic pagination, you can at least easily access the links. For example:

$uri = 'https://api.github.com/repositories/49609581/issues?page=2'
$Page2 = Invoke-WebRequest -Uri $Uri
$Page3 = Invoke-WebRequest -Uri $Page2.RelationLink['next']

This was also added in Pull Request #3828.


Support for multipart/form-data Submissions

Many APIs and Websites require a multipart/form-data submission for uploading files or making mixed data and text form submissions. In Windows PowerShell 5.1 it was not possible to do this with Invoke-RestMethod and Invoke-WebRequest. multipart/form-data submissions required a workaround and direct calls to .NET APIs. In PowerShell Core 6.0.0 we have added initial support for multipart/form-data submissions. This is only the first phase of support. It is still a bit ugly and requires some .NET API calls but it lays the groundwork for an improved implementation planned for 6.1.0.

I cover this in depth in my Multipart/form-data Support for Invoke-WebRequest and Invoke-RestMethod in PowerShell Core article. Rather than go in-depth here I will only provide an example of submitting text and a file.

Example:

# Create the MultipartFormDataContent object
$multipartContent = [System.Net.Http.MultipartFormDataContent]::new()

# Create the GivenName string content and add it to the MultipartFormDataContent
$stringHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data")
$stringHeader.Name = "GivenName"
$StringContent = [System.Net.Http.StringContent]::new("Mark")
$StringContent.Headers.ContentDisposition = $stringHeader
$multipartContent.Add($stringContent)

# Create the Surname string content and add it to the MultipartFormDataContent
$stringHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data")
$stringHeader.Name = "Surname"
$StringContent = [System.Net.Http.StringContent]::new("Kraus")
$StringContent.Headers.ContentDisposition = $stringHeader
$multipartContent.Add($stringContent)

# Create a profile text file and add it as a StreamContent to the MultipartFormDataContent
$multipartFile = 'C:\temp\profile.txt'
'Mark Loves PowerShell.' | Set-Content $multipartFile
$FileStream = [System.IO.FileStream]::new($multipartFile, [System.IO.FileMode]::Open)
$fileHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data")
$fileHeader.Name = "ProfileText"
$fileHeader.FileName = 'profile.text'
$fileContent = [System.Net.Http.StreamContent]::new($FileStream)
$fileContent.Headers.ContentDisposition = $fileHeader
$fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("image/png")
$multipartContent.Add($fileContent)

# Use the MultipartFormDataContent as the -Body value
$Params = @{
    Uri    = $uri
    Body   = $multipartContent
    Method = 'POST'
}
$Response = Invoke-WebRequest @Params
$Response.Content

Result:

{
"args": {},
  "data": "",
  "files": {
    "ProfileText": "Mark Loves PowerShell.\r\n"
  },
  "form": {
    "GivenName": "Mark",
    "Surname": "Kraus"
  },
  "headers": {
    "Connection": "close",
    "Content-Length": "481",
    "Content-Type": "multipart/form-data; boundary=\"2f26f0ed-be5c-47c6-93db-934366e219a4\"",
    "Host": "httpbin.org",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.16299; en-US) PowerShell/6.0.0"
  },
  "json": null,
  "origin": "173.239.232.143",
  "url": "https://httpbin.org/post"
}

Basically you can now supply a MultipartFormDataContent object as the -Body parameter value. We completed the specification for the simplified version of this but it was completed too late to be accepted for 6.0.0. This is what you can tentatively look forward to in 6.1.0:

# Not Yet Available
# Coming Soon in 6.1.0
$Form = @{
    GivenName = 'Mark'
    SurName = 'Kraus'
    ProfileText = Get-Item 'C:\temp\profile.txt'
}
$Params = @{
    Uri    = $uri
    Method = 'POST'
    Form = $Form
}
$Response = Invoke-WebRequest @Params

The current -Body parameter implementation in 6.0.0 will remain and you may still need to use it for more advanced multipart/form-data submissions, but the common uses will be further simplified. Notice the use of Get-Item. The -Form parameter will require a System.IO.FileInfo for files to distinguish file fields from text/string fields.


Single Value Null JSON Literal Support for Invoke-RestMethod

This is a breaking change fix in PowerShell Core 6.0.0. JSON supports single values as valid JSON. Usually, an API will return a dictionary or an array. Usually, they will return an array even if the array only contains a single value. But, a single JSON string, integer, or literal (true, false, or null) by itself alone is considered valid JSON. Windows PowerShell 5.1 works properly if the single value JSON is anything but null. When null is returned from an API, Invoke-RestMethod in Windows PowerShell 5.1 interprets it as the string "null" instead of $null. This behavior breaks parity with the API and requires logic to detect and correct in your own PowerShell code.

This has been fixed in Pull Request #5338. Run the following in both Windows PowerShell 5.1 and PowerShell Core 6.0.0:

$uri = 'http://urlecho.appspot.com/echo?status=200&Content-Type=application%2Fjson&body=null'
$result = Invoke-RestMethod -uri $uri
'Is $null?  {0}' -f ($null -eq $result)
'Is "null"? {0}' -f ('null' -eq $result)

Windows PowerShell 5.1 Result:

Is $null?  False
Is "null"? True

PowerShell Core 6.0.0 Result:

Is $null?  True
Is "null"? False

A quick note on empty responses: A 0 byte response or a white-space-only string is not valid JSON. JSON must be a string, integer, literal, dictionary, or array. As such, Invoke-RestMethod does not return $null for a 0 byte response or a white-space-only string. Instead, it returns either an empty string or a string that contains the white-space. This is by design and intended behavior. If you have an endpoint that returns 0 byte results or white-space-only strings that they document as being null, you will need to do your own null parsing. I recommend you contact the API owners and point them to the JSON RFCs. There is some confusion because an earlier RFC revision did not allow for single value literals and as a result it has persisted as a myth that they are not supported.  This is why empty responses have been incorrectly used to represent null.


Conclusion

I hope you have enjoyed this in-depth series on the PowerShell Core 6.0.0 Web Cmdlets. As you can see, so many things have been changed. Hopefully I have covered everything and haven't missed anything. If you notice something is missing, please reach out to me and I will add it.

The general operation of Invoke-RestMethod and Invoke-WebRequest has mostly not changed on the surface, but these 2 cmdlets are radically different in PowerShell Core 6.0.0. We have more features in store for the future too. As mentioned, an Invoke-Download like cmdlet will be added as the de facto file download cmdlet. We also plan for the simplified multipart/form-data support. Custom Certificate Validation is also slated for future versions. Another idea floating around is to add a webstream focused cmdlet for working with things like log streams and other stream based web request. There are also several issues open (such as RFC compliant JSON character encoding detection) that will be fixed in future versions.

Thanks for reading and Happy Holidays!

Join the conversation on Reddit!