diff --git a/samples/NativeIISSample/Properties/launchSettings.json b/samples/NativeIISSample/Properties/launchSettings.json index 03665b8024..ca62d6c648 100644 --- a/samples/NativeIISSample/Properties/launchSettings.json +++ b/samples/NativeIISSample/Properties/launchSettings.json @@ -1,6 +1,6 @@ { "iisSettings": { - "windowsAuthentication": false, + "windowsAuthentication": true, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:5762/", diff --git a/samples/NativeIISSample/Startup.cs b/samples/NativeIISSample/Startup.cs index 85f6a5dd51..ef7bbad8ce 100644 --- a/samples/NativeIISSample/Startup.cs +++ b/samples/NativeIISSample/Startup.cs @@ -3,9 +3,11 @@ using System; using System.Linq; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.IISIntegration; namespace NativeIISSample { @@ -13,7 +15,7 @@ namespace NativeIISSample { // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) + public void Configure(IApplicationBuilder app, IHostingEnvironment env, IAuthenticationSchemeProvider authSchemeProvider) { app.Run(async (context) => { @@ -38,8 +40,8 @@ namespace NativeIISSample await context.Response.WriteAsync(Environment.NewLine); await context.Response.WriteAsync("User: " + context.User.Identity.Name + Environment.NewLine); - //var scheme = await authSchemeProvider.GetSchemeAsync(IISDefaults.AuthenticationScheme); - //await context.Response.WriteAsync("DisplayName: " + scheme?.DisplayName + Environment.NewLine); + var scheme = await authSchemeProvider.GetSchemeAsync(IISDefaults.AuthenticationScheme); + await context.Response.WriteAsync("DisplayName: " + scheme?.DisplayName + Environment.NewLine); await context.Response.WriteAsync(Environment.NewLine); diff --git a/samples/NativeIISSample/applicationhost.config b/samples/NativeIISSample/applicationhost.config index b11f212ef2..dd5daa0454 100644 --- a/samples/NativeIISSample/applicationhost.config +++ b/samples/NativeIISSample/applicationhost.config @@ -299,12 +299,12 @@ - + - + diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/NativeMethods.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/NativeMethods.cs index 8015f9c004..4d718793d6 100644 --- a/src/Microsoft.AspNetCore.Server.IISIntegration/NativeMethods.cs +++ b/src/Microsoft.AspNetCore.Server.IISIntegration/NativeMethods.cs @@ -75,7 +75,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration public unsafe static extern bool http_set_managed_context(IntPtr pHttpContext, IntPtr pvManagedContext); [DllImport(AspNetCoreModuleDll)] - public unsafe static extern int http_get_application_paths([MarshalAs(UnmanagedType.BStr)] out string fullPath, [MarshalAs(UnmanagedType.BStr)] out string virtualPath); + public unsafe static extern int http_get_application_properties(ref IISConfigurationData iiConfigData); [DllImport(AspNetCoreModuleDll)] public unsafe static extern bool http_shutdown(); @@ -98,6 +98,9 @@ namespace Microsoft.AspNetCore.Server.IISIntegration [DllImport(AspNetCoreModuleDll)] internal unsafe static extern int http_response_set_known_header(IntPtr pHttpContext, int headerId, byte* pHeaderValue, ushort length, bool fReplace); + [DllImport(AspNetCoreModuleDll)] + public unsafe static extern int http_get_authentication_information(IntPtr pHttpContext, [MarshalAs(UnmanagedType.BStr)] out string authType, out IntPtr token); + [DllImport("kernel32.dll")] public static extern IntPtr GetModuleHandle(string lpModuleName); @@ -105,6 +108,5 @@ namespace Microsoft.AspNetCore.Server.IISIntegration { return GetModuleHandle(AspNetCoreModuleDll) != IntPtr.Zero; } - } } diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISConfigurationData.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISConfigurationData.cs new file mode 100644 index 0000000000..76910ca9d3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISConfigurationData.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.InteropServices; + +namespace Microsoft.AspNetCore.Server.IISIntegration +{ + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct IISConfigurationData + { + [MarshalAs(UnmanagedType.BStr)] + public string pwzFullApplicationPath; + [MarshalAs(UnmanagedType.BStr)] + public string pwzVirtualApplicationPath; + public bool fWindowsAuthEnabled; + public bool fBasicAuthEnabled; + public bool fAnonymousAuthEnable; + } +} diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.FeatureCollection.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.FeatureCollection.cs index 27962f0c08..2c2c9ef5fe 100644 --- a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.FeatureCollection.cs +++ b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.FeatureCollection.cs @@ -7,10 +7,12 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; +using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Primitives; @@ -22,7 +24,8 @@ namespace Microsoft.AspNetCore.Server.IISIntegration IHttpUpgradeFeature, IHttpConnectionFeature, IHttpRequestLifetimeFeature, - IHttpRequestIdentifierFeature + IHttpRequestIdentifierFeature, + IHttpAuthenticationFeature { // NOTE: When feature interfaces are added to or removed from this HttpProtocol implementation, // then the list of `implementedFeatures` in the generated code project MUST also be updated. @@ -223,6 +226,14 @@ namespace Microsoft.AspNetCore.Server.IISIntegration set => TraceIdentifier = value; } + ClaimsPrincipal IHttpAuthenticationFeature.User + { + get => User; + set => User = value; + } + + public IAuthenticationHandler Handler { get; set; } + object IFeatureCollection.this[Type key] { get => FastFeatureGet(key); diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.Features.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.Features.cs index 6b534405f5..5ad7590ade 100644 --- a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.Features.cs +++ b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.Features.cs @@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration private static readonly Type ISessionFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.ISessionFeature); private static readonly Type IHttpBodyControlFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature); private static readonly Type IHttpSendFileFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature); + private static readonly Type IISHttpContextType = typeof(IISHttpContext); private object _currentIHttpRequestFeature; private object _currentIHttpResponseFeature; @@ -61,6 +62,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration _currentIHttpMinRequestBodyDataRateFeature = this; _currentIHttpMinResponseDataRateFeature = this; _currentIHttpBodyControlFeature = this; + _currentIHttpAuthenticationFeature = this; } internal object FastFeatureGet(Type key) @@ -133,6 +135,11 @@ namespace Microsoft.AspNetCore.Server.IISIntegration { return _currentIHttpSendFileFeature; } + if (key == IISHttpContextType) + { + return this; + } + return ExtraFeatureGet(key); } @@ -224,6 +231,10 @@ namespace Microsoft.AspNetCore.Server.IISIntegration { _currentIHttpSendFileFeature = feature; return; + } + if (key == IISHttpContextType) + { + throw new InvalidOperationException("Cannot set IISHttpContext in feature collection"); }; ExtraFeatureSet(key, feature); } diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.cs index a6520425e2..db83adc7cb 100644 --- a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.cs +++ b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.cs @@ -10,9 +10,12 @@ using System.IO; using System.IO.Pipelines; using System.Net; using System.Runtime.InteropServices; +using System.Security.Claims; +using System.Security.Principal; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.AspNetCore.WebUtilities; @@ -57,7 +60,11 @@ namespace Microsoft.AspNetCore.Server.IISIntegration private CurrentOperationType _currentOperationType; private Task _currentOperation = Task.CompletedTask; - internal unsafe IISHttpContext(PipeFactory pipeFactory, IntPtr pHttpContext) + private const string NtlmString = "NTLM"; + private const string NegotiateString = "Negotiate"; + private const string BasicString = "Basic"; + + internal unsafe IISHttpContext(PipeFactory pipeFactory, IntPtr pHttpContext, IISOptions options) : base((HttpApiTypes.HTTP_REQUEST*)NativeMethods.http_get_raw_request(pHttpContext)) { _thisHandle = GCHandle.Alloc(this); @@ -120,6 +127,15 @@ namespace Microsoft.AspNetCore.Server.IISIntegration HttpResponseHeaders = new HeaderCollection(); // TODO Optimize for known headers ResponseHeaders = HttpResponseHeaders; + if (options.ForwardWindowsAuthentication) + { + WindowsUser = GetWindowsPrincipal(); + if (options.AutomaticAuthentication) + { + User = WindowsUser; + } + } + ResetFeatureCollection(); } @@ -146,7 +162,8 @@ namespace Microsoft.AspNetCore.Server.IISIntegration public int LocalPort { get; set; } public string RequestConnectionId { get; set; } public string TraceIdentifier { get; set; } - + public ClaimsPrincipal User { get; set; } + internal WindowsPrincipal WindowsUser { get; set; } public Stream RequestBody { get; set; } public Stream ResponseBody { get; set; } public IPipe Input { get; set; } @@ -827,7 +844,10 @@ namespace Microsoft.AspNetCore.Server.IISIntegration // TODO: dispose managed state (managed objects). _thisHandle.Free(); } - + if (WindowsUser?.Identity is WindowsIdentity wi) + { + wi.Dispose(); + } disposedValue = true; } } @@ -851,5 +871,21 @@ namespace Microsoft.AspNetCore.Server.IISIntegration Write, Flush } + + private WindowsPrincipal GetWindowsPrincipal() + { + var hr = NativeMethods.http_get_authentication_information(_pHttpContext, out var authenticationType, out var token); + + if (hr == 0 && token != IntPtr.Zero && authenticationType != null) + { + if ((authenticationType.Equals(NtlmString, StringComparison.OrdinalIgnoreCase) + || authenticationType.Equals(NegotiateString, StringComparison.OrdinalIgnoreCase) + || authenticationType.Equals(BasicString, StringComparison.OrdinalIgnoreCase))) + { + return new WindowsPrincipal(new WindowsIdentity(token, authenticationType)); + } + } + return null; + } } } diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContextOfT.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContextOfT.cs index 4fbd3b5956..1701122ec6 100644 --- a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContextOfT.cs +++ b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContextOfT.cs @@ -3,9 +3,10 @@ using System; using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting.Server; using System.Threading; using System.IO.Pipelines; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Server; namespace Microsoft.AspNetCore.Server.IISIntegration { @@ -13,8 +14,8 @@ namespace Microsoft.AspNetCore.Server.IISIntegration { private readonly IHttpApplication _application; - public IISHttpContextOfT(PipeFactory pipeFactory, IHttpApplication application, IntPtr pHttpContext) - : base(pipeFactory, pHttpContext) + public IISHttpContextOfT(PipeFactory pipeFactory, IHttpApplication application, IntPtr pHttpContext, IISOptions options) + : base(pipeFactory, pHttpContext, options) { _application = application; } diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpServer.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpServer.cs index 9260293613..4417e54c5b 100644 --- a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpServer.cs +++ b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpServer.cs @@ -6,9 +6,12 @@ using System.IO.Pipelines; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Server.IISIntegration { @@ -19,22 +22,30 @@ namespace Microsoft.AspNetCore.Server.IISIntegration private static NativeMethods.PFN_ASYNC_COMPLETION _onAsyncCompletion = OnAsyncCompletion; private IISContextFactory _iisContextFactory; - private PipeFactory _pipeFactory = new PipeFactory(); private GCHandle _httpServerHandle; - private IApplicationLifetime _applicationLifetime; + private readonly IApplicationLifetime _applicationLifetime; + private readonly IAuthenticationSchemeProvider _authentication; + private readonly IISOptions _options; public IFeatureCollection Features { get; } = new FeatureCollection(); - public IISHttpServer(IApplicationLifetime applicationLifetime) + public IISHttpServer(IApplicationLifetime applicationLifetime, IAuthenticationSchemeProvider authentication, IOptions options) { _applicationLifetime = applicationLifetime; + _authentication = authentication; + _options = options.Value; + + if (_options.ForwardWindowsAuthentication) + { + authentication.AddScheme(new AuthenticationScheme(IISDefaults.AuthenticationScheme, _options.AuthenticationDisplayName, typeof(IISServerAuthenticationHandler))); + } } public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) { _httpServerHandle = GCHandle.Alloc(this); - _iisContextFactory = new IISContextFactory(_pipeFactory, application); + _iisContextFactory = new IISContextFactory(_pipeFactory, application, _options); // Start the server by registering the callback NativeMethods.register_callbacks(_requestHandler, _shutdownHandler, _onAsyncCompletion, (IntPtr)_httpServerHandle, (IntPtr)_httpServerHandle); @@ -115,16 +126,18 @@ namespace Microsoft.AspNetCore.Server.IISIntegration { private readonly IHttpApplication _application; private readonly PipeFactory _pipeFactory; + private readonly IISOptions _options; - public IISContextFactory(PipeFactory pipeFactory, IHttpApplication application) + public IISContextFactory(PipeFactory pipeFactory, IHttpApplication application, IISOptions options) { _application = application; _pipeFactory = pipeFactory; + _options = options; } public IISHttpContext CreateHttpContext(IntPtr pHttpContext) { - return new IISHttpContextOfT(_pipeFactory, _application, pHttpContext); + return new IISHttpContextOfT(_pipeFactory, _application, pHttpContext, _options); } } } diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISServerAuthenticationHandler.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISServerAuthenticationHandler.cs new file mode 100644 index 0000000000..eba243f9f3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISServerAuthenticationHandler.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Server.IISIntegration +{ + public class IISServerAuthenticationHandler : IAuthenticationHandler + { + private HttpContext _context; + private IISHttpContext _iisHttpContext; + + internal AuthenticationScheme Scheme { get; private set; } + + public Task AuthenticateAsync() + { + var user = _iisHttpContext.WindowsUser; + if (user != null && user.Identity != null && user.Identity.IsAuthenticated) + { + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(user, Scheme.Name))); + } + else + { + return Task.FromResult(AuthenticateResult.NoResult()); + } + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + // We would normally set the www-authenticate header here, but IIS does that for us. + _context.Response.StatusCode = 401; + return Task.CompletedTask; + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + _context.Response.StatusCode = 403; + return Task.CompletedTask; + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + _iisHttpContext = context.Features.Get(); + if (_iisHttpContext == null) + { + throw new InvalidOperationException("No IISHttpContext found."); + } + + Scheme = scheme; + _context = context; + + return Task.CompletedTask; + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/WebHostBuilderIISExtensions.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/WebHostBuilderIISExtensions.cs index 8a92946ae1..89ecd56739 100644 --- a/src/Microsoft.AspNetCore.Server.IISIntegration/WebHostBuilderIISExtensions.cs +++ b/src/Microsoft.AspNetCore.Server.IISIntegration/WebHostBuilderIISExtensions.cs @@ -46,19 +46,27 @@ namespace Microsoft.AspNetCore.Hosting hostBuilder.CaptureStartupErrors(true); // TODO consider adding a configuration load where all variables needed are loaded from ANCM in one call. - var hResult = NativeMethods.http_get_application_paths(out var fullPath, out var virtualPath); + var iisConfigData = new IISConfigurationData(); + var hResult = NativeMethods.http_get_application_properties(ref iisConfigData); + var exception = Marshal.GetExceptionForHR(hResult); if (exception != null) { throw exception; } - hostBuilder.UseContentRoot(fullPath); + hostBuilder.UseContentRoot(iisConfigData.pwzFullApplicationPath); return hostBuilder.ConfigureServices(services => { services.AddSingleton(); - services.AddSingleton(new IISServerSetupFilter(virtualPath)); + services.AddSingleton(new IISServerSetupFilter(iisConfigData.pwzVirtualApplicationPath)); services.AddAuthenticationCore(); + services.Configure( + options => + { + options.ForwardWindowsAuthentication = iisConfigData.fWindowsAuthEnabled || iisConfigData.fBasicAuthEnabled; + } + ); }); } diff --git a/test/IISIntegration.FunctionalTests/NtlmAuthentationTest.cs b/test/IISIntegration.FunctionalTests/NtlmAuthentationTest.cs index a61cd3f447..20f2c65c9c 100644 --- a/test/IISIntegration.FunctionalTests/NtlmAuthentationTest.cs +++ b/test/IISIntegration.FunctionalTests/NtlmAuthentationTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. #if NET461 diff --git a/test/IISIntegration.IISServerFunctionalTests/AuthenticationTests.cs b/test/IISIntegration.IISServerFunctionalTests/AuthenticationTests.cs new file mode 100644 index 0000000000..d069b5211e --- /dev/null +++ b/test/IISIntegration.IISServerFunctionalTests/AuthenticationTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if NET461 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace IISIntegration.IISServerFunctionalTests +{ + public class AuthenticationTests : LoggedTest + { + public AuthenticationTests(ITestOutputHelper output) : base(output) + { + } + + [Fact(Skip = "See https://github.com/aspnet/IISIntegration/issues/424")] + public Task Authentication_InProcess_IISExpress() + { + return Authentication(); + } + + private async Task Authentication() + { + var serverType = ServerType.IISExpress; + var architecture = RuntimeArchitecture.x64; + var testName = $"Authentication_{RuntimeFlavor.CoreClr}"; + using (StartLog(out var loggerFactory, testName)) + { + var logger = loggerFactory.CreateLogger("AuthenticationTest"); + + var deploymentParameters = new DeploymentParameters(Helpers.GetTestSitesPath(), serverType, RuntimeFlavor.CoreClr, architecture) + { + ApplicationBaseUriHint = $"http://localhost:5051", + EnvironmentName = "Authentication", // Will pick the Start class named 'StartupAuthentication', + ServerConfigTemplateContent = (serverType == ServerType.IISExpress) ? File.ReadAllText("Http.config") : null, + SiteName = "HttpTestSite", // This is configured in the Http.config + TargetFramework = "netcoreapp2.0", + ApplicationType = ApplicationType.Portable + }; + + using (var deployer = ApplicationDeployerFactory.Create(deploymentParameters, loggerFactory)) + { + var deploymentResult = await deployer.DeployAsync(); + var httpClient = deploymentResult.HttpClient; + deploymentResult.HttpClient.Timeout = TimeSpan.FromSeconds(5); + + // Request to base address and check if various parts of the body are rendered & measure the cold startup time. + var response = await RetryHelper.RetryRequest(() => + { + return deploymentResult.HttpClient.GetAsync(string.Empty); + }, logger, deploymentResult.HostShutdownToken, retryCount: 30); + + var responseText = await response.Content.ReadAsStringAsync(); + try + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Hello World", responseText); + + response = await httpClient.GetAsync("/Anonymous"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Anonymous?True", responseText); + + response = await httpClient.GetAsync("/Restricted"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Contains("NTLM", response.Headers.WwwAuthenticate.ToString()); + Assert.Contains("Negotiate", response.Headers.WwwAuthenticate.ToString()); + + response = await httpClient.GetAsync("/RestrictedNTLM"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Contains("NTLM", response.Headers.WwwAuthenticate.ToString()); + // Note we can't restrict a challenge to a specific auth type, the native auth modules always add themselves. + Assert.Contains("Negotiate", response.Headers.WwwAuthenticate.ToString()); + + response = await httpClient.GetAsync("/Forbidden"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + + var httpClientHandler = new HttpClientHandler() { UseDefaultCredentials = true }; + httpClient = deploymentResult.CreateHttpClient(httpClientHandler); + + response = await httpClient.GetAsync("/Anonymous"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Anonymous?True", responseText); + + response = await httpClient.GetAsync("/Restricted"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(responseText); + } + catch (XunitException) + { + logger.LogWarning(response.ToString()); + logger.LogWarning(responseText); + throw; + } + } + } + } + } +} +#elif NETCOREAPP2_0 +#else +#error Target frameworks need to be updated +#endif diff --git a/test/IISIntegration.IISServerFunctionalTests/HelloWorldTests.cs b/test/IISIntegration.IISServerFunctionalTests/HelloWorldTests.cs index 1411934bf9..610ac8c977 100644 --- a/test/IISIntegration.IISServerFunctionalTests/HelloWorldTests.cs +++ b/test/IISIntegration.IISServerFunctionalTests/HelloWorldTests.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests { } - [Fact( Skip ="See https://github.com/aspnet/IISIntegration/issues/424")] + [Fact(Skip = "See https://github.com/aspnet/IISIntegration/issues/424")] public Task HelloWorld_InProcess_IISExpress_CoreClr_X64_Portable() { return HelloWorld(RuntimeFlavor.CoreClr, ApplicationType.Portable); diff --git a/test/IISIntegration.IISServerFunctionalTests/Http.config b/test/IISIntegration.IISServerFunctionalTests/Http.config index a92444f453..c044b3294c 100644 --- a/test/IISIntegration.IISServerFunctionalTests/Http.config +++ b/test/IISIntegration.IISServerFunctionalTests/Http.config @@ -1026,4 +1026,14 @@ + + + + + + + + + + diff --git a/test/IISTestSite/StartupAuthentication.cs b/test/IISTestSite/StartupAuthentication.cs new file mode 100644 index 0000000000..1306332d1f --- /dev/null +++ b/test/IISTestSite/StartupAuthentication.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Principal; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.IISIntegration; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace IISTestSite +{ + public class StartupAuthentication + { + public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) + { + // Simple error page without depending on Diagnostics. + app.Use(async (context, next) => + { + try + { + await next(); + } + catch (Exception ex) + { + if (context.Response.HasStarted) + { + throw; + } + + context.Response.Clear(); + context.Response.StatusCode = 500; + await context.Response.WriteAsync(ex.ToString()); + } + }); + + app.Use((context, next) => + { + if (context.Request.Path.Equals("/Anonymous")) + { + return context.Response.WriteAsync("Anonymous?" + !context.User.Identity.IsAuthenticated); + } + + if (context.Request.Path.Equals("/Restricted")) + { + if (context.User.Identity.IsAuthenticated) + { + Assert.IsType(context.User); + return context.Response.WriteAsync(context.User.Identity.AuthenticationType); + } + else + { + return context.ChallengeAsync(IISDefaults.AuthenticationScheme); + } + } + + if (context.Request.Path.Equals("/Forbidden")) + { + return context.ForbidAsync(IISDefaults.AuthenticationScheme); + } + + if (context.Request.Path.Equals("/RestrictedNTLM")) + { + if (string.Equals("NTLM", context.User.Identity.AuthenticationType, StringComparison.Ordinal)) + { + return context.Response.WriteAsync("NTLM"); + } + else + { + return context.ChallengeAsync(IISDefaults.AuthenticationScheme); + } + } + + return context.Response.WriteAsync("Hello World"); + }); + } + } +}