Mac + C# + .NET Core3.1で、ActiveDirectoryのLDAP認証をしてみた

Mac .NET Core 3.1のアプリを開発する中、ユーザーがActiveDirectory (以降、AD)に参加していない環境で、ADサーバーを使ったLDAP認証をする機会があったため、メモを残します。

 
目次

 

環境

  • Mac OSX 10.14.6
  • .NET Core 3.1
  • Novell.Directory.Ldap.NETStandard 3.0.1

 
また、ADまわりは以下の環境です。

項目
ADサーバ Windows Server 2016
ドメインの機能 2016
Macユーザ ドメインに不参加、ただしADユーザのIDとパスワードは分かる
Mac ドメインのコンピュータとして参加済、ただしドメインのComputersへ登録のみ
AD - Mac間接続 直接 (Proxyなし)
ADドメイン sub.example.co.jp

 
ADユーザ情報はこんな感じ

項目
ユーザーログオン名 foo_user@sub.example.co.jp
ユーザーログオン名(Windows2000以前) foo_user
foo
bar
表示名 foo bar

 

そもそも、なぜWindows認証ではないのか

ADを使った認証というと、まずはWindows認証が思い浮かびます。

ASP.NET Core 3.1でWindows認証が可能な方法を調べたところ、以下の3つでした。
ASP.NET Core での Windows 認証を構成します。 | Microsoft Docs

  • IIS (IIS Express)
  • HTTP.sys
  • Kestrel

 
このうち、Mac上で ASP.NET Coreをホストして使えるのは、 Kestrel だけでした。

とはいえ、Kestrelは機能不足なところがありそうなので、できれば避けたいと考えました。
ASP.NET Coreを動かすためのIISの構築方法 - Qiita

 
他に方法がないか考えたところ、以前PythonRubyLDAP認証を使っていたことを思い出しました。

 
そこで、 .NET Coreで使えるLDAP認証ライブラリを探したところ、 dsbenghe/Novell.Directory.Ldap.NETStandard がありました。
dsbenghe/Novell.Directory.Ldap.NETStandard: LDAP client library for .NET Standard 1.3 and up - works with any LDAP protocol compatible directory server (including Microsoft Active Directory).

最近リリースされたばかり(2020年1月下旬)だったので、以下を参考に試してみました。
.NET Core LDAP authentication

 

準備

ソリューションとプロジェクトの作成

今回は、コンソールアプリケーションで作ります。

$ dotnet new sln -n LdapAuth

$ dotnet new console -o LdapAuthConsole

$ dotnet sln add ./LdapAuthConsole

 

NuGetでインストール

Novell.Directory.Ldap.NETStandard をインストールします。

$ cd LdapAuthConsole/
$ dotnet add package Novell.Directory.Ldap.NETStandard

 

使い方の流れ

コネクションの作成

まずはLDAPコネクションを作成します。

using (var connection = new LdapConnection { SecureSocketLayer = false })

 

Bind

続いてBindします。

この時、ADのユーザIDとパスワードが必要になります。Macのユーザとは異なっていて問題ないです。

connection.Connect(ipAddress, port);
connection.Bind(userDn, password);

 
接続が成功した場合は、 connection.Boundtrue になります。

 

ADの検索

検索するには、 connection.Search() を使います。

引数は5つあります。

 

第一引数 (searchBase)

どこを探すかを指定します。

今回はドメインすべてを検索するため、 dc=sub, dc=example, dc=co, dc=jp を指定します。

なお、dcの数を動的に指定したかったため、以下のような関数を用意しました。

これで、 sub.example.co.jpdc=sub, dc=example, dc=co, dc=jp になります。

static string ConvertDomain(string domain)
{
    return domain.Split(".")
        .Select(s => $"dc={s}")
        .Aggregate((result, element) => $"{result}, {element}");
}

 

第二引数 (LdapConnectionクラス)

どの範囲で検索するかを指定します。

今回は、ドメイン内全てを検索するため、 LdapConnection.ScopeSub とします*1

 

第三引数 (フィルタ)

絞り込み条件を指定します。

今回は、ADユーザのログインIDをキーにデータを取得するため、 SAMAccountName を使います。

またあいまい検索もしたいため、 * を使った

$"(SAMAccountName=*{userName}*)"

を指定します。

 

第四引数 (取得したい項目)

ADから取得したい項目をここで指定します。

どのような値を指定できるかは以下が参考になりました。

 
今回は以下を指定しました。

new []
{
    "displayName",        // 表示名
    "cn",                 // 表示名と同じ
    "sn",                 // 姓
    "givenName",          // 名
    "userPrincipalName",  // ユーザーログオン名
    "sAMAccountName",     // ユーザーログオン名(Windows 2000以前)
    "description"         // 説明
},

 
主な引数はこんな感じです。

 

値の取得

Search()で取得した値は、 GetAttribute() を使って取得できます。

var user = result.Next();
var displayName = user.GetAttribute("displayName").StringValue;

 

ソースコード全体

ここまでをまとめると以下の感じです。

using System;
using System.Linq;
using Novell.Directory.Ldap;

namespace LdapAuthConsole
{
    class Program
    {
        static string ConvertDomain(string domain)
        {
            var r = domain.Split(".")
                .Select(s => $"dc={s}")
                .Aggregate((result, element) => $"{result}, {element}");
            
            Console.WriteLine(r);
            return r;
        }
        
        static void Main(string[] args)
        {
            var userName = "foo_user";  // Domain Users
            var password = "YOUR_PASSWORD";
            var domain = "sub.example.co.jp";
            var ipAddress = "192.168.xxx.xxx";
            var port = 389;
            var userDn = $"{userName}@{domain}";
            
            try
            {
                using (var connection = new LdapConnection { SecureSocketLayer = false })
                {
                    connection.Connect(ipAddress, port);
                    connection.Bind(userDn, password);

                    if (connection.Bound){
                        Console.WriteLine("接続できました");
                        
                        // 検索
                        var searchBase = ConvertDomain(domain);
                        
                        // あいまい検索が使える
                        var searchFilter = $"(SAMAccountName=*{userName}*)";
                        var result = connection.Search(
                            searchBase,
                            // ドメイン直下からすべてのサブを調べる
                            LdapConnection.ScopeSub,
                            searchFilter,
                            new []
                            {
                                "displayName",        // 表示名
                                "cn",                 // 表示名と同じ
                                "sn",                 // 姓
                                "givenName",          // 名
                                "userPrincipalName",  // ユーザーログオン名
                                "sAMAccountName",     // ユーザーログオン名(Windows 2000以前)
                                "description"         // 説明
                            },
                            false
                        );
                        var user = result.Next();
                        var displayName = user.GetAttribute("displayName").StringValue;
                        
                        Console.WriteLine(user);
                        Console.WriteLine(displayName);
                    }
                    else
                    {
                        Console.WriteLine("接続できませんでした");
                    }
                }
            }
            catch (LdapException ex)
            {
                // Log exception
                // TODO 例外処理を実装
                Console.WriteLine("例外が出ました");
                Console.WriteLine(ex);
            }
        }
    }
}

 

結果

実行すると、MacでADユーザを使っていなくても、LDAP認証を使ってADから情報を取得できました。

$ dotnet run
接続できました
dc=sub, dc=example, dc=co, dc=jp

# LDAPで取得した中身
LdapEntry: CN=foo bar,CN=Users,DC=sub,DC=example,DC=co,DC=jp; LdapAttributeSet: LdapAttribute: {type='cn', value='foo bar'} LdapAttribute: {type='sn', value='foo'} LdapAttribute: {type='givenName', value='bar'} LdapAttribute: {type='displayName', value='foo bar'} LdapAttribute: {type='sAMAccountName', value='foo_user'} LdapAttribute: {type='userPrincipalName', value='foo_user@ad.jsl.co.jp'}

# GetAttribute()の結果
foo bar

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi-sandbox/CSharp_Ldap_Auth-sample

*1:2系は大文字の名前でしたが、3系はCamelCaseになりました