diff --git a/src/Servers/HttpSys/src/AuthenticationManager.cs b/src/Servers/HttpSys/src/AuthenticationManager.cs index 7bf734d6ffa8..f40e3ea0d4e8 100644 --- a/src/Servers/HttpSys/src/AuthenticationManager.cs +++ b/src/Servers/HttpSys/src/AuthenticationManager.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; +using Windows.Win32; using Windows.Win32.Networking.HttpServer; namespace Microsoft.AspNetCore.Server.HttpSys; @@ -63,6 +64,24 @@ public bool AllowAnonymous /// public string? AuthenticationDisplayName { get; set; } + /// + /// If true, the Kerberos authentication credentials are persisted per connection + /// and re-used for subsequent anonymous requests on the same connection. + /// Kerberos or Negotiate authentication must be enabled. The default is false. + /// This option maps to the native HTTP_AUTH_EX_FLAG_ENABLE_KERBEROS_CREDENTIAL_CACHING flag. + /// + /// + public bool EnableKerberosCredentialCaching { get; set; } + + /// + /// If true, the server captures user credentials from the thread that starts the + /// host and impersonates that user during Kerberos or Negotiate authentication. + /// Kerberos or Negotiate authentication must be enabled. The default is false. + /// This option maps to the native HTTP_AUTH_EX_FLAG_CAPTURE_CREDENTIAL flag. + /// + /// + public bool CaptureCredentials { get; set; } + internal void SetUrlGroupSecurity(UrlGroup urlGroup) { Debug.Assert(_urlGroup == null, "SetUrlGroupSecurity called more than once."); @@ -85,18 +104,39 @@ private unsafe void SetUrlGroupSecurity() { authInfo.AuthSchemes = (uint)_authSchemes; + authInfo.ExFlags = 0; + + if (EnableKerberosCredentialCaching) + { + authInfo.ExFlags |= (byte)PInvoke.HTTP_AUTH_EX_FLAG_ENABLE_KERBEROS_CREDENTIAL_CACHING; + } + + if (CaptureCredentials) + { + authInfo.ExFlags |= (byte)PInvoke.HTTP_AUTH_EX_FLAG_CAPTURE_CREDENTIAL; + } + // TODO: // NTLM auth sharing (on by default?) DisableNTLMCredentialCaching - // Kerberos auth sharing (off by default?) HTTP_AUTH_EX_FLAG_ENABLE_KERBEROS_CREDENTIAL_CACHING // Mutual Auth - ReceiveMutualAuth // Digest domain and realm - HTTP_SERVER_AUTHENTICATION_DIGEST_PARAMS // Basic realm - HTTP_SERVER_AUTHENTICATION_BASIC_PARAMS + HTTP_SERVER_PROPERTY property; + if (authInfo.ExFlags != 0) + { + // We need to modify extended fields such as ExFlags, set the extended auth property. + property = HTTP_SERVER_PROPERTY.HttpServerExtendedAuthenticationProperty; + } + else + { + // Otherwise set the regular auth property. + property = HTTP_SERVER_PROPERTY.HttpServerAuthenticationProperty; + } + IntPtr infoptr = new IntPtr(&authInfo); - _urlGroup.SetProperty( - HTTP_SERVER_PROPERTY.HttpServerAuthenticationProperty, - infoptr, (uint)AuthInfoSize); + _urlGroup.SetProperty(property, infoptr, (uint)AuthInfoSize); } } diff --git a/src/Servers/HttpSys/src/NativeMethods.txt b/src/Servers/HttpSys/src/NativeMethods.txt index 548306dfeade..3d6f700d6101 100644 --- a/src/Servers/HttpSys/src/NativeMethods.txt +++ b/src/Servers/HttpSys/src/NativeMethods.txt @@ -5,6 +5,7 @@ CloseHandle FILE_SKIP_COMPLETION_PORT_ON_SUCCESS FILE_SKIP_SET_EVENT_ON_HANDLE +HTTP_AUTH_EX_FLAG_* HTTP_AUTH_STATUS HTTP_BINDING_INFO HTTP_CACHE_POLICY diff --git a/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt b/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..f68a65d79547 100644 --- a/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt +++ b/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt @@ -1 +1,5 @@ #nullable enable +Microsoft.AspNetCore.Server.HttpSys.AuthenticationManager.CaptureCredentials.get -> bool +Microsoft.AspNetCore.Server.HttpSys.AuthenticationManager.CaptureCredentials.set -> void +Microsoft.AspNetCore.Server.HttpSys.AuthenticationManager.EnableKerberosCredentialCaching.get -> bool +Microsoft.AspNetCore.Server.HttpSys.AuthenticationManager.EnableKerberosCredentialCaching.set -> void diff --git a/src/Servers/HttpSys/test/FunctionalTests/AuthenticationTests.cs b/src/Servers/HttpSys/test/FunctionalTests/AuthenticationTests.cs index f82e870c3e2d..cbafac88e448 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/AuthenticationTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/AuthenticationTests.cs @@ -400,6 +400,60 @@ public async Task AuthTypes_DisableAutomaticAuthentication(AuthenticationSchemes } } + [ConditionalTheory] + [InlineData(AuthenticationSchemes.Negotiate)] + [InlineData(AuthenticationSchemes.NTLM)] + [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM)] + public async Task AuthTypes_EnableKerberosCredentialCaching(AuthenticationSchemes authType) + { + using (var server = Utilities.CreateDynamicHost(out var address, options => + { + options.Authentication.Schemes = authType; + options.Authentication.AllowAnonymous = DenyAnoymous; + options.Authentication.EnableKerberosCredentialCaching = true; + }, + httpContext => + { + // There doesn't seem to be a simple way of testing the `EnableKerberosCredentialCaching` + // setting, but at least check that the server works. + Assert.NotNull(httpContext.User); + Assert.NotNull(httpContext.User.Identity); + Assert.True(httpContext.User.Identity.IsAuthenticated); + return Task.FromResult(0); + }, LoggerFactory)) + { + var response = await SendRequestAsync(address, useDefaultCredentials: true); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + } + + [ConditionalTheory] + [InlineData(AuthenticationSchemes.Negotiate)] + [InlineData(AuthenticationSchemes.NTLM)] + [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM)] + public async Task AuthTypes_CaptureCredentials(AuthenticationSchemes authType) + { + using (var server = Utilities.CreateDynamicHost(out var address, options => + { + options.Authentication.Schemes = authType; + options.Authentication.AllowAnonymous = DenyAnoymous; + options.Authentication.CaptureCredentials = true; + }, + httpContext => + { + // There doesn't seem to be a simple way of testing the `CaptureCredentials` + // setting, but at least check that the server works. + Assert.NotNull(httpContext.User); + Assert.NotNull(httpContext.User.Identity); + Assert.True(httpContext.User.Identity.IsAuthenticated); + return Task.FromResult(0); + }, LoggerFactory)) + { + var response = await SendRequestAsync(address, useDefaultCredentials: true); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + } + private async Task SendRequestAsync(string uri, bool useDefaultCredentials = false) { HttpClientHandler handler = new HttpClientHandler();