diff --git a/.spelling b/.spelling index 802cdb665f7..3207ea808fd 100644 --- a/.spelling +++ b/.spelling @@ -1239,5 +1239,7 @@ v6.0. #region test/tools/WebListener/README.md Overrides - test/tools/WebListener/README.md +Auth +NTLM ResponseHeaders #endregion 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 69542eb16ef..1b142f273f4 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 @@ -354,6 +354,12 @@ internal virtual void ValidateParameters() "WebCmdletAllowUnencryptedAuthenticationRequiredException"); ThrowTerminatingError(error); } + if (!AllowUnencryptedAuthentication && (null != Credential || UseDefaultCredentials) && (Uri.Scheme != "https")) + { + ErrorRecord error = GetValidationError(WebCmdletStrings.AllowUnencryptedAuthenticationRequired, + "WebCmdletAllowUnencryptedAuthenticationRequiredException"); + ThrowTerminatingError(error); + } // credentials if (UseDefaultCredentials && (null != Credential)) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 index 8dc1b1aaaef..c4560daae04 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 @@ -1312,6 +1312,8 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" { $credential = [pscredential]::new("testuser",$token) $httpUri = Get-WebListenerUrl -Test 'Get' $httpsUri = Get-WebListenerUrl -Test 'Get' -Https + $httpBasicUri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Basic' + $httpsBasicUri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Basic' -Https $testCases = @( @{Authentication = "bearer"} @{Authentication = "OAuth"} @@ -1412,6 +1414,83 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" { $result.Headers.Authorization | Should BeExactly "Bearer testpassword" } + + It "Verifies Invoke-WebRequest Negotiated -Credential over HTTPS" { + $params = @{ + Uri = $httpsBasicUri + Credential = $credential + SkipCertificateCheck = $true + } + $Response = Invoke-WebRequest @params + $result = $response.Content | ConvertFrom-Json + + $result.Headers.Authorization | Should BeExactly "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" + } + + It "Verifies Invoke-WebRequest Negotiated -Credential Requires HTTPS" { + $params = @{ + Uri = $httpBasicUri + Credential = $credential + ErrorAction = 'Stop' + } + { Invoke-WebRequest @params } | ShouldBeErrorId "WebCmdletAllowUnencryptedAuthenticationRequiredException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" + } + + It "Verifies Invoke-WebRequest Negotiated -Credential Can use HTTP with -AllowUnencryptedAuthentication" { + $params = @{ + Uri = $httpBasicUri + Credential = $credential + AllowUnencryptedAuthentication = $true + } + $Response = Invoke-WebRequest @params + $result = $response.Content | ConvertFrom-Json + + $result.Headers.Authorization | Should BeExactly "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" + } + + # UseDefaultCredentials is only reliably testable on Windows + It "Verifies Invoke-WebRequest Negotiated -UseDefaultCredentials with '' over HTTPS" -Skip:$(!$IsWindows) -TestCases @( + @{AuthType = 'NTLM'} + @{AuthType = 'Negotiate'} + ) { + param($AuthType) + $params = @{ + Uri = Get-WebListenerUrl -Test 'Auth' -TestValue $AuthType -Https + UseDefaultCredentials = $true + SkipCertificateCheck = $true + } + $Response = Invoke-WebRequest @params + $result = $response.Content | ConvertFrom-Json + + $result.Headers.Authorization | Should Match "^$AuthType " + } + + # The error condition can at least be tested on all platforms. + It "Verifies Invoke-WebRequest Negotiated -UseDefaultCredentials Requires HTTPS" { + $params = @{ + Uri = $httpUri + UseDefaultCredentials = $true + ErrorAction = 'Stop' + } + { Invoke-WebRequest @params } | ShouldBeErrorId "WebCmdletAllowUnencryptedAuthenticationRequiredException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" + } + + # UseDefaultCredentials is only reliably testable on Windows + It "Verifies Invoke-WebRequest Negotiated -UseDefaultCredentials with '' Can use HTTP with -AllowUnencryptedAuthentication" -Skip:$(!$IsWindows) -TestCases @( + @{AuthType = 'NTLM'} + @{AuthType = 'Negotiate'} + ) { + param($AuthType) + $params = @{ + Uri = Get-WebListenerUrl -Test 'Auth' -TestValue $AuthType + UseDefaultCredentials = $true + AllowUnencryptedAuthentication = $true + } + $Response = Invoke-WebRequest @params + $result = $response.Content | ConvertFrom-Json + + $result.Headers.Authorization | Should Match "^$AuthType " + } } BeforeEach { @@ -2233,6 +2312,8 @@ Describe "Invoke-RestMethod tests" -Tags "Feature" { $credential = [pscredential]::new("testuser",$token) $httpUri = Get-WebListenerUrl -Test 'Get' $httpsUri = Get-WebListenerUrl -Test 'Get' -Https + $httpBasicUri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Basic' + $httpsBasicUri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Basic' -Https $testCases = @( @{Authentication = "bearer"} @{Authentication = "OAuth"} @@ -2330,6 +2411,79 @@ Describe "Invoke-RestMethod tests" -Tags "Feature" { $result.Headers.Authorization | Should BeExactly "Bearer testpassword" } + + It "Verifies Invoke-RestMethod Negotiated -Credential over HTTPS" { + $params = @{ + Uri = $httpsBasicUri + Credential = $credential + SkipCertificateCheck = $true + } + $result = Invoke-RestMethod @params + + $result.Headers.Authorization | Should BeExactly "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" + } + + It "Verifies Invoke-RestMethod Negotiated -Credential Requires HTTPS" { + $params = @{ + Uri = $httpBasicUri + Credential = $credential + ErrorAction = 'Stop' + } + { Invoke-RestMethod @params } | ShouldBeErrorId "WebCmdletAllowUnencryptedAuthenticationRequiredException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" + } + + It "Verifies Invoke-RestMethod Negotiated -Credential Can use HTTP with -AllowUnencryptedAuthentication" { + $params = @{ + Uri = $httpBasicUri + Credential = $credential + AllowUnencryptedAuthentication = $true + } + $result = Invoke-RestMethod @params + + $result.Headers.Authorization | Should BeExactly "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" + } + + # UseDefaultCredentials is only reliably testable on Windows + It "Verifies Invoke-RestMethod Negotiated -UseDefaultCredentials with '' over HTTPS" -Skip:$(!$IsWindows) -TestCases @( + @{AuthType = 'NTLM'} + @{AuthType = 'Negotiate'} + ) { + param($AuthType) + $params = @{ + Uri = Get-WebListenerUrl -Test 'Auth' -TestValue $AuthType -Https + UseDefaultCredentials = $true + SkipCertificateCheck = $true + } + $result = Invoke-RestMethod @params + + $result.Headers.Authorization | Should Match "^$AuthType " + } + + # The error condition can at least be tested on all platforms. + It "Verifies Invoke-RestMethod Negotiated -UseDefaultCredentials Requires HTTPS" { + $params = @{ + Uri = $httpUri + UseDefaultCredentials = $true + ErrorAction = 'Stop' + } + { Invoke-RestMethod @params } | ShouldBeErrorId "WebCmdletAllowUnencryptedAuthenticationRequiredException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" + } + + # UseDefaultCredentials is only reliably testable on Windows + It "Verifies Invoke-RestMethod Negotiated -UseDefaultCredentials with '' Can use HTTP with -AllowUnencryptedAuthentication" -Skip:$(!$IsWindows) -TestCases @( + @{AuthType = 'NTLM'} + @{AuthType = 'Negotiate'} + ) { + param($AuthType) + $params = @{ + Uri = Get-WebListenerUrl -Test 'Auth' -TestValue $AuthType + UseDefaultCredentials = $true + AllowUnencryptedAuthentication = $true + } + $result = Invoke-RestMethod @params + + $result.Headers.Authorization | Should Match "^$AuthType " + } } BeforeEach { diff --git a/test/tools/Modules/WebListener/WebListener.psm1 b/test/tools/Modules/WebListener/WebListener.psm1 index a3125d07c6b..3aa83c7a81e 100644 --- a/test/tools/Modules/WebListener/WebListener.psm1 +++ b/test/tools/Modules/WebListener/WebListener.psm1 @@ -113,6 +113,7 @@ function Get-WebListenerUrl { param ( [switch]$Https, [ValidateSet( + 'Auth', 'Cert', 'Compression', 'Delay', diff --git a/test/tools/WebListener/Controllers/AuthController.cs b/test/tools/WebListener/Controllers/AuthController.cs new file mode 100644 index 00000000000..c75eff0b7e7 --- /dev/null +++ b/test/tools/WebListener/Controllers/AuthController.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.Primitives; +using mvc.Models; + +namespace mvc.Controllers +{ + public class AuthController : Controller + { + public JsonResult Basic() + { + StringValues authorization; + if (Request.Headers.TryGetValue("Authorization", out authorization)) + { + var getController = new GetController(); + getController.ControllerContext = this.ControllerContext; + return getController.Index(); + } + else + { + Response.Headers.Add("WWW-Authenticate","Basic realm=\"WebListener\""); + Response.StatusCode = 401; + return Json("401 Unauthorized"); + } + } + + public JsonResult Negotiate() + { + StringValues authorization; + if (Request.Headers.TryGetValue("Authorization", out authorization)) + { + var getController = new GetController(); + getController.ControllerContext = this.ControllerContext; + return getController.Index(); + } + else + { + Response.Headers.Add("WWW-Authenticate","Negotiate"); + Response.StatusCode = 401; + return Json("401 Unauthorized"); + } + } + + public JsonResult Ntlm() + { + StringValues authorization; + if (Request.Headers.TryGetValue("Authorization", out authorization)) + { + var getController = new GetController(); + getController.ControllerContext = this.ControllerContext; + return getController.Index(); + } + else + { + Response.Headers.Add("WWW-Authenticate","NTLM"); + Response.StatusCode = 401; + return Json("401 Unauthorized"); + } + } + + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } + } +} diff --git a/test/tools/WebListener/README.md b/test/tools/WebListener/README.md index 04dbd20ccb5..5aca1cc4def 100644 --- a/test/tools/WebListener/README.md +++ b/test/tools/WebListener/README.md @@ -34,6 +34,76 @@ $Listener = Start-WebListener -HttpPort 8083 -HttpsPort 8084 Returns a static HTML page containing links and descriptions of the available tests in WebListener. This can be used as a default or general test where no specific test functionality or return data is required. +## /Auth/Basic/ + +Provides a mock Basic authentication challenge. If a basic authorization header is sent, then the same results as /Get/ are returned. + +```powershell +$credential = Get-Credential +$uri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Basic' -Https +Invoke-RestMethod -Uri $uri -Credential $credential -SkipCertificateCheck +``` + +```json +{ + "headers":{ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.15063; en-US) PowerShell/6.0.0", + "Connection": "Keep-Alive", + "Authorization": "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk", + "Host": "localhost:8084" + }, + "origin": "127.0.0.1", + "args": {}, + "url": "https://localhost:8084/Auth/Basic" +} +``` + +## /Auth/Negotiate/ + +Provides a mock Negotiate authentication challenge. If a basic authorization header is sent, then the same results as /Get/ are returned. + +```powershell +$uri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Negotiate' -Https +Invoke-RestMethod -Uri $uri -UseDefaultCredential -SkipCertificateCheck +``` + +```json +{ + "headers":{ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.15063; en-US) PowerShell/6.0.0", + "Connection": "Keep-Alive", + "Authorization": "Negotiate jjaguasgtisi7tiqkagasjjajvs", + "Host": "localhost:8084" + }, + "origin": "127.0.0.1", + "args": {}, + "url": "https://localhost:8084/Auth/Negotiate" +} +``` + +## /Auth/NTLM/ + +Provides a mock NTLM authentication challenge. If a basic authorization header is sent, then the same results as /Get/ are returned. + +```powershell +$uri = Get-WebListenerUrl -Test 'Auth' -TestValue 'NTLM' -Https +Invoke-RestMethod -Uri $uri -UseDefaultCredential -SkipCertificateCheck +``` + +```json +{ + "headers":{ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.15063; en-US) PowerShell/6.0.0", + "Connection": "Keep-Alive", + "Authorization": "NTLM jjaguasgtisi7tiqkagasjjajvs", + "Host": "localhost:8084" + }, + "origin": "127.0.0.1", + "args": {}, + "url": "https://localhost:8084/Auth/NTLM" +} +``` + ## /Cert/ Returns a JSON object containing the details of the Client Certificate if one is provided in the request. diff --git a/test/tools/WebListener/Views/Home/Index.cshtml b/test/tools/WebListener/Views/Home/Index.cshtml index 6c88ce5e0a3..13699a36b33 100644 --- a/test/tools/WebListener/Views/Home/Index.cshtml +++ b/test/tools/WebListener/Views/Home/Index.cshtml @@ -1,6 +1,9 @@ 

Available Tests