Skip to content

Commit 098be5f

Browse files
author
John Luo
authored
Use LDAP support from DirectoryServices.Protocols for RBAC claim resolution on Linux for Negotiate (#25075)
1 parent c2f0331 commit 098be5f

17 files changed

+435
-7
lines changed

eng/Dependencies.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ and are generated based on the last package release.
6464
<LatestPackageReference Include="System.ComponentModel.Annotations" />
6565
<LatestPackageReference Include="System.Diagnostics.DiagnosticSource" />
6666
<LatestPackageReference Include="System.Diagnostics.EventLog" />
67+
<LatestPackageReference Include="System.DirectoryServices.Protocols" />
6768
<LatestPackageReference Include="System.Drawing.Common" />
6869
<LatestPackageReference Include="System.IO.Pipelines" />
6970
<LatestPackageReference Include="System.Net.Http" />

eng/Version.Details.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,10 @@
205205
<Uri>https://github.com/dotnet/runtime</Uri>
206206
<Sha>f4e99f4afa445b519abcd7c5c87cbf54771614db</Sha>
207207
</Dependency>
208+
<Dependency Name="System.DirectoryServices.Protocols" Version="5.0.0-rc.1.20425.1">
209+
<Uri>https://github.com/dotnet/runtime</Uri>
210+
<Sha>f4e99f4afa445b519abcd7c5c87cbf54771614db</Sha>
211+
</Dependency>
208212
<Dependency Name="System.Drawing.Common" Version="5.0.0-rc.1.20425.1">
209213
<Uri>https://github.com/dotnet/runtime</Uri>
210214
<Sha>f4e99f4afa445b519abcd7c5c87cbf54771614db</Sha>

eng/Versions.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
<SystemComponentModelAnnotationsPackageVersion>5.0.0-rc.1.20425.1</SystemComponentModelAnnotationsPackageVersion>
109109
<SystemDiagnosticsDiagnosticSourcePackageVersion>5.0.0-rc.1.20425.1</SystemDiagnosticsDiagnosticSourcePackageVersion>
110110
<SystemDiagnosticsEventLogPackageVersion>5.0.0-rc.1.20425.1</SystemDiagnosticsEventLogPackageVersion>
111+
<SystemDirectoryServicesProtocolsPackageVersion>5.0.0-rc.1.20425.1</SystemDirectoryServicesProtocolsPackageVersion>
111112
<SystemDrawingCommonPackageVersion>5.0.0-rc.1.20425.1</SystemDrawingCommonPackageVersion>
112113
<SystemIOPipelinesPackageVersion>5.0.0-rc.1.20425.1</SystemIOPipelinesPackageVersion>
113114
<SystemNetHttpJsonPackageVersion>5.0.0-rc.1.20425.1</SystemNetHttpJsonPackageVersion>

src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Startup.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System.Runtime.InteropServices;
45
using System.Threading.Tasks;
56
using Microsoft.AspNetCore.Authentication.Negotiate;
67
using Microsoft.AspNetCore.Builder;
@@ -22,6 +23,23 @@ public void ConfigureServices(IServiceCollection services)
2223
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
2324
.AddNegotiate(options =>
2425
{
26+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
27+
{
28+
/*
29+
options.EnableLdap("DOMAIN.net");
30+
31+
options.EnableLdap(settings =>
32+
{
33+
// Mandatory settings
34+
settings.Domain = "DOMAIN.com";
35+
// Optional settings
36+
settings.MachineAccountName = "machineName";
37+
settings.MachineAccountPassword = "PassW0rd";
38+
settings.IgnoreNestedGroups = true;
39+
});
40+
*/
41+
}
42+
2543
options.Events = new NegotiateEvents()
2644
{
2745
OnAuthenticationFailed = context =>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace Microsoft.AspNetCore.Authentication.Negotiate
7+
{
8+
/// <summary>
9+
/// State for the RetrieveLdapClaims event.
10+
/// </summary>
11+
public class LdapContext : ResultContext<NegotiateOptions>
12+
{
13+
/// <summary>
14+
/// Creates a new <see cref="LdapContext"/>.
15+
/// </summary>
16+
/// <param name="context"></param>
17+
/// <param name="scheme"></param>
18+
/// <param name="options"></param>
19+
/// <param name="settings"></param>
20+
public LdapContext(
21+
HttpContext context,
22+
AuthenticationScheme scheme,
23+
NegotiateOptions options,
24+
LdapSettings settings)
25+
: base(context, scheme, options)
26+
{
27+
LdapSettings = settings;
28+
}
29+
30+
/// <summary>
31+
/// The LDAP settings to use for the RetrieveLdapClaims event.
32+
/// </summary>
33+
public LdapSettings LdapSettings { get; }
34+
}
35+
}

src/Security/Authentication/Negotiate/src/Events/NegotiateEvents.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ public class NegotiateEvents
1616
/// </summary>
1717
public Func<AuthenticationFailedContext, Task> OnAuthenticationFailed { get; set; } = context => Task.CompletedTask;
1818

19+
/// <summary>
20+
/// Invoked after the authentication before ClaimsIdentity is populated with claims retrieved through the LDAP connection.
21+
/// This event is invoked when <see cref="LdapSettings.EnableLdapClaimResolution"/> is set to true on <see cref="LdapSettings"/>.
22+
/// </summary>
23+
public Func<LdapContext, Task> OnRetrieveLdapClaims { get; set; } = context => Task.CompletedTask;
24+
1925
/// <summary>
2026
/// Invoked after the authentication is complete and a ClaimsIdentity has been generated.
2127
/// </summary>
@@ -31,6 +37,11 @@ public class NegotiateEvents
3137
/// </summary>
3238
public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context);
3339

40+
/// <summary>
41+
/// Invoked after the authentication before ClaimsIdentity is populated with claims retrieved through the LDAP connection.
42+
/// </summary>
43+
public virtual Task RetrieveLdapClaims(LdapContext context) => OnRetrieveLdapClaims(context);
44+
3445
/// <summary>
3546
/// Invoked after the authentication is complete and a ClaimsIdentity has been generated.
3647
/// </summary>
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.DirectoryServices.Protocols;
5+
using System.Linq;
6+
using System.Security.Claims;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
using Microsoft.Extensions.Logging;
10+
11+
namespace Microsoft.AspNetCore.Authentication.Negotiate
12+
{
13+
internal static class LdapAdapter
14+
{
15+
public static async Task RetrieveClaimsAsync(LdapSettings settings, ClaimsIdentity identity, ILogger logger)
16+
{
17+
var user = identity.Name;
18+
var userAccountName = user.Substring(0, user.IndexOf('@'));
19+
var distinguishedName = settings.Domain.Split('.').Select(name => $"dc={name}").Aggregate((a, b) => $"{a},{b}");
20+
21+
var filter = $"(&(objectClass=user)(sAMAccountName={userAccountName}))"; // This is using ldap search query language, it is looking on the server for someUser
22+
var searchRequest = new SearchRequest(distinguishedName, filter, SearchScope.Subtree, null);
23+
var searchResponse = (SearchResponse) await Task<DirectoryResponse>.Factory.FromAsync(
24+
settings.LdapConnection.BeginSendRequest,
25+
settings.LdapConnection.EndSendRequest,
26+
searchRequest,
27+
PartialResultProcessing.NoPartialResultSupport,
28+
null);
29+
30+
if (searchResponse.Entries.Count > 0)
31+
{
32+
if (searchResponse.Entries.Count > 1)
33+
{
34+
logger.LogWarning($"More than one response received for query: {filter} with distinguished name: {distinguishedName}");
35+
}
36+
37+
var userFound = searchResponse.Entries[0]; //Get the object that was found on ldap
38+
var memberof = userFound.Attributes["memberof"]; // You can access ldap Attributes with Attributes property
39+
40+
foreach (var group in memberof)
41+
{
42+
// Example distinguished name: CN=TestGroup,DC=KERB,DC=local
43+
var groupDN = $"{Encoding.UTF8.GetString((byte[])group)}";
44+
var groupCN = groupDN.Split(',')[0].Substring("CN=".Length);
45+
46+
if (!settings.IgnoreNestedGroups)
47+
{
48+
GetNestedGroups(settings.LdapConnection, identity, distinguishedName, groupCN, logger);
49+
}
50+
else
51+
{
52+
AddRole(identity, groupCN);
53+
}
54+
}
55+
}
56+
else
57+
{
58+
logger.LogWarning($"No response received for query: {filter} with distinguished name: {distinguishedName}");
59+
}
60+
}
61+
62+
private static void GetNestedGroups(LdapConnection connection, ClaimsIdentity principal, string distinguishedName, string groupCN, ILogger logger)
63+
{
64+
var filter = $"(&(objectClass=group)(sAMAccountName={groupCN}))"; // This is using ldap search query language, it is looking on the server for someUser
65+
var searchRequest = new SearchRequest(distinguishedName, filter, System.DirectoryServices.Protocols.SearchScope.Subtree, null);
66+
var searchResponse = (SearchResponse)connection.SendRequest(searchRequest);
67+
68+
if (searchResponse.Entries.Count > 0)
69+
{
70+
if (searchResponse.Entries.Count > 1)
71+
{
72+
logger.LogWarning($"More than one response received for query: {filter} with distinguished name: {distinguishedName}");
73+
}
74+
75+
var group = searchResponse.Entries[0]; //Get the object that was found on ldap
76+
string name = group.DistinguishedName;
77+
AddRole(principal, name);
78+
79+
var memberof = group.Attributes["memberof"]; // You can access ldap Attributes with Attributes property
80+
if (memberof != null)
81+
{
82+
foreach (var member in memberof)
83+
{
84+
var groupDN = $"{Encoding.UTF8.GetString((byte[])member)}";
85+
var nestedGroupCN = groupDN.Split(',')[0].Substring("CN=".Length);
86+
GetNestedGroups(connection, principal, distinguishedName, nestedGroupCN, logger);
87+
}
88+
}
89+
}
90+
}
91+
92+
private static void AddRole(ClaimsIdentity identity, string role)
93+
{
94+
identity.AddClaim(new Claim(identity.RoleClaimType, role));
95+
}
96+
}
97+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.AspNetCore.Hosting;
7+
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.Extensions.Options;
9+
10+
namespace Microsoft.AspNetCore.Authentication.Negotiate.Internal
11+
{
12+
internal class NegotiateOptionsValidationStartupFilter : IStartupFilter
13+
{
14+
private readonly string _authenticationScheme;
15+
16+
public NegotiateOptionsValidationStartupFilter(string authenticationScheme)
17+
{
18+
_authenticationScheme = authenticationScheme;
19+
}
20+
21+
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
22+
{
23+
return builder =>
24+
{
25+
// Resolve NegotiateOptions on startup to trigger post configuration and bind LdapConnection if needed
26+
var options = builder.ApplicationServices.GetRequiredService<IOptionsMonitor<NegotiateOptions>>().Get(_authenticationScheme);
27+
next(builder);
28+
};
29+
}
30+
}
31+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.DirectoryServices.Protocols;
6+
7+
namespace Microsoft.AspNetCore.Authentication.Negotiate
8+
{
9+
/// <summary>
10+
/// Options class for configuring LDAP connections on Linux
11+
/// </summary>
12+
public class LdapSettings
13+
{
14+
/// <summary>
15+
/// Configure whether LDAP connection should be used to resolve claims.
16+
/// This is mainly used on Linux.
17+
/// </summary>
18+
public bool EnableLdapClaimResolution { get; set; }
19+
20+
/// <summary>
21+
/// The domain to use for the LDAP connection. This is a mandatory setting.
22+
/// </summary>
23+
/// <example>
24+
/// DOMAIN.com
25+
/// </example>
26+
public string Domain { get; set; }
27+
28+
/// <summary>
29+
/// The machine account name to use when opening the LDAP connection.
30+
/// If this is not provided, the machine wide credentials of the
31+
/// domain joined machine will be used.
32+
/// </summary>
33+
public string MachineAccountName { get; set; }
34+
35+
/// <summary>
36+
/// The machine account password to use when opening the LDAP connection.
37+
/// This must be provided if a <see cref="MachineAccountName"/> is provided.
38+
/// </summary>
39+
public string MachineAccountPassword { get; set; }
40+
41+
/// <summary>
42+
/// This option indicates whether nested groups should be ignored when
43+
/// resolving Roles. The default is false.
44+
/// </summary>
45+
public bool IgnoreNestedGroups { get; set; }
46+
47+
/// <summary>
48+
/// The <see cref="LdapConnection"/> to be used to retrieve role claims.
49+
/// If no explicit connection is provided, an LDAP connection will be
50+
/// automatically created based on the <see cref="Domain"/>,
51+
/// <see cref="MachineAccountName"/> and <see cref="MachineAccountPassword"/>
52+
/// options. If provided, this connection will be used and the
53+
/// <see cref="Domain"/>, <see cref="MachineAccountName"/> and
54+
/// <see cref="MachineAccountPassword"/> options will not be used to create
55+
/// the <see cref="LdapConnection"/>.
56+
/// </summary>
57+
public LdapConnection LdapConnection { get; set; }
58+
59+
public void Validate()
60+
{
61+
if (EnableLdapClaimResolution)
62+
{
63+
if (string.IsNullOrEmpty(Domain))
64+
{
65+
throw new ArgumentException($"{nameof(EnableLdapClaimResolution)} is set to true but {nameof(Domain)} is not set.");
66+
}
67+
68+
if (string.IsNullOrEmpty(MachineAccountName) && !string.IsNullOrEmpty(MachineAccountPassword))
69+
{
70+
throw new ArgumentException($"{nameof(MachineAccountPassword)} should only be specified when {nameof(MachineAccountName)} is configured.");
71+
}
72+
}
73+
}
74+
}
75+
}

src/Security/Authentication/Negotiate/src/Microsoft.AspNetCore.Authentication.Negotiate.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
<ItemGroup>
1111
<Reference Include="Microsoft.AspNetCore.Authentication" />
1212
<Reference Include="Microsoft.AspNetCore.Connections.Abstractions" />
13+
<Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
1314
<Reference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" />
15+
<Reference Include="System.DirectoryServices.Protocols" />
1416
</ItemGroup>
1517

1618
</Project>

0 commit comments

Comments
 (0)