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
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,19 @@ public void ConfigureServices(IServiceCollection services)
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
.AddNegotiate(options =>
{
/*
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var ldapOptions = options.LdapOptions;
// Mandatory settings
ldapOptions.EnableLdapRoleClaimResolution = true;
ldapOptions.Domain = "DOMAIN.com";
// Optional settings
ldapOptions.MachineAccountName = "machineName";
ldapOptions.MachineAccountPassword = "PassW0rd";
ldapOptions.ResolveNestedGroups = true;
}*/

options.Events = new NegotiateEvents()
{
OnAuthenticationFailed = context =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,57 @@

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

public LinuxAdapter(NegotiateOptions options, ILogger logger)
public LdapAdapter(LdapOptions options, ILogger logger)
{
_logger = logger;
_options = options.LdapConnectionOptions;
_options = options;
_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
if (_options.LdapConnection == null)
{
// Use specific specific machine account
var machineAccount = _options.MachineAccountName + "@" + _options.Domain;
var credentials = new NetworkCredential(machineAccount, _options.MachineAccountPassword);
_connection = new LdapConnection(di, credentials);
}
var di = new LdapDirectoryIdentifier(server: _options.Domain, fullyQualifiedDnsHostName: true, connectionless: false);

_connection.SessionOptions.ProtocolVersion = 3; //Setting LDAP Protocol to latest version
_connection.Timeout = TimeSpan.FromMinutes(1);
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);
}

// Additional custom configuration
_options.ConfigureLdapConnection?.Invoke(_connection);
_connection.SessionOptions.ProtocolVersion = 3; //Setting LDAP Protocol to latest version
_connection.Timeout = TimeSpan.FromMinutes(1);
}

// TODO: Async? IO?
_connection.Bind(); // This line actually makes the connection.
}

public Task OnAuthenticatedAsync(AuthenticatedContext context)
public async 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);
var searchResponse = (SearchResponse) await Task<DirectoryResponse>.Factory.FromAsync(
_connection.BeginSendRequest,
_connection.EndSendRequest,
searchRequest,
PartialResultProcessing.NoPartialResultSupport,
null);

if (searchResponse.Entries.Count > 0)
{
Expand Down Expand Up @@ -92,7 +97,7 @@ public Task OnAuthenticatedAsync(AuthenticatedContext context)
_logger.LogWarning($"No response received for query: {filter} with distinguished name: {_distinguishedName}");
}

return Task.CompletedTask;
return;
}

private void GetNestedGroups(ClaimsIdentity principal, string groupCN)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate
/// <summary>
/// Options class for configuring LDAP connections on Linux
/// </summary>
public class LdapConnectionOptions
public class LdapOptions
{
/// <summary>
/// Configure whether LDAP connection should be used to resolve role claims.
/// This should only be enabled on Linux.
/// </summary>
public bool EnableLdapRoleClaimResolution { get; set; }

/// <summary>
/// The domain to use for the LDAP connection. This is a mandatory setting.
/// </summary>
Expand All @@ -28,8 +34,7 @@ public class LdapConnectionOptions

/// <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.
/// This must be provided if a MachineAccountName is provided.
/// </summary>
public string MachineAccountPassword { get; set; }

Expand All @@ -40,8 +45,25 @@ public class LdapConnectionOptions
public bool ResolveNestedGroups { get; set; } = true;

/// <summary>
/// Additional configuration on the created LdapConnection.
/// The LdapConnection to be used to retrieve role claims. If no explicit
/// connection is provided, an LDAP connection will be automatically created
/// based on the Domain, MachineAccountName and MachineAccountPassword options.
/// </summary>
public Action<LdapConnection> ConfigureLdapConnection { get; set; }
public LdapConnection LdapConnection { get; set; }

public void Validate()
{
if (EnableLdapRoleClaimResolution)
{
if (string.IsNullOrEmpty(Domain))
{
throw new ArgumentException($"{nameof(EnableLdapRoleClaimResolution)} 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.");
}
}
}
}
}
11 changes: 3 additions & 8 deletions src/Security/Authentication/Negotiate/src/NegotiateHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class NegotiateHandler : AuthenticationHandler<NegotiateOptions>, IAuthen

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

/// <summary>
/// Creates a new <see cref="NegotiateHandler"/>
Expand Down Expand Up @@ -332,14 +332,9 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
Principal = user
};

if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && Options?.LdapConnectionOptions != null && _linuxAdapter == null)
if (Options.LdapOptions.EnableLdapRoleClaimResolution && _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);
_linuxAdapter = new LdapAdapter(Options.LdapOptions, Logger);
}

if (_linuxAdapter != null)
Expand Down
20 changes: 19 additions & 1 deletion src/Security/Authentication/Negotiate/src/NegotiateOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,25 @@ public class NegotiateOptions : AuthenticationSchemeOptions
/// 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;
public LdapOptions LdapOptions { get; } = new LdapOptions();

/// <summary>
/// Checks that the options are valid for a specific scheme
/// </summary>
/// <param name="scheme">The scheme being validated.</param>
public override void Validate(string scheme)
{
Validate();
}

/// <summary>
/// Check that the options are valid. Should throw an exception if things are not ok.
/// </summary>
public override void Validate()
{
base.Validate();
LdapOptions.Validate();
}

/// <summary>
/// Indicates if integrated server Windows Auth is being used instead of this handler.
Expand Down