diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs
index e9eb8f77e561..a63d0e4c39bc 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs
@@ -10,6 +10,7 @@
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 @@
namespace Microsoft.PowerShell.Commands
{
+ ///
+ /// The valid values for the -Authentication parameter for Invoke-RestMethod and Invoke-WebRequest
+ ///
+ public enum WebAuthenticationType
+ {
+ ///
+ /// No authentication. Default.
+ ///
+ None,
+
+ ///
+ /// RFC-7617 Basic Authentication. Requires -Credential
+ ///
+ Basic,
+
+ ///
+ /// RFC-6750 OAuth 2.0 Bearer Authentication. Requires -Token
+ ///
+ Bearer,
+
+ ///
+ /// RFC-6750 OAuth 2.0 Bearer Authentication. Requires -Token
+ ///
+ OAuth,
+ }
+
///
/// Base class for Invoke-RestMethod and Invoke-WebRequest commands.
///
@@ -61,6 +88,22 @@ public abstract partial class WebRequestPSCmdlet : PSCmdlet
#region Authorization and Credentials
+ ///
+ /// Gets or sets the AllowUnencryptedAuthentication property
+ ///
+ [Parameter]
+ public virtual SwitchParameter AllowUnencryptedAuthentication { get; set; }
+
+ ///
+ /// 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
+ ///
+ [Parameter]
+ public virtual WebAuthenticationType Authentication { get; set; } = WebAuthenticationType.None;
+
///
/// gets or sets the Credential property
///
@@ -94,6 +137,12 @@ public abstract partial class WebRequestPSCmdlet : PSCmdlet
[Parameter]
public virtual SwitchParameter SkipCertificateCheck { get; set; }
+ ///
+ /// Gets or sets the Token property. Token is required by Authentication OAuth and Bearer.
+ ///
+ [Parameter]
+ public virtual SecureString Token { get; set; }
+
#endregion
#region Headers
@@ -274,6 +323,38 @@ internal virtual void ValidateParameters()
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 @@ internal virtual void PrepareSession()
//
// handle credentials
//
- if (null != Credential)
+ if (null != Credential && Authentication == WebAuthenticationType.None)
{
// get the relevant NetworkCredential
NetworkCredential netCred = Credential.GetNetworkCredential();
@@ -398,6 +479,10 @@ internal virtual void PrepareSession()
// 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 @@ private bool IsCustomMethodSet()
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
}
}
diff --git a/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx b/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx
index 17fccac5f44b..5ee6d6baf8ce 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx
+++ b/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx
@@ -120,6 +120,21 @@
Access to the path '{0}' is denied.
+
+ 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.
+
+
+ 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.
+
+
+ The cmdlet cannot run because the following parameter is not specified: Credential. The supplied Authentication type requires a Credential. Specify Credential, then retry.
+
+
+ The cmdlet cannot run because the following parameter is not specified: Token. The supplied Authentication type requires a Token. Specify Token, then retry.
+
+
+ The cmdlet cannot run because the following conflicting parameters are specified: Credential and Token. Specify either Credential or Token, then retry.
+
The cmdlet cannot run because the following conflicting parameters are specified: Body and InFile. Specify either Body or Infile, then retry.
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1
index a91875f8e082..927f2151c584 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1
@@ -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 " -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 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 " -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 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