LDAP でユーザー認証

.NET Framework の LdapConnection を使って、LDAP でユーザー認証するサンプルコード。
このままだと、指定 DN 直下にいるユーザーでのみ認証できる。
DN を固定せず認証したい場合は、一旦 LDAP を検索可能かつ DN が判明しているアカウントでバインドしてユーザー名を検索したのち、そうして得た認証対象ユーザーの DN とパスワードを使ってバインドしてみる、って流れになる。

```cs
using System;
using System.DirectoryServices.Protocols;
using System.Text.RegularExpressions;
using System.Collections;
using System.Collections.Generic;
using System.Net;

namespace LdapTest
{
    public static class LdapAuthnTest
    {
        /// <summary>
        /// LDAP で認証する。
        /// </summary>
        /// <param name="userId">ユーザー名</param>
        /// <param name="userPassword">ユーザーのパスワード</param>
        /// <param name="userAttr">DN に指定されている属性 (多くの場合、CN)</param>
        /// <param name="baseDN">ベース DN</param>
        /// <param name="ldapServer">LDAP サーバーのホスト名または IP アドレス</param>
        /// <param name="isTls">LDAPS にする場合 true。LDAP のままにする場合 false。</param>
        /// <returns>認証成功でそのユーザーの <see cref="SearchResultEntry" />、認証失敗は null。</returns>
        /// <exception cref="LdapException">LDAP 例外</exception>
        public static SearchResultEntry LdapAuthn(string userId, string userPassword, string userAttr, string baseDN, string ldapServer, bool isTls)
        {
            // 認証情報が空の場合は即認証失敗
            if (userId.Length < 1 || userPassword.Length < 1)
            {
                return null;
            }

            ldapServer = ServerPortSpecify(ldapServer, isTls);

            LdapConnection ldapConnection = new LdapConnection(ldapServer) {
                Credential = new NetworkCredential(userAttr + "=" + LdapEscape(userId) + "," + baseDN, userPassword),
                AuthType = AuthType.Basic,
                Timeout = new TimeSpan(0, 0, 10)
            };
            ldapConnection.SessionOptions.ProtocolVersion = 3;
            ldapConnection.SessionOptions.SecureSocketLayer = isTls;

            SearchResultEntry searchResultEntry = null;
            try
            {

                // 認証したいユーザーでバインドしてみる
                ldapConnection.Bind();

                // バインドが通ったらユーザー情報を得る
                SearchRequest searchRequest = new SearchRequest()
                {
                    DistinguishedName = baseDN,
                    Filter = "(" + userAttr + "=" + userId + ")"
                };
                SearchResponse searchResponse = ldapConnection.SendRequest(searchRequest) as SearchResponse;

                if (searchResponse.Entries.Count == 1)
                {
                    foreach (SearchResultEntry item in searchResponse.Entries)
                    {
                        searchResultEntry = item;
                        break;
                    }
                }

                if (searchResultEntry == null)
                {
                    // 念のため。この例外がスローされることはない。
                    throw new ArgumentOutOfRangeException(searchResponse.Entries.Count.ToString());
                }
            }
            catch (LdapException e)
            {
                if (e.ErrorCode != 49) // エラー コード 49 は「認証失敗」
                {
                    throw e;
                }
                else
                {
                    System.Diagnostics.Debug.WriteLine(e.Message);
                    System.Diagnostics.Debug.WriteLine(e.ServerErrorMessage);
                }
            }
            finally
            {
                if (ldapConnection != null)
                {
                    try
                    {
                        ldapConnection.Dispose();
                    }
                    catch (Exception e)
                    {
                        System.Diagnostics.Debug.WriteLine(e);
                    }
                }
            }

            return searchResultEntry;
        }

        /// <summary>
        /// TCP ポート指定なし、かつ、LDAPS のときはポート指定「:636」を追加する。
        /// </summary>
        /// <param name="ldapServer">LDAP サーバー指定</param>
        private static string ServerPortSpecify(string ldapServer, bool isTls)
        {
            if (isTls && !Regex.IsMatch(ldapServer, ":[1-9][0-9]*$")) return ldapServer + ":636";
            else return ldapServer;
        }

        /// <summary>
        /// 正常に処理できるよう、特殊文字をエスケープする。(RFC2253)
        /// </summary>
        /// <param name="ldapValue">エスケープ処理前の文字列</param>
        /// <returns>エスケープ処理済みの文字列</returns>
        static string LdapEscape(string ldapValue)
        {
            return "\"" + ldapValue.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
        }
    }
}
```