dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.25k stars 4.73k forks source link

LdapConnection throws `LdapException: The feature is not supported` when using Negotiate or Kerberos authentication #109449

Open 0xced opened 17 hours ago

0xced commented 17 hours ago

Description

Trying to perform an LDAP request using Negotiate or Kerberos authentication with the System.DirectoryServices.Protocols package on macOS throws LdapException: The feature is not supported.

Reproduction Steps

ldap.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.DirectoryServices.Protocols" Version="8.0.0" />
  </ItemGroup>

</Project>

Program.cs

using System;
using System.DirectoryServices.Protocols;
using System.Linq;

using var ldapConnection = new LdapConnection(new LdapDirectoryIdentifier(Environment.GetEnvironmentVariable("LDAPHOST")), null, AuthType.Kerberos);
ldapConnection.SessionOptions.ReferralChasing = ReferralChasingOptions.None;
var searchRequest = new SearchRequest(Environment.GetEnvironmentVariable("LDAPBASE"), "(&(objectCategory=person)(objectClass=user)(c=ZZ))", SearchScope.Subtree, "mail");
var response = (SearchResponse)ldapConnection.SendRequest(searchRequest);
foreach (SearchResultEntry entry in response.Entries)
{
    Console.WriteLine($"mail: {entry.Attributes["mail"]?.GetValues(typeof(string))?.OfType<string>().FirstOrDefault()}");
}
Console.WriteLine($"Found {response.Entries.Count} entries");

Export the LDAPHOST environment variable to your actual domain controller (e.g. dc.contoso.com) and LDAPBASE to your actual search base (e.g. DC=contoso,DC=com) then run the ldap console app with the dotnet run command.

export LDAPHOST="dc.contoso.com"
export LDAPBASE="DC=contoso,DC=com"
dotnet run

Expected behavior

The program runs and prints Found 0 entries (because no user actually matches the c=ZZ filter criteria).

Actual behavior

The program throws System.DirectoryServices.Protocols.LdapException: The feature is not supported.

Stack trace:

at System.DirectoryServices.Protocols.LdapConnection.BindHelper(NetworkCredential newCredential, Boolean needSetCredential)
at System.DirectoryServices.Protocols.LdapConnection.SendRequestHelper(DirectoryRequest request, Int32& messageID)
at System.DirectoryServices.Protocols.LdapConnection.SendRequest(DirectoryRequest request, TimeSpan requestTimeout)
at System.DirectoryServices.Protocols.LdapConnection.SendRequest(DirectoryRequest request)
at Program.<Main>$(String[] args) in Program.cs:line 8

Regression?

Probably not a regression.

Known Workarounds

It's possible to get the code working by explicitly settings the LDAP protocol to version 3 by either adding ldapConnection.SessionOptions.ProtocolVersion = 3; just after creating the LdapConnection object or by creating an ~/.ldaprc file with this content, as documented in ldap.conf(5):

VERSION 3

Configuration

The bug probably happens on Linux too since it shares a lot of the implementation, but I have not tested.

dotnet --info
.NET SDK:
 Version:           8.0.403
 Commit:            c64aa40a71
 Workload version:  8.0.400-manifests.51490631
 MSBuild version:   17.11.9+a69bbaaf5

Runtime Environment:
 OS Name:     Mac OS X
 OS Version:  13.6
 OS Platform: Darwin
 RID:         osx-arm64
 Base Path:   /usr/local/share/dotnet/sdk/8.0.403/

.NET workloads installed:
Configured to use loose manifests when installing new manifests.
There are no installed workloads to display.

Host:
  Version:      8.0.10
  Architecture: arm64
  Commit:       81cabf2857

.NET SDKs installed:
  6.0.425 [/usr/local/share/dotnet/sdk]
  8.0.403 [/usr/local/share/dotnet/sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 6.0.33 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 8.0.10 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 6.0.33 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 8.0.10 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]

Other architectures found:
  x64   [/usr/local/share/dotnet/x64]
    registered at [/etc/dotnet/install_location_x64]

Environment variables:
  Not set

global.json file:
  Not found

Learn more:
  https://aka.ms/dotnet/info

Download .NET:
  https://aka.ms/dotnet/download

Other information

The workaround looks simple but it took me many hours reading the OpenLDAP source code the to figure it out!

I had to understand that the default LDAP version is V2 but that the command line tools such as ldapsearch works fine because the LDAP version is explicitly set to V3 when the -P option (protocol version) is not specified. The key part was understanding that the LdapException was coming from the protocol version check in the ldap_int_sasl_bind method.

/* do a quick !LDAPv3 check... ldap_sasl_bind will do the rest. */
if (ld->ld_version < LDAP_VERSION3) {
    ld->ld_errno = LDAP_NOT_SUPPORTED;
    return ld->ld_errno;
}

Since the System.DirectoryServices.Protocols package description says a managed implementation of Lightweight Directory Access Protocol (LDAP) version 3 I think it should use the LDAPV3 protocol by default and thus completely prevent this exception.

I'll submit a pull request shortly to address this issue.

dotnet-policy-service[bot] commented 17 hours ago

Tagging subscribers to this area: @dotnet/area-system-directoryservices, @jay98014 See info in area-owners.md if you want to be subscribed.