Add Authentication Parameter to Web Cmdlets for Basic and OAuth (#5052)

Closes #4274

Adds an -Authentication parameter to Invoke-RestMethod and Invoke-WebRequest
Adds an -Token parameter to Invoke-RestMethod and Invoke-WebRequest
Adds an -AllowUnencryptedAuthentication parameter to Invoke-RestMethod and Invoke-WebRequest
Adds tests for various -Authorization uses
-Authentication Parameter has 3 options: Basic, OAuth, and Bearer
Basic requires -Credential and provides RFC-7617 Basic Authorization credentials to the remote server
OAuth and Bearer require the -Token which is a SecureString containing the bearer token to send to the remote server
If any authentication is provided for any transport scheme other than HTTPS, the request will result in an error. A user may use the -AllowUnencryptedAuthentication switch to bypass this behavior and send their secrets unencrypted at their own risk.
-Authentication does not work with -UseDefaultCredentials and will result in an error.
The existing behavior with -Credential is left untouched. When not supplying -Authentication, A user will not receive an error when using -Credential over unencrypted connections.

Code design choice is meant to accommodate more Authentication types in the future.

Documentation Needed

The 3 new parameters will need to be added to the Invoke-RestMethod and Invoke-WebRequest documentation along with examples. Syntax will need to be updated.
This commit is contained in:
Mark Kraus 2017-10-17 21:08:06 -05:00 committed by Travis Plunk
parent 2cc091115b
commit 7c9bddfa3d
3 changed files with 344 additions and 1 deletions

View file

@ -10,6 +10,7 @@ using System.IO;
using System.Text;
using System.Collections;
using System.Globalization;
using System.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
#if !CORECLR
@ -19,6 +20,32 @@ using Microsoft.Win32;
namespace Microsoft.PowerShell.Commands
{
/// <summary>
/// The valid values for the -Authentication parameter for Invoke-RestMethod and Invoke-WebRequest
/// </summary>
public enum WebAuthenticationType
{
/// <summary>
/// No authentication. Default.
/// </summary>
None,
/// <summary>
/// RFC-7617 Basic Authentication. Requires -Credential
/// </summary>
Basic,
/// <summary>
/// RFC-6750 OAuth 2.0 Bearer Authentication. Requires -Token
/// </summary>
Bearer,
/// <summary>
/// RFC-6750 OAuth 2.0 Bearer Authentication. Requires -Token
/// </summary>
OAuth,
}
/// <summary>
/// Base class for Invoke-RestMethod and Invoke-WebRequest commands.
/// </summary>
@ -61,6 +88,22 @@ namespace Microsoft.PowerShell.Commands
#region Authorization and Credentials
/// <summary>
/// Gets or sets the AllowUnencryptedAuthentication property
/// </summary>
[Parameter]
public virtual SwitchParameter AllowUnencryptedAuthentication { get; set; }
/// <summary>
/// Gets or sets the Authentication property used to determin the Authentication method for the web session.
/// Authentication does not work with UseDefaultCredentials.
/// Authentication over unencrypted sessions requires AllowUnencryptedAuthentication.
/// Basic: Requires Credential
/// OAuth/Bearer: Requires Token
/// </summary>
[Parameter]
public virtual WebAuthenticationType Authentication { get; set; } = WebAuthenticationType.None;
/// <summary>
/// gets or sets the Credential property
/// </summary>
@ -94,6 +137,12 @@ namespace Microsoft.PowerShell.Commands
[Parameter]
public virtual SwitchParameter SkipCertificateCheck { get; set; }
/// <summary>
/// Gets or sets the Token property. Token is required by Authentication OAuth and Bearer.
/// </summary>
[Parameter]
public virtual SecureString Token { get; set; }
#endregion
#region Headers
@ -274,6 +323,38 @@ namespace Microsoft.PowerShell.Commands
ThrowTerminatingError(error);
}
// Authentication
if (UseDefaultCredentials && (Authentication != WebAuthenticationType.None))
{
ErrorRecord error = GetValidationError(WebCmdletStrings.AuthenticationConflict,
"WebCmdletAuthenticationConflictException");
ThrowTerminatingError(error);
}
if ((Authentication != WebAuthenticationType.None) && (null != Token) && (null != Credential))
{
ErrorRecord error = GetValidationError(WebCmdletStrings.AuthenticationTokenConflict,
"WebCmdletAuthenticationTokenConflictException");
ThrowTerminatingError(error);
}
if ((Authentication == WebAuthenticationType.Basic) && (null == Credential))
{
ErrorRecord error = GetValidationError(WebCmdletStrings.AuthenticationCredentialNotSupplied,
"WebCmdletAuthenticationCredentialNotSuppliedException");
ThrowTerminatingError(error);
}
if ((Authentication == WebAuthenticationType.OAuth || Authentication == WebAuthenticationType.Bearer) && (null == Token))
{
ErrorRecord error = GetValidationError(WebCmdletStrings.AuthenticationTokenNotSupplied,
"WebCmdletAuthenticationTokenNotSuppliedException");
ThrowTerminatingError(error);
}
if (!AllowUnencryptedAuthentication && (Authentication != WebAuthenticationType.None) && (Uri.Scheme != "https"))
{
ErrorRecord error = GetValidationError(WebCmdletStrings.AllowUnencryptedAuthenticationRequired,
"WebCmdletAllowUnencryptedAuthenticationRequiredException");
ThrowTerminatingError(error);
}
// credentials
if (UseDefaultCredentials && (null != Credential))
{
@ -389,7 +470,7 @@ namespace Microsoft.PowerShell.Commands
//
// handle credentials
//
if (null != Credential)
if (null != Credential && Authentication == WebAuthenticationType.None)
{
// get the relevant NetworkCredential
NetworkCredential netCred = Credential.GetNetworkCredential();
@ -398,6 +479,10 @@ namespace Microsoft.PowerShell.Commands
// supplying a credential overrides the UseDefaultCredentials setting
WebSession.UseDefaultCredentials = false;
}
else if ((null != Credential || null!= Token) && Authentication != WebAuthenticationType.None)
{
ProcessAuthentication();
}
else if (UseDefaultCredentials)
{
WebSession.UseDefaultCredentials = true;
@ -666,6 +751,34 @@ namespace Microsoft.PowerShell.Commands
return (ParameterSetName == "CustomMethod");
}
private string GetBasicAuthorizationHeader()
{
string unencoded = String.Format("{0}:{1}", Credential.UserName, Credential.GetNetworkCredential().Password);
Byte[] bytes = Encoding.UTF8.GetBytes(unencoded);
return String.Format("Basic {0}", Convert.ToBase64String(bytes));
}
private string GetBearerAuthorizationHeader()
{
return String.Format("Bearer {0}", new NetworkCredential(String.Empty, Token).Password);
}
private void ProcessAuthentication()
{
if(Authentication == WebAuthenticationType.Basic)
{
WebSession.Headers["Authorization"] = GetBasicAuthorizationHeader();
}
else if (Authentication == WebAuthenticationType.Bearer || Authentication == WebAuthenticationType.OAuth)
{
WebSession.Headers["Authorization"] = GetBearerAuthorizationHeader();
}
else
{
Diagnostics.Assert(false, String.Format("Unrecognized Authentication value: {0}", Authentication));
}
}
#endregion Helper Methods
}
}

View file

@ -120,6 +120,21 @@
<data name="AccessDenied" xml:space="preserve">
<value>Access to the path '{0}' is denied.</value>
</data>
<data name="AllowUnencryptedAuthenticationRequired" xml:space="preserve">
<value>The cmdlet cannot protect plain text secrets sent over unencrypted connections. To supress this warning and send plain text secrets over unencrypted networks, reissue the command specifying the AllowUnencryptedAuthentication parameter.</value>
</data>
<data name="AuthenticationConflict" xml:space="preserve">
<value>The cmdlet cannot run because the following conflicting parameters are specified: Authentication and UseDefaultCredentials. Authentication does not support Default Credentials. Specify either Authentication or UseDefaultCredentials, then retry.</value>
</data>
<data name="AuthenticationCredentialNotSupplied" xml:space="preserve">
<value>The cmdlet cannot run because the following parameter is not specified: Credential. The supplied Authentication type requires a Credential. Specify Credential, then retry.</value>
</data>
<data name="AuthenticationTokenNotSupplied" xml:space="preserve">
<value>The cmdlet cannot run because the following parameter is not specified: Token. The supplied Authentication type requires a Token. Specify Token, then retry.</value>
</data>
<data name="AuthenticationTokenConflict" xml:space="preserve">
<value>The cmdlet cannot run because the following conflicting parameters are specified: Credential and Token. Specify either Credential or Token, then retry.</value>
</data>
<data name="BodyConflict" xml:space="preserve">
<value>The cmdlet cannot run because the following conflicting parameters are specified: Body and InFile. Specify either Body or Infile, then retry. </value>
</data>

View file

@ -1286,6 +1286,115 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" {
}
}
Context "Invoke-WebRequest -Authentication tests" {
BeforeAll {
#[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Demo/doc/test secret.")]
$token = "testpassword" | ConvertTo-SecureString -AsPlainText -Force
$credential = [pscredential]::new("testuser",$token)
$httpUri = Get-WebListenerUrl -Test 'Get'
$httpsUri = Get-WebListenerUrl -Test 'Get' -Https
$testCases = @(
@{Authentication = "bearer"}
@{Authentication = "OAuth"}
)
}
It "Verifies Invoke-WebRequest -Authentication Basic" {
$params = @{
Uri = $httpsUri
Authentication = "Basic"
Credential = $credential
SkipCertificateCheck = $true
}
$Response = Invoke-WebRequest @params
$result = $response.Content | ConvertFrom-Json
$result.Headers.Authorization | Should BeExactly "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk"
}
It "Verifies Invoke-WebRequest -Authentication <Authentication>" -TestCases $testCases {
param($Authentication)
$params = @{
Uri = $httpsUri
Authentication = $Authentication
Token = $token
SkipCertificateCheck = $true
}
$Response = Invoke-WebRequest @params
$result = $response.Content | ConvertFrom-Json
$result.Headers.Authorization | Should BeExactly "Bearer testpassword"
}
It "Verifies Invoke-WebRequest -Authentication does not support -UseDefaultCredentials" {
$params = @{
Uri = $httpsUri
Token = $token
Authentication = "OAuth"
UseDefaultCredentials = $true
ErrorAction = 'Stop'
SkipCertificateCheck = $true
}
{ Invoke-WebRequest @params } | ShouldBeErrorId "WebCmdletAuthenticationConflictException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand"
}
It "Verifies Invoke-WebRequest -Authentication does not support Both -Credential and -Token" {
$params = @{
Uri = $httpsUri
Token = $token
Credential = $credential
Authentication = "OAuth"
ErrorAction = 'Stop'
SkipCertificateCheck = $true
}
{ Invoke-WebRequest @params } | ShouldBeErrorId "WebCmdletAuthenticationTokenConflictException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand"
}
It "Verifies Invoke-WebRequest -Authentication <Authentication> requires -Token" -TestCases $testCases {
param($Authentication)
$params = @{
Uri = $httpsUri
Authentication = $Authentication
ErrorAction = 'Stop'
SkipCertificateCheck = $true
}
{ Invoke-WebRequest @params } | ShouldBeErrorId "WebCmdletAuthenticationTokenNotSuppliedException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand"
}
It "Verifies Invoke-WebRequest -Authentication Basic requires -Credential" {
$params = @{
Uri = $httpsUri
Authentication = "Basic"
ErrorAction = 'Stop'
SkipCertificateCheck = $true
}
{ Invoke-WebRequest @params } | ShouldBeErrorId "WebCmdletAuthenticationCredentialNotSuppliedException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand"
}
It "Verifies Invoke-WebRequest -Authentication Requires HTTPS" {
$params = @{
Uri = $httpUri
Token = $token
Authentication = "OAuth"
ErrorAction = 'Stop'
}
{ Invoke-WebRequest @params } | ShouldBeErrorId "WebCmdletAllowUnencryptedAuthenticationRequiredException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand"
}
It "Verifies Invoke-WebRequest -Authentication Can use HTTP with -AllowUnencryptedAuthentication" {
$params = @{
Uri = $httpUri
Token = $token
Authentication = "OAuth"
AllowUnencryptedAuthentication = $true
}
$Response = Invoke-WebRequest @params
$result = $response.Content | ConvertFrom-Json
$result.Headers.Authorization | Should BeExactly "Bearer testpassword"
}
}
BeforeEach {
if ($env:http_proxy) {
$savedHttpProxy = $env:http_proxy
@ -2097,6 +2206,112 @@ Describe "Invoke-RestMethod tests" -Tags "Feature" {
}
}
Context "Invoke-RestMethod -Authentication tests" {
BeforeAll {
#[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Demo/doc/test secret.")]
$token = "testpassword" | ConvertTo-SecureString -AsPlainText -Force
$credential = [pscredential]::new("testuser",$token)
$httpUri = Get-WebListenerUrl -Test 'Get'
$httpsUri = Get-WebListenerUrl -Test 'Get' -Https
$testCases = @(
@{Authentication = "bearer"}
@{Authentication = "OAuth"}
)
}
It "Verifies Invoke-RestMethod -Authentication Basic" {
$params = @{
Uri = $httpsUri
Authentication = "Basic"
Credential = $credential
SkipCertificateCheck = $true
}
$result = Invoke-RestMethod @params
$result.Headers.Authorization | Should BeExactly "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk"
}
It "Verifies Invoke-RestMethod -Authentication <Authentication>" -TestCases $testCases {
param($Authentication)
$params = @{
Uri = $httpsUri
Authentication = $Authentication
Token = $token
SkipCertificateCheck = $true
}
$result = Invoke-RestMethod @params
$result.Headers.Authorization | Should BeExactly "Bearer testpassword"
}
It "Verifies Invoke-RestMethod -Authentication does not support -UseDefaultCredentials" {
$params = @{
Uri = $httpsUri
Token = $token
Authentication = "OAuth"
UseDefaultCredentials = $true
ErrorAction = 'Stop'
SkipCertificateCheck = $true
}
{ Invoke-RestMethod @params } | ShouldBeErrorId "WebCmdletAuthenticationConflictException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand"
}
It "Verifies Invoke-RestMethod -Authentication does not support Both -Credential and -Token" {
$params = @{
Uri = $httpsUri
Token = $token
Credential = $credential
Authentication = "OAuth"
ErrorAction = 'Stop'
SkipCertificateCheck = $true
}
{ Invoke-RestMethod @params } | ShouldBeErrorId "WebCmdletAuthenticationTokenConflictException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand"
}
It "Verifies Invoke-RestMethod -Authentication <Authentication> requires -Token" -TestCases $testCases {
param($Authentication)
$params = @{
Uri = $httpsUri
Authentication = $Authentication
ErrorAction = 'Stop'
SkipCertificateCheck = $true
}
{ Invoke-RestMethod @params } | ShouldBeErrorId "WebCmdletAuthenticationTokenNotSuppliedException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand"
}
It "Verifies Invoke-RestMethod -Authentication Basic requires -Credential" {
$params = @{
Uri = $httpsUri
Authentication = "Basic"
ErrorAction = 'Stop'
SkipCertificateCheck = $true
}
{ Invoke-RestMethod @params } | ShouldBeErrorId "WebCmdletAuthenticationCredentialNotSuppliedException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand"
}
It "Verifies Invoke-RestMethod -Authentication Requires HTTPS" {
$params = @{
Uri = $httpUri
Token = $token
Authentication = "OAuth"
ErrorAction = 'Stop'
}
{ Invoke-RestMethod @params } | ShouldBeErrorId "WebCmdletAllowUnencryptedAuthenticationRequiredException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand"
}
It "Verifies Invoke-RestMethod -Authentication Can use HTTP with -AllowUnencryptedAuthentication" {
$params = @{
Uri = $httpUri
Token = $token
Authentication = "OAuth"
AllowUnencryptedAuthentication = $true
}
$result = Invoke-RestMethod @params
$result.Headers.Authorization | Should BeExactly "Bearer testpassword"
}
}
BeforeEach {
if ($env:http_proxy) {
$savedHttpProxy = $env:http_proxy