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();