Use LDAP support from DirectoryServices.Protocols for RBAC claim resolution on Linux for Negotiate (#25075)
This commit is contained in:
parent
c2f0331805
commit
098be5f5ee
|
|
@ -64,6 +64,7 @@ and are generated based on the last package release.
|
||||||
<LatestPackageReference Include="System.ComponentModel.Annotations" />
|
<LatestPackageReference Include="System.ComponentModel.Annotations" />
|
||||||
<LatestPackageReference Include="System.Diagnostics.DiagnosticSource" />
|
<LatestPackageReference Include="System.Diagnostics.DiagnosticSource" />
|
||||||
<LatestPackageReference Include="System.Diagnostics.EventLog" />
|
<LatestPackageReference Include="System.Diagnostics.EventLog" />
|
||||||
|
<LatestPackageReference Include="System.DirectoryServices.Protocols" />
|
||||||
<LatestPackageReference Include="System.Drawing.Common" />
|
<LatestPackageReference Include="System.Drawing.Common" />
|
||||||
<LatestPackageReference Include="System.IO.Pipelines" />
|
<LatestPackageReference Include="System.IO.Pipelines" />
|
||||||
<LatestPackageReference Include="System.Net.Http" />
|
<LatestPackageReference Include="System.Net.Http" />
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,10 @@
|
||||||
<Uri>https://github.com/dotnet/runtime</Uri>
|
<Uri>https://github.com/dotnet/runtime</Uri>
|
||||||
<Sha>f4e99f4afa445b519abcd7c5c87cbf54771614db</Sha>
|
<Sha>f4e99f4afa445b519abcd7c5c87cbf54771614db</Sha>
|
||||||
</Dependency>
|
</Dependency>
|
||||||
|
<Dependency Name="System.DirectoryServices.Protocols" Version="5.0.0-rc.1.20425.1">
|
||||||
|
<Uri>https://github.com/dotnet/runtime</Uri>
|
||||||
|
<Sha>f4e99f4afa445b519abcd7c5c87cbf54771614db</Sha>
|
||||||
|
</Dependency>
|
||||||
<Dependency Name="System.Drawing.Common" Version="5.0.0-rc.1.20425.1">
|
<Dependency Name="System.Drawing.Common" Version="5.0.0-rc.1.20425.1">
|
||||||
<Uri>https://github.com/dotnet/runtime</Uri>
|
<Uri>https://github.com/dotnet/runtime</Uri>
|
||||||
<Sha>f4e99f4afa445b519abcd7c5c87cbf54771614db</Sha>
|
<Sha>f4e99f4afa445b519abcd7c5c87cbf54771614db</Sha>
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@
|
||||||
<SystemComponentModelAnnotationsPackageVersion>5.0.0-rc.1.20425.1</SystemComponentModelAnnotationsPackageVersion>
|
<SystemComponentModelAnnotationsPackageVersion>5.0.0-rc.1.20425.1</SystemComponentModelAnnotationsPackageVersion>
|
||||||
<SystemDiagnosticsDiagnosticSourcePackageVersion>5.0.0-rc.1.20425.1</SystemDiagnosticsDiagnosticSourcePackageVersion>
|
<SystemDiagnosticsDiagnosticSourcePackageVersion>5.0.0-rc.1.20425.1</SystemDiagnosticsDiagnosticSourcePackageVersion>
|
||||||
<SystemDiagnosticsEventLogPackageVersion>5.0.0-rc.1.20425.1</SystemDiagnosticsEventLogPackageVersion>
|
<SystemDiagnosticsEventLogPackageVersion>5.0.0-rc.1.20425.1</SystemDiagnosticsEventLogPackageVersion>
|
||||||
|
<SystemDirectoryServicesProtocolsPackageVersion>5.0.0-rc.1.20425.1</SystemDirectoryServicesProtocolsPackageVersion>
|
||||||
<SystemDrawingCommonPackageVersion>5.0.0-rc.1.20425.1</SystemDrawingCommonPackageVersion>
|
<SystemDrawingCommonPackageVersion>5.0.0-rc.1.20425.1</SystemDrawingCommonPackageVersion>
|
||||||
<SystemIOPipelinesPackageVersion>5.0.0-rc.1.20425.1</SystemIOPipelinesPackageVersion>
|
<SystemIOPipelinesPackageVersion>5.0.0-rc.1.20425.1</SystemIOPipelinesPackageVersion>
|
||||||
<SystemNetHttpJsonPackageVersion>5.0.0-rc.1.20425.1</SystemNetHttpJsonPackageVersion>
|
<SystemNetHttpJsonPackageVersion>5.0.0-rc.1.20425.1</SystemNetHttpJsonPackageVersion>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authentication.Negotiate;
|
using Microsoft.AspNetCore.Authentication.Negotiate;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
|
@ -22,6 +23,23 @@ namespace NegotiateAuthSample
|
||||||
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
|
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
|
||||||
.AddNegotiate(options =>
|
.AddNegotiate(options =>
|
||||||
{
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
options.EnableLdap("DOMAIN.net");
|
||||||
|
|
||||||
|
options.EnableLdap(settings =>
|
||||||
|
{
|
||||||
|
// Mandatory settings
|
||||||
|
settings.Domain = "DOMAIN.com";
|
||||||
|
// Optional settings
|
||||||
|
settings.MachineAccountName = "machineName";
|
||||||
|
settings.MachineAccountPassword = "PassW0rd";
|
||||||
|
settings.IgnoreNestedGroups = true;
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
options.Events = new NegotiateEvents()
|
options.Events = new NegotiateEvents()
|
||||||
{
|
{
|
||||||
OnAuthenticationFailed = context =>
|
OnAuthenticationFailed = context =>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
// 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 Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Authentication.Negotiate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// State for the RetrieveLdapClaims event.
|
||||||
|
/// </summary>
|
||||||
|
public class LdapContext : ResultContext<NegotiateOptions>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="LdapContext"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
/// <param name="scheme"></param>
|
||||||
|
/// <param name="options"></param>
|
||||||
|
/// <param name="settings"></param>
|
||||||
|
public LdapContext(
|
||||||
|
HttpContext context,
|
||||||
|
AuthenticationScheme scheme,
|
||||||
|
NegotiateOptions options,
|
||||||
|
LdapSettings settings)
|
||||||
|
: base(context, scheme, options)
|
||||||
|
{
|
||||||
|
LdapSettings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The LDAP settings to use for the RetrieveLdapClaims event.
|
||||||
|
/// </summary>
|
||||||
|
public LdapSettings LdapSettings { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,12 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<AuthenticationFailedContext, Task> OnAuthenticationFailed { get; set; } = context => Task.CompletedTask;
|
public Func<AuthenticationFailedContext, Task> OnAuthenticationFailed { get; set; } = context => Task.CompletedTask;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked after the authentication before ClaimsIdentity is populated with claims retrieved through the LDAP connection.
|
||||||
|
/// This event is invoked when <see cref="LdapSettings.EnableLdapClaimResolution"/> is set to true on <see cref="LdapSettings"/>.
|
||||||
|
/// </summary>
|
||||||
|
public Func<LdapContext, Task> OnRetrieveLdapClaims { get; set; } = context => Task.CompletedTask;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invoked after the authentication is complete and a ClaimsIdentity has been generated.
|
/// Invoked after the authentication is complete and a ClaimsIdentity has been generated.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -31,6 +37,11 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context);
|
public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked after the authentication before ClaimsIdentity is populated with claims retrieved through the LDAP connection.
|
||||||
|
/// </summary>
|
||||||
|
public virtual Task RetrieveLdapClaims(LdapContext context) => OnRetrieveLdapClaims(context);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invoked after the authentication is complete and a ClaimsIdentity has been generated.
|
/// Invoked after the authentication is complete and a ClaimsIdentity has been generated.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
// 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.DirectoryServices.Protocols;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Authentication.Negotiate
|
||||||
|
{
|
||||||
|
internal static class LdapAdapter
|
||||||
|
{
|
||||||
|
public static async Task RetrieveClaimsAsync(LdapSettings settings, ClaimsIdentity identity, ILogger logger)
|
||||||
|
{
|
||||||
|
var user = identity.Name;
|
||||||
|
var userAccountName = user.Substring(0, user.IndexOf('@'));
|
||||||
|
var distinguishedName = settings.Domain.Split('.').Select(name => $"dc={name}").Aggregate((a, b) => $"{a},{b}");
|
||||||
|
|
||||||
|
var filter = $"(&(objectClass=user)(sAMAccountName={userAccountName}))"; // This is using ldap search query language, it is looking on the server for someUser
|
||||||
|
var searchRequest = new SearchRequest(distinguishedName, filter, SearchScope.Subtree, null);
|
||||||
|
var searchResponse = (SearchResponse) await Task<DirectoryResponse>.Factory.FromAsync(
|
||||||
|
settings.LdapConnection.BeginSendRequest,
|
||||||
|
settings.LdapConnection.EndSendRequest,
|
||||||
|
searchRequest,
|
||||||
|
PartialResultProcessing.NoPartialResultSupport,
|
||||||
|
null);
|
||||||
|
|
||||||
|
if (searchResponse.Entries.Count > 0)
|
||||||
|
{
|
||||||
|
if (searchResponse.Entries.Count > 1)
|
||||||
|
{
|
||||||
|
logger.LogWarning($"More than one response received for query: {filter} with distinguished name: {distinguishedName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var userFound = searchResponse.Entries[0]; //Get the object that was found on ldap
|
||||||
|
var memberof = userFound.Attributes["memberof"]; // You can access ldap Attributes with Attributes property
|
||||||
|
|
||||||
|
foreach (var group in memberof)
|
||||||
|
{
|
||||||
|
// Example distinguished name: CN=TestGroup,DC=KERB,DC=local
|
||||||
|
var groupDN = $"{Encoding.UTF8.GetString((byte[])group)}";
|
||||||
|
var groupCN = groupDN.Split(',')[0].Substring("CN=".Length);
|
||||||
|
|
||||||
|
if (!settings.IgnoreNestedGroups)
|
||||||
|
{
|
||||||
|
GetNestedGroups(settings.LdapConnection, identity, distinguishedName, groupCN, logger);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddRole(identity, groupCN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning($"No response received for query: {filter} with distinguished name: {distinguishedName}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void GetNestedGroups(LdapConnection connection, ClaimsIdentity principal, string distinguishedName, string groupCN, ILogger logger)
|
||||||
|
{
|
||||||
|
var filter = $"(&(objectClass=group)(sAMAccountName={groupCN}))"; // This is using ldap search query language, it is looking on the server for someUser
|
||||||
|
var searchRequest = new SearchRequest(distinguishedName, filter, System.DirectoryServices.Protocols.SearchScope.Subtree, null);
|
||||||
|
var searchResponse = (SearchResponse)connection.SendRequest(searchRequest);
|
||||||
|
|
||||||
|
if (searchResponse.Entries.Count > 0)
|
||||||
|
{
|
||||||
|
if (searchResponse.Entries.Count > 1)
|
||||||
|
{
|
||||||
|
logger.LogWarning($"More than one response received for query: {filter} with distinguished name: {distinguishedName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var group = searchResponse.Entries[0]; //Get the object that was found on ldap
|
||||||
|
string name = group.DistinguishedName;
|
||||||
|
AddRole(principal, name);
|
||||||
|
|
||||||
|
var memberof = group.Attributes["memberof"]; // You can access ldap Attributes with Attributes property
|
||||||
|
if (memberof != null)
|
||||||
|
{
|
||||||
|
foreach (var member in memberof)
|
||||||
|
{
|
||||||
|
var groupDN = $"{Encoding.UTF8.GetString((byte[])member)}";
|
||||||
|
var nestedGroupCN = groupDN.Split(',')[0].Substring("CN=".Length);
|
||||||
|
GetNestedGroups(connection, principal, distinguishedName, nestedGroupCN, logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddRole(ClaimsIdentity identity, string role)
|
||||||
|
{
|
||||||
|
identity.AddClaim(new Claim(identity.RoleClaimType, role));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// 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 Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Authentication.Negotiate.Internal
|
||||||
|
{
|
||||||
|
internal class NegotiateOptionsValidationStartupFilter : IStartupFilter
|
||||||
|
{
|
||||||
|
private readonly string _authenticationScheme;
|
||||||
|
|
||||||
|
public NegotiateOptionsValidationStartupFilter(string authenticationScheme)
|
||||||
|
{
|
||||||
|
_authenticationScheme = authenticationScheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
|
||||||
|
{
|
||||||
|
return builder =>
|
||||||
|
{
|
||||||
|
// Resolve NegotiateOptions on startup to trigger post configuration and bind LdapConnection if needed
|
||||||
|
var options = builder.ApplicationServices.GetRequiredService<IOptionsMonitor<NegotiateOptions>>().Get(_authenticationScheme);
|
||||||
|
next(builder);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
// 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.DirectoryServices.Protocols;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Authentication.Negotiate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Options class for configuring LDAP connections on Linux
|
||||||
|
/// </summary>
|
||||||
|
public class LdapSettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configure whether LDAP connection should be used to resolve claims.
|
||||||
|
/// This is mainly used on Linux.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableLdapClaimResolution { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The domain to use for the LDAP connection. This is a mandatory setting.
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// DOMAIN.com
|
||||||
|
/// </example>
|
||||||
|
public string Domain { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The machine account name to use when opening the LDAP connection.
|
||||||
|
/// If this is not provided, the machine wide credentials of the
|
||||||
|
/// domain joined machine will be used.
|
||||||
|
/// </summary>
|
||||||
|
public string MachineAccountName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The machine account password to use when opening the LDAP connection.
|
||||||
|
/// This must be provided if a <see cref="MachineAccountName"/> is provided.
|
||||||
|
/// </summary>
|
||||||
|
public string MachineAccountPassword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This option indicates whether nested groups should be ignored when
|
||||||
|
/// resolving Roles. The default is false.
|
||||||
|
/// </summary>
|
||||||
|
public bool IgnoreNestedGroups { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="LdapConnection"/> to be used to retrieve role claims.
|
||||||
|
/// If no explicit connection is provided, an LDAP connection will be
|
||||||
|
/// automatically created based on the <see cref="Domain"/>,
|
||||||
|
/// <see cref="MachineAccountName"/> and <see cref="MachineAccountPassword"/>
|
||||||
|
/// options. If provided, this connection will be used and the
|
||||||
|
/// <see cref="Domain"/>, <see cref="MachineAccountName"/> and
|
||||||
|
/// <see cref="MachineAccountPassword"/> options will not be used to create
|
||||||
|
/// the <see cref="LdapConnection"/>.
|
||||||
|
/// </summary>
|
||||||
|
public LdapConnection LdapConnection { get; set; }
|
||||||
|
|
||||||
|
public void Validate()
|
||||||
|
{
|
||||||
|
if (EnableLdapClaimResolution)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(Domain))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"{nameof(EnableLdapClaimResolution)} is set to true but {nameof(Domain)} is not set.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(MachineAccountName) && !string.IsNullOrEmpty(MachineAccountPassword))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"{nameof(MachineAccountPassword)} should only be specified when {nameof(MachineAccountName)} is configured.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,9 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.AspNetCore.Authentication" />
|
<Reference Include="Microsoft.AspNetCore.Authentication" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Connections.Abstractions" />
|
<Reference Include="Microsoft.AspNetCore.Connections.Abstractions" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" />
|
<Reference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" />
|
||||||
|
<Reference Include="System.DirectoryServices.Protocols" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authentication.Negotiate;
|
using Microsoft.AspNetCore.Authentication.Negotiate;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Negotiate.Internal;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
|
@ -52,6 +54,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<NegotiateOptions> configureOptions)
|
public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<NegotiateOptions> configureOptions)
|
||||||
{
|
{
|
||||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<NegotiateOptions>, PostConfigureNegotiateOptions>());
|
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<NegotiateOptions>, PostConfigureNegotiateOptions>());
|
||||||
|
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IStartupFilter>(new NegotiateOptionsValidationStartupFilter(authenticationScheme)));
|
||||||
return builder.AddScheme<NegotiateOptions, NegotiateHandler>(authenticationScheme, displayName, configureOptions);
|
return builder.AddScheme<NegotiateOptions, NegotiateHandler>(authenticationScheme, displayName, configureOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Security.Principal;
|
using System.Security.Principal;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
|
|
@ -324,10 +325,37 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate
|
||||||
user = new ClaimsPrincipal(new ClaimsIdentity(identity));
|
user = new ClaimsPrincipal(new ClaimsIdentity(identity));
|
||||||
}
|
}
|
||||||
|
|
||||||
var authenticatedContext = new AuthenticatedContext(Context, Scheme, Options)
|
AuthenticatedContext authenticatedContext;
|
||||||
|
|
||||||
|
if (Options.LdapSettings.EnableLdapClaimResolution)
|
||||||
{
|
{
|
||||||
Principal = user
|
var ldapContext = new LdapContext(Context, Scheme, Options, Options.LdapSettings)
|
||||||
};
|
{
|
||||||
|
Principal = user
|
||||||
|
};
|
||||||
|
|
||||||
|
await Events.RetrieveLdapClaims(ldapContext);
|
||||||
|
|
||||||
|
if (ldapContext.Result != null)
|
||||||
|
{
|
||||||
|
return ldapContext.Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
await LdapAdapter.RetrieveClaimsAsync(ldapContext.LdapSettings, ldapContext.Principal.Identity as ClaimsIdentity, Logger);
|
||||||
|
|
||||||
|
authenticatedContext = new AuthenticatedContext(Context, Scheme, Options)
|
||||||
|
{
|
||||||
|
Principal = ldapContext.Principal
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
authenticatedContext = new AuthenticatedContext(Context, Scheme, Options)
|
||||||
|
{
|
||||||
|
Principal = user
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
await Events.Authenticated(authenticatedContext);
|
await Events.Authenticated(authenticatedContext);
|
||||||
|
|
||||||
if (authenticatedContext.Result != null)
|
if (authenticatedContext.Result != null)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Authentication.Negotiate
|
namespace Microsoft.AspNetCore.Authentication.Negotiate
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -33,6 +35,42 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool PersistNtlmCredentials { get; set; } = true;
|
public bool PersistNtlmCredentials { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration settings for LDAP connections used to retrieve claims.
|
||||||
|
/// This should only be used on Linux systems.
|
||||||
|
/// </summary>
|
||||||
|
internal LdapSettings LdapSettings { get; } = new LdapSettings();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Use LDAP connections used to retrieve claims for the given domain.
|
||||||
|
/// This should only be used on Linux systems.
|
||||||
|
/// </summary>
|
||||||
|
public void EnableLdap(string domain)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(domain))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(domain));
|
||||||
|
}
|
||||||
|
|
||||||
|
LdapSettings.EnableLdapClaimResolution = true;
|
||||||
|
LdapSettings.Domain = domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Use LDAP connections used to retrieve claims using the configured settings.
|
||||||
|
/// This should only be used on Linux systems.
|
||||||
|
/// </summary>
|
||||||
|
public void EnableLdap(Action<LdapSettings> configureSettings)
|
||||||
|
{
|
||||||
|
if (configureSettings == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(configureSettings));
|
||||||
|
}
|
||||||
|
|
||||||
|
LdapSettings.EnableLdapClaimResolution = true;
|
||||||
|
configureSettings(LdapSettings);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates if integrated server Windows Auth is being used instead of this handler.
|
/// Indicates if integrated server Windows Auth is being used instead of this handler.
|
||||||
/// See <see cref="PostConfigureNegotiateOptions"/>.
|
/// See <see cref="PostConfigureNegotiateOptions"/>.
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.DirectoryServices.Protocols;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
using Microsoft.AspNetCore.Hosting.Server;
|
using Microsoft.AspNetCore.Hosting.Server;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
@ -59,6 +61,36 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate
|
||||||
+ " Enable Windows Authentication for the server and the Negotiate Authentication handler will defer to it.");
|
+ " Enable Windows Authentication for the server and the Negotiate Authentication handler will defer to it.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ldapSettings = options.LdapSettings;
|
||||||
|
|
||||||
|
if (ldapSettings.EnableLdapClaimResolution)
|
||||||
|
{
|
||||||
|
ldapSettings.Validate();
|
||||||
|
|
||||||
|
if (ldapSettings.LdapConnection == null)
|
||||||
|
{
|
||||||
|
var di = new LdapDirectoryIdentifier(server: ldapSettings.Domain, fullyQualifiedDnsHostName: true, connectionless: false);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(ldapSettings.MachineAccountName))
|
||||||
|
{
|
||||||
|
// Use default credentials
|
||||||
|
ldapSettings.LdapConnection = new LdapConnection(di);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Use specific specific machine account
|
||||||
|
var machineAccount = ldapSettings.MachineAccountName + "@" + ldapSettings.Domain;
|
||||||
|
var credentials = new NetworkCredential(machineAccount, ldapSettings.MachineAccountPassword);
|
||||||
|
ldapSettings.LdapConnection = new LdapConnection(di, credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
ldapSettings.LdapConnection.SessionOptions.ProtocolVersion = 3; //Setting LDAP Protocol to latest version
|
||||||
|
ldapSettings.LdapConnection.Timeout = TimeSpan.FromMinutes(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ldapSettings.LdapConnection.Bind(); // This line actually makes the connection.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Reflection.Metadata;
|
|
||||||
using System.Security.Principal;
|
using System.Security.Principal;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
|
@ -13,7 +12,6 @@ using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.TestHost;
|
using Microsoft.AspNetCore.TestHost;
|
||||||
using Microsoft.AspNetCore.Testing;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
@ -371,6 +369,27 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate
|
||||||
Assert.Equal(1, callCount);
|
Assert.Equal(1, callCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task OnRetrieveLdapClaims_DoesNotFireWhenLdapDisabled()
|
||||||
|
{
|
||||||
|
var callCount = 0;
|
||||||
|
using var host = await CreateHostAsync(options =>
|
||||||
|
{
|
||||||
|
options.Events = new NegotiateEvents()
|
||||||
|
{
|
||||||
|
OnRetrieveLdapClaims = context =>
|
||||||
|
{
|
||||||
|
callCount++;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
var server = host.GetTestServer();
|
||||||
|
|
||||||
|
await KerberosStage1And2Auth(server, new TestConnection());
|
||||||
|
Assert.Equal(0, callCount);
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task KerberosStage1And2Auth(TestServer server, TestConnection testConnection)
|
private static async Task KerberosStage1And2Auth(TestServer server, TestConnection testConnection)
|
||||||
{
|
{
|
||||||
await KerberosStage1Auth(server, testConnection);
|
await KerberosStage1Auth(server, testConnection);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
// 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 Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Authentication.Negotiate.Test
|
||||||
|
{
|
||||||
|
public class LdapSettingsValidationTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void EnabledWithoutDomainThrows()
|
||||||
|
{
|
||||||
|
var settings = new LdapSettings
|
||||||
|
{
|
||||||
|
EnableLdapClaimResolution = true
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentException>(() => settings.Validate());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AccountPasswordWithoutAccountNameThrows()
|
||||||
|
{
|
||||||
|
var settings = new LdapSettings
|
||||||
|
{
|
||||||
|
EnableLdapClaimResolution = true,
|
||||||
|
MachineAccountPassword = "Passw0rd"
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentException>(() => settings.Validate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,8 +29,7 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ServerSupportsAuthButDisabled_Error()
|
public async Task ServerSupportsAuthButDisabled_Error()
|
||||||
{
|
{
|
||||||
using var host = await CreateHostAsync(supportsAuth: true, isEnabled: false);
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await CreateHostAsync(supportsAuth: true, isEnabled: false));
|
||||||
var ex = Assert.Throws<InvalidOperationException>(() => host.Services.GetRequiredService<IOptions<NegotiateOptions>>().Value);
|
|
||||||
Assert.Equal("The Negotiate Authentication handler cannot be used on a server that directly supports Windows Authentication."
|
Assert.Equal("The Negotiate Authentication handler cannot be used on a server that directly supports Windows Authentication."
|
||||||
+ " Enable Windows Authentication for the server and the Negotiate Authentication handler will defer to it.", ex.Message);
|
+ " Enable Windows Authentication for the server and the Negotiate Authentication handler will defer to it.", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue