Skip to content

Commit

Permalink
Use LDAP support from DirectoryServices.Protocols for RBAC claim reso…
Browse files Browse the repository at this point in the history
…lution on Linux for Negotiate (#25075)
  • Loading branch information
John Luo authored Aug 25, 2020
1 parent c2f0331 commit 098be5f
Show file tree
Hide file tree
Showing 17 changed files with 435 additions and 7 deletions.
1 change: 1 addition & 0 deletions eng/Dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ and are generated based on the last package release.
<LatestPackageReference Include="System.ComponentModel.Annotations" />
<LatestPackageReference Include="System.Diagnostics.DiagnosticSource" />
<LatestPackageReference Include="System.Diagnostics.EventLog" />
<LatestPackageReference Include="System.DirectoryServices.Protocols" />
<LatestPackageReference Include="System.Drawing.Common" />
<LatestPackageReference Include="System.IO.Pipelines" />
<LatestPackageReference Include="System.Net.Http" />
Expand Down
4 changes: 4 additions & 0 deletions eng/Version.Details.xml
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@
<Uri>https://github.com/dotnet/runtime</Uri>
<Sha>f4e99f4afa445b519abcd7c5c87cbf54771614db</Sha>
</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">
<Uri>https://github.com/dotnet/runtime</Uri>
<Sha>f4e99f4afa445b519abcd7c5c87cbf54771614db</Sha>
Expand Down
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
<SystemComponentModelAnnotationsPackageVersion>5.0.0-rc.1.20425.1</SystemComponentModelAnnotationsPackageVersion>
<SystemDiagnosticsDiagnosticSourcePackageVersion>5.0.0-rc.1.20425.1</SystemDiagnosticsDiagnosticSourcePackageVersion>
<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>
<SystemIOPipelinesPackageVersion>5.0.0-rc.1.20425.1</SystemIOPipelinesPackageVersion>
<SystemNetHttpJsonPackageVersion>5.0.0-rc.1.20425.1</SystemNetHttpJsonPackageVersion>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Negotiate;
using Microsoft.AspNetCore.Builder;
Expand All @@ -22,6 +23,23 @@ public void ConfigureServices(IServiceCollection services)
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
.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()
{
OnAuthenticationFailed = context =>
Expand Down
35 changes: 35 additions & 0 deletions src/Security/Authentication/Negotiate/src/Events/LdapContext.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ public class NegotiateEvents
/// </summary>
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>
/// Invoked after the authentication is complete and a ClaimsIdentity has been generated.
/// </summary>
Expand All @@ -31,6 +37,11 @@ public class NegotiateEvents
/// </summary>
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>
/// Invoked after the authentication is complete and a ClaimsIdentity has been generated.
/// </summary>
Expand Down
97 changes: 97 additions & 0 deletions src/Security/Authentication/Negotiate/src/Internal/LdapAdapter.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
};
}
}
}
75 changes: 75 additions & 0 deletions src/Security/Authentication/Negotiate/src/LdapSettings.cs
Original file line number Diff line number Diff line change
@@ -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.");
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Authentication" />
<Reference Include="Microsoft.AspNetCore.Connections.Abstractions" />
<Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
<Reference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" />
<Reference Include="System.DirectoryServices.Protocols" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using System;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Negotiate;
using Microsoft.AspNetCore.Authentication.Negotiate.Internal;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;

Expand Down Expand Up @@ -52,6 +54,7 @@ public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder buil
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<IStartupFilter>(new NegotiateOptionsValidationStartupFilter(authenticationScheme)));
return builder.AddScheme<NegotiateOptions, NegotiateHandler>(authenticationScheme, displayName, configureOptions);
}
}
Expand Down
Loading

0 comments on commit 098be5f

Please sign in to comment.