Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use RBAC support from DirectoryServices.Protocols for Role claim resolution on Linux for Negotiate #25075

Merged
merged 19 commits into from
Aug 25, 2020
Merged
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
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