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>0862d48a9c9fbda879206b194a16d8e607deac42</Sha>
</Dependency>
<Dependency Name="System.DirectoryServices.Protocols" Version="5.0.0-rc.1.20416.7">
<Uri>https://github.com/dotnet/runtime</Uri>
<Sha>0862d48a9c9fbda879206b194a16d8e607deac42</Sha>
</Dependency>
<Dependency Name="System.Drawing.Common" Version="5.0.0-rc.1.20416.7">
<Uri>https://github.com/dotnet/runtime</Uri>
<Sha>0862d48a9c9fbda879206b194a16d8e607deac42</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.20416.7</SystemComponentModelAnnotationsPackageVersion>
<SystemDiagnosticsDiagnosticSourcePackageVersion>5.0.0-rc.1.20416.7</SystemDiagnosticsDiagnosticSourcePackageVersion>
<SystemDiagnosticsEventLogPackageVersion>5.0.0-rc.1.20416.7</SystemDiagnosticsEventLogPackageVersion>
<SystemDirectoryServicesProtocolsPackageVersion>5.0.0-rc.1.20416.7</SystemDirectoryServicesProtocolsPackageVersion>
<SystemDrawingCommonPackageVersion>5.0.0-rc.1.20416.7</SystemDrawingCommonPackageVersion>
<SystemIOPipelinesPackageVersion>5.0.0-rc.1.20416.7</SystemIOPipelinesPackageVersion>
<SystemNetHttpJsonPackageVersion>5.0.0-rc.1.20416.7</SystemNetHttpJsonPackageVersion>
Expand Down
133 changes: 133 additions & 0 deletions src/Security/Authentication/Negotiate/src/Internal/LinuxAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// 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;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Authentication.Negotiate
{
internal class LinuxAdapter
{
private readonly string _distinguishedName;
private readonly LdapConnection _connection;
private readonly ILogger _logger;
private readonly LdapConnectionOptions _options;

public LinuxAdapter(NegotiateOptions options, ILogger logger)
{
_logger = logger;
_options = options.LdapConnectionOptions;
_distinguishedName = _options.Domain.Split('.').Select(name => $"dc={name}").Aggregate((a, b) => $"{a},{b}");


var di = new LdapDirectoryIdentifier(server: _options.Domain, fullyQualifiedDnsHostName: true, connectionless: false);

if (string.IsNullOrEmpty(_options.MachineAccountName))
{
// Use default credentials
_connection = new LdapConnection(di);
}
else
{
// Use specific specific machine account
var machineAccount = _options.MachineAccountName + "@" + _options.Domain;
var credentials = new NetworkCredential(machineAccount, _options.MachineAccountPassword);
_connection = new LdapConnection(di, credentials);
}

_connection.SessionOptions.ProtocolVersion = 3; //Setting LDAP Protocol to latest version
_connection.Timeout = TimeSpan.FromMinutes(1);

// Additional custom configuration
_options.ConfigureLdapConnection?.Invoke(_connection);

_connection.Bind(); // This line actually makes the connection.
}

public Task OnAuthenticatedAsync(AuthenticatedContext context)
{
var user = context.Principal.Identity.Name;
var userAccountName = user.Substring(0, user.IndexOf('@'));

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)_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 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

var claimsIdentity = context.Principal.Identity as ClaimsIdentity;

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 (_options.ResolveNestedGroups)
{
GetNestedGroups(claimsIdentity, groupCN);
}
else
{
AddRole(claimsIdentity, groupCN);
}
}
}
else
{
_logger.LogWarning($"No response received for query: {filter} with distinguished name: {_distinguishedName}");
}

return Task.CompletedTask;
}

private void GetNestedGroups(ClaimsIdentity principal, string groupCN)
{
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(principal, nestedGroupCN);
}
}
}
}

private static void AddRole(ClaimsIdentity identity, string role)
{
identity.AddClaim(new Claim(identity.RoleClaimType, role));
}
}
}
47 changes: 47 additions & 0 deletions src/Security/Authentication/Negotiate/src/LdapConnectionOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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 LdapConnectionOptions
{
/// <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.
/// If this is not provided, the machine wide credentials of the
/// domain joined machine will be used.
/// </summary>
public string MachineAccountPassword { get; set; }

/// <summary>
/// This option indicates whether nested groups should be examined when
/// resolving AD Roles.
/// </summary>
public bool ResolveNestedGroups { get; set; } = true;

/// <summary>
/// Additional configuration on the created LdapConnection.
/// </summary>
public Action<LdapConnection> ConfigureLdapConnection { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<Reference Include="Microsoft.AspNetCore.Authentication" />
<Reference Include="Microsoft.AspNetCore.Connections.Abstractions" />
<Reference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" />
<Reference Include="System.DirectoryServices.Protocols" />
</ItemGroup>

</Project>
21 changes: 20 additions & 1 deletion src/Security/Authentication/Negotiate/src/NegotiateHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Claims;
using System.Security.Principal;
using System.Text.Encodings.Web;
Expand All @@ -29,6 +30,7 @@ public class NegotiateHandler : AuthenticationHandler<NegotiateOptions>, IAuthen

private bool _requestProcessed;
private INegotiateState _negotiateState;
private LinuxAdapter _linuxAdapter;

/// <summary>
/// Creates a new <see cref="NegotiateHandler"/>
Expand All @@ -39,7 +41,8 @@ public class NegotiateHandler : AuthenticationHandler<NegotiateOptions>, IAuthen
/// <param name="clock"></param>
public NegotiateHandler(IOptionsMonitor<NegotiateOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{ }
{
}

/// <summary>
/// The handler calls methods on the events which give the application control at certain points where processing is occurring.
Expand Down Expand Up @@ -328,6 +331,22 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
Principal = user
};

if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && Options?.LdapConnectionOptions != null && _linuxAdapter == null)
{
if (string.IsNullOrEmpty(Options.LdapConnectionOptions.Domain))
{
throw new InvalidOperationException($"{nameof(LdapConnectionOptions)} is configured but {nameof(LdapConnectionOptions.Domain)} is not set");
}

_linuxAdapter = new LinuxAdapter(Options, Logger);
}

if (_linuxAdapter != null)
{
await _linuxAdapter.OnAuthenticatedAsync(authenticatedContext);
}

await Events.Authenticated(authenticatedContext);

if (authenticatedContext.Result != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ public class NegotiateOptions : AuthenticationSchemeOptions
/// </summary>
public bool PersistNtlmCredentials { get; set; } = true;

/// <summary>
/// Configuration settings for LDAP connections used to retrieve AD Role claims.
/// This is only used on Linux systems.
/// </summary>
public LdapConnectionOptions LdapConnectionOptions { get; set; } = null;

/// <summary>
/// Indicates if integrated server Windows Auth is being used instead of this handler.
/// See <see cref="PostConfigureNegotiateOptions"/>.
Expand Down