# LDAP 認証

## 導入

Zend_Auth_Adapter_Ldap は、LDAP サービスによるウェブアプリケーションの認証をサポートします。 ユーザ名やドメイン名の正規化や複数ドメインの認証、 フェイルオーバー機能などがあります。 Microsoft Active Directory と OpenLDAP で動作確認をしていますが、それ以外の LDAP サービスプロバイダでも動作するはずです。

このドキュメントで扱う内容は、 Zend_Auth_Adapter_Ldap の使い方やその API、 使用可能なオプション、トラブルシューティングの方法、 そして Active Directory 用および OpenLDAP 用の設定例です。

## 使用法

Zend_Auth_Adapter_Ldap による認証をアプリケーションに手っ取り早く組み込むには、 Zend_Controller を使うかどうかにかかわらず、次のようなコードを書くことになります。

    $username = $this->_request->getParam('username');
    $password = $this->_request->getParam('password');

    $auth = Zend_Auth::getInstance();

    $config = new Zend_Config_Ini('../application/config/config.ini',
                                  'production');
    $log_path = $config->ldap->log_path;
    $options = $config->ldap->toArray();
    unset($options['log_path']);

    $adapter = new Zend_Auth_Adapter_Ldap($options, $username,
                                          $password);

    $result = $auth->authenticate($adapter);

    if ($log_path) {
        $messages = $result->getMessages();

        $logger = new Zend_Log();
        $logger->addWriter(new Zend_Log_Writer_Stream($log_path));
        $filter = new Zend_Log_Filter_Priority(Zend_Log::DEBUG);
        $logger->addFilter($filter);

        foreach ($messages as $i => $message) {
            if ($i-- > 1) { // $messages[2] 以降はログメッセージです
                $message = str_replace("\n", "\n  ", $message);
                $logger->log("Ldap: $i: $message", Zend_Log::DEBUG);
            }
        }
    }

もちろんログを記録するかどうかは自由ですが、 ロガーを使用することを強く推奨します。 Zend_Auth_Adapter_Ldap は、皆がほしがるであろう情報をすべて `$messages` (詳細は以下で) に記録します。 この機能を使用すれば、デバッグを容易に行えるようになります。

上のコードでは、Zend_Config_Ini を用いてアダプタのオプションを読み込んでいます。 これもまた必須ではありません。普通の配列を使用しても同様に動作します。 以下に `application/config/config.ini` ファイルの例を示します。 このファイルでは、ふたつの別のサーバの設定を記述しています。 複数のサーバのオプションを設定しておくと、 アダプタ側では認証に成功するまで順にそれぞれのサーバへの認証を試みます。 サーバの名前 ('server1' や 'server2' など) は任意です。オプション配列についての詳細は、以下の サーバのオプション に関するセクションを参照ください。 Zend_Config_Ini では、等号 (=) を含む値 (以下の例では DN など) はクォートしなければならないことに注意しましょう。

    [production]

    ldap.log_path = /tmp/ldap.log

    ; OpenLDAP 用の設定の例
    ldap.server1.host = s0.foo.net
    ldap.server1.accountDomainName = foo.net
    ldap.server1.accountDomainNameShort = FOO
    ldap.server1.accountCanonicalForm = 3
    ldap.server1.username = "CN=user1,DC=foo,DC=net"
    ldap.server1.password = pass1
    ldap.server1.baseDn = "OU=Sales,DC=foo,DC=net"
    ldap.server1.bindRequiresDn = true

    ; Active Directory 用の設定の例
    ldap.server2.host = dc1.w.net
    ldap.server2.useStartTls = true
    ldap.server2.accountDomainName = w.net
    ldap.server2.accountDomainNameShort = W
    ldap.server2.accountCanonicalForm = 3
    ldap.server2.baseDn = "CN=Users,DC=w,DC=net"

この設定を使用すると、Zend_Auth_Adapter_Ldap はまず OpenLDAP サーバ `s0.foo.net` でのユーザ認証を試みます。 何らかの理由で認証に失敗した場合は、AD サーバ `dc1.w.net` を用いて認証を試みます。

異なるドメインのサーバを指定したことで、 この設定では複数ドメインの認証を行えるようになっています。 同一ドメイン内の複数サーバを指定して冗長性を確保することもできます。

この場合、OpenLDAP には短い形式の NetBIOS ドメイン名 (Windows で使用するもの) は不要ですが、設定していることに注意しましょう。これは、名前の正規化のために使用します (以下の ユーザ名の正規化 のセクションを参照ください)。

## API

Zend_Auth_Adapter_Ldap のコンストラクタは、3 つのパラメータを受け取ります。

`$options` パラメータは必須で、 ひとつあるいは複数のオプションを含む配列でなければなりません。 これは、Zend_Ldap のオプションの 配列の配列 であることに注意しましょう。 単一の LDAP サーバの設定のみを指定する場合でも、 「設定オプションの配列を配列の中に格納する」形式でなければなりません。

以下に、サンプルのオプションパラメータを print_r() で出力した例を示します。これは、ふたつの LDAP サーバ `s0.foo.net` と `dc1.w.net` の設定を含むものです (先ほどの INI ファイルと同じ設定です)。

    Array
    (
        [server2] => Array
            (
                [host] => dc1.w.net
                [useStartTls] => 1
                [accountDomainName] => w.net
                [accountDomainNameShort] => W
                [accountCanonicalForm] => 3
                [baseDn] => CN=Users,DC=w,DC=net
            )

        [server1] => Array
            (
                [host] => s0.foo.net
                [accountDomainName] => foo.net
                [accountDomainNameShort] => FOO
                [accountCanonicalForm] => 3
                [username] => CN=user1,DC=foo,DC=net
                [password] => pass1
                [baseDn] => OU=Sales,DC=foo,DC=net
                [bindRequiresDn] => 1
            )

    )

上の各オプションで設定した内容の違いの主な理由は、AD へのバインド時にはユーザ名が DN 形式である必要がないということです (以下の サーバのオプション における bindRequiresDn の説明を参照ください)。 つまり、認証時のユーザ名から DN を取得するために使用する多くのオプションは 省略できるということです。

Distinguished Name とは?

DN ("distinguished name") とは、 LDAP ディレクトリ内のオブジェクトへのパスを表す文字列のことです。 カンマで区切られた各部分が、ノードを表す属性と値となります。 各部分は逆順に評価されます。たとえば、ユーザアカウント CN=Bob Carter,CN=Users,DC=w,DC=net は、ディレクトリ CN=Users,DC=w,DC=net container の配下に位置することになります。 この構造をたどるには、ADSI Edit MMC snap-in for Active Directory や phpLDAPadmin といった LDAP ブラウザが最適です。

サーバの名前 (上の例における 'server1' や 'server2') は基本的には何でもかまいません。しかし、Zend_Config を用いる場合は、 (数値インデックスではなく) 識別子を使用しなければなりません。また、 各ファイルフォーマットで特別な意味を持つ文字 (INI のプロパティ区切り文字 '.' や XML エンティティ参照の '&' など) は含まないようにしましょう。

複数のサーバオプションを設定しておけば、 このアダプタで複数ドメインのユーザ認証を行うことができます。 また、ひとつのサーバが使用できない場合に別のサーバに問い合わせを行う フェイルオーバー機能も提供できます。

認証メソッドの中では実際に何が行われているのか?

authenticate() メソッドがコールされると、 アダプタは各サーバ設定を順に処理し、内部で管理する Zend_Ldap のインスタンスに設定したうえでユーザ名とパスワードを指定して Zend_Ldap::bind() メソッドをコールします。 Zend_Ldap クラスは、そのユーザ名がドメインつきのものであるかどうか (`alice@foo.net` や `FOO\alice` といった形式であるかどうか) を調べます。ドメインが指定されているけれどもそれがどのサーバのドメイン名 (`foo.net` あるいは FOO) とも一致しない場合は、特別な例外がスローされます。この例外は Zend_Auth_Adapter_Ldap で捕捉され、 そのサーバを無視して次に指定されているサーバ設定を利用するようにします。 ドメインがマッチ しない 場合、 あるいはユーザがドメインつきのユーザ名を指定しなかった場合は、 Zend_Ldap は指定された認証情報でのバインドを試みます。 バインドに失敗した場合は Zend_Ldap は Zend_Ldap_Exception をスローします。これは Zend_Auth_Adapter_Ldap で捕捉され、 次に設定されているサーバでの認証を試みます。 バインドが成功した場合はそこで処理を終了し、アダプタの authenticate() メソッドは成功したという結果を返します。 設定されているサーバをすべて試したけれどもどれも成功しなかったという場合は、 認証は失敗し、authenticate() は最後のエラーメッセージとともにその結果を返します。

Zend_Auth_Adapter_Ldap コンストラクタのパラメータに渡す ユーザ名とパスワードは、認証に用いる情報 (つまり、HTML のログインフォームでユーザが入力した情報) を表します。これらは、setUsername() メソッドと setPassword() メソッドで指定することもできます。

## サーバのオプション

Zend_Auth_Adapter_Ldap のコンテキストにおける サーバのオプションは次のようなものです。これらは、ほとんどそのままの形で Zend_Ldap::setOptions() に渡されます。

サーバのオプション

名前 | 説明
--- | ---
host | このオプションが表す LDAP サーバのホスト名。必須です。
port | LDAP サーバが待ち受けるポート。useSsl が `TRUE` の場合、デフォルトの port は 636 となります。useSsl が `FALSE` の場合、デフォルトの port は 389 です。
useStartTls | LDAP クライアントが TLS (SSLv2) で暗号化されたトランスポートを用いるかどうか。 実運用環境では、この値を `TRUE` にすることを強く推奨します。 そうすれば、パスワードが平文で転送されることを防ぐことができます。 デフォルト値は `FALSE` です。 というのも、別途証明書のインストールを要するサーバが多く存在するからです。 useSsl と useStartTls は互いに排他的です。 useStartTls オプションのほうが useSsl よりおすすめですが、中にはこの新しい仕組みをサポートしていないサーバもあります。
useSsl | LDAP クライアントが SSL で暗号化されたトランスポートを用いるかどうか。 useSsl と useStartTls は互いに排他的ですが、 サーバや LDAP クライアントライブラリが対応している場合は useStartTls を使うことを推奨します。 この値によって、デフォルトの port の値が変わります (上の port の説明を参照ください)。
username | アカウントの DN を探す際に使用するアカウントの DN。 バインド時のユーザ名が DN 形式であることを要求する LDAP サーバで、このオプションを使用します。 bindRequiresDn が `TRUE` の場合はこのオプションが必須となります。 このアカウントは特権アカウントである必要はありません。baseDn 配下のオブジェクトに対する読み込み権限がありさえすればいいのです (これは Principle of Least Privilege: 最小特権の原則 にもかなっています)。
password | アカウントの DN を探す際に使用するアカウントのパスワード。 このオプションを省略した場合は、LDAP クライアントがアカウントの DN を探す際に "匿名バインド" を試みます。
bindRequiresDn | LDAP サーバによっては、バインド時に使用するユーザ名が CN=Alice Baker,OU=Sales,DC=foo,DC=net のような DN 形式でなければならないものもあります (基本的に、AD 以外 のすべてのサーバがそうです)。 このオプションが `TRUE` の場合、 もし認証対象のユーザ名が DN 形式でなければ Zend_Ldap に自動的に DN を取得させ、 その DN で再度バインドさせるようにします。 デフォルト値は `FALSE` です。現時点で、 バインド時のユーザ名が DN 形式で なくてもよい サーバとして知られているのは Microsoft Active Directory Server (ADS) のみです。したがって、AD を使用する場合はこのオプションを `FALSE` にしてもかまいません (そうするべきです。 DN を取得するために、サーバとの余計なやりとりが発生してしまうわけですから)。 それ以外の場合 (OpenLDAP など) は、このオプションを `TRUE` にしなければなりません。このオプションは、 アカウントを検索する際に使用する acountFilterFormat のデフォルト値にも影響を及ぼします。 accountFilterFormat オプションも参照ください。
baseDn | 認証対象となるアカウントが配置されている場所の DN。このオプションは必須です。 正しい baseDn の値がよくわからない場合は、 ユーザの DNS ドメインを DC= コンポーネントで表したものと考えれば差し支えないでしょう。 たとえば、ユーザ名が `alice@foo.net` である場合は baseDn を DC=foo,DC=net とすれば動作するでしょう。しかし、より正確な場所 (OU=Sales,DC=foo,DC=net など) を指定したほうが効率的です。
accountCanonicalForm | 2、3 あるいは 4 を指定し、認証に成功した後のアカウント名の正規化方式を指定します。 それぞれの値の意味は次のとおりです。2 は伝統的なユーザ名 (例: alice)、3 はバックスラッシュ形式の名前 (例: `FOO\alice`) そして 4 はプリンシパル形式のユーザ名 (例: `alice@foo.net`) となります。デフォルト値は 4 (例: `alice@foo.net`) です。 たとえば 3 を指定したとすると、 Zend_Auth_Result::getIdentity() (Zend_Auth を使う場合は Zend_Auth::getIdentity()) の返す識別子は常に FOO\alice となります。 これは、Alice が入力した内容が `alice`、 `alice@foo.net`、`FOO\alice`、 `FoO\aLicE`、`foo.net\alice` などのいずれであろうが同じです。詳細は、Zend_Ldap のドキュメントの アカウント名の正規化 のセクションを参照ください。複数のサーバのオプションを設定する場合は、 すべてのサーバで accountCanonicalForm を同じにしておくことを推奨します (必須ではありません)。 そうすれば、結果のユーザ名はいつでも同じ形式に正規化されることになります (もし AD サーバでは `EXAMPLE\username`、OpenLDAP サーバでは `username@example.com` を返すようになっていれば、 アプリケーション側のロジックが不格好になります)。
accountDomainName | 対象となる LDAP サーバの FQDN ドメイン (例 `example.com`)。 このオプションは、名前を正規化する際に使用します。 バインド時に、ユーザが指定したユーザ名を必要に応じて変換します。 指定したユーザ名がそのサーバに存在するかどうかを調べる際にも使用します (accountDomainName が foo.net でユーザが bob@bar.net を入力した場合、 サーバへの問い合わせを行わず、結果は失敗となります)。 このオプションは必須ではありませんが、もし指定していなければ プリンシパル形式のユーザ名 (例 `alice@foo.net`) はサポートされません。このオプションを指定しておくことを推奨します。 プリンシパル形式のユーザ名が必要となる場面は数多くあるからです。
accountDomainNameShort | 対象となる LDAP サーバの '短い' ドメイン (例 FOO)。 accountDomainName と accountDomainNameShort は一対一対応となることに注意しましょう。このオプションは Windows ネットワークの NetBIOS ドメイン名として用いられますが、 AD 以外のサーバで用いられることもあります (複数のサーバオプションでバックスラッシュ形式の accountCanonicalForm を使用する場合など)。 このオプションは必須ではありませんが、もし指定していなければ バックスラッシュ形式のユーザ名 (例 `FOO\alice`) はサポートされません。
accountFilterFormat | アカウントを検索する際に使用する LDAP 検索フィルタ。 この文字列は printf() 形式のものとなり、ユーザ名を表す '%s' をひとつ含む必要があります。デフォルト値は '(&(objectClass=user)(sAMAccountName=%s))' です。 ただし、bindRequiresDn が `TRUE` の場合のデフォルト値は '(&(objectClass=posixAccount)(uid=%s))' となります。たとえば、何らかの理由で AD 環境で bindRequiresDn = true を使いたい場合は accountFilterFormat = '(&(objectClass=user)(sAMAccountName=%s))' と設定する必要があります。
optReferrals | `TRUE` に設定すると、 参照先を追跡するよう LDAP クライアントに指示します。 デフォルト値は `FALSE` です。

useStartTls = `TRUE` あるいは useSsl = `TRUE` としていると、LDAP クライアント側で 「サーバの証明書を検証できない」というエラーが発生することに気づかれるかもしれません。 PHP の LDAP 拡張モジュールは OpenLDAP クライアントライブラリと密接につながっているので、 この問題を解決するには OpenLDAP クライアントの `ldap.conf` で "`TLS_REQCERT never`" を設定 (そしてウェブサーバを再起動) して OpenLDAP クライアントライブラリがサーバを信頼するようにします。 もしいわゆる「なりすまし」が心配なら、LDAP サーバのルート証明書をエクスポートしてそれをウェブサーバに配置すれば、 OpenLDAP クライアントがサーバを検証できるようになります。

## デバッグメッセージの収集

Zend_Auth_Adapter_Ldap は、authenticate() メソッド内でのデバッグ情報を収集します。この情報は、Zend_Auth_Result オブジェクト内にメッセージとして保存されます。 Zend_Auth_Result::getMessages() が返す配列は次のような形式になります。

デバッグメッセージ

メッセージ配列の添字 | 説明
--- | ---
0 | ユーザ向けの表示に適した、全般的なメッセージ (認証に失敗したなど)。 認証に成功した場合は、この文字列は空となります。
1 | より詳細なエラーメッセージ。ユーザ向けに表示するには適しませんが、 サーバ管理者向けには記録しておくべき内容です。 認証に成功した場合は、この文字列は空となります。
2 以降 | すべてのログメッセージが、インデックス 2 以降に順に格納されます。

実際に使用する上では、まずインデックス 0 の内容はユーザ向けに表示することになります (FlashMessenger ヘルパーなどを使用します)。そしてインデックス 1 はログに記録し、 デバッグ情報が必要ならインデックス 2 以降も同様に記録します (最後のメッセージには、常にインデックス 1 の内容も含まれています)。

## サーバ固有の共通オプション

### Active Directory 用のオプション

ADS 用のオプションとして注目すべきものは次のとおりです。

Active Directory 用のオプション

名前 | 補足説明
--- | ---
host | すべてのサーバでこのオプションは必須です。
useStartTls | セキュリティの観点からは、これは `TRUE` にしておくべきです。 この場合、サーバに証明書をインストールしておく必要があります。
useSsl | useStartTls の代替として用いられます (上を参照ください)。
baseDn | すべてのサーバでこのオプションは必須です。デフォルトの AD では すべてのユーザアカウントが Users コンテナ (たとえば CN=Users,DC=foo,DC=net) の配下におかれますが、 もっと長い組織になることもあるので共通のデフォルトはありません。 AD の管理者に問い合わせて、アプリケーションのアカウントでどんな DN を使用したらよいのかを確認しましょう。
accountCanonicalForm | ほとんどの場合は 3 を指定してバックスラッシュ形式の名前 (例 FOO\alice) を使用することになるでしょう。 これは Windows ユーザにとってもっともなじみ深い形式です。修飾されていない形式である 2 (例 alice) を使っては いけません。 これは、他の信頼済みドメインに属する同じユーザ名のユーザにも アプリケーションへのアクセスを許可してしまうことになるからです (たとえば BAR\alice と FOO\alice は同じユーザという扱いになります)。以下の注意も参照ください。
accountDomainName | これは AD には必須です。accountCanonicalForm が 2 の場合は不要ですが、何度も言うようにこれはおすすめしません。
accountDomainNameShort | ユーザが属するドメインの NetBIOS 名で、AD サーバの認証対象となります。 これは、バックスラッシュ形式の accountCanonicalForm を使用する場合には必須です。

技術的には、現在の Zend_Auth_Adapter_Ldap の実装では 意図せぬクロスドメイン認証の危険はあり得ません。 サーバのドメインが明示的にチェックされるからです。 しかし、将来にわたってもそうであるかどうかはわかりません。 実行時にドメインを見つけるような実装に変わったり、 別のアダプタ (Kerberos など) を使うことになるかもしれません。 一般論として、あいまいなアカウント名はセキュリティ問題の原因となりやすいものです。 修飾した形式のアカウント名を使うようにしましょう。

### OpenLDAP 用のオプション

OpenLDAP、あるいは posixAccount 形式のスキーマを用いる一般的な LDAP サーバ用のオプションとして注目すべきものは次のとおりです。

OpenLDAP 用のオプション

名前 | 補足説明
--- | ---
host | すべてのサーバでこのオプションは必須です。
useStartTls | セキュリティの観点からは、これは `TRUE` にしておくべきです。 この場合、サーバに証明書をインストールしておく必要があります。
useSsl | useStartTls の代替として用いられます (上を参照ください)。
username | 必須、かつ DN である必要があります。OpenLDAP のバインド時には、 ユーザ名が DN 形式であることが必須だからです。 特権アカウント以外を使用するようにしましょう。
password | 上のユーザ名に対応するパスワード。しかし、 匿名バインドによるユーザ検索を LDAP サーバがサポートしている場合には省略することもできます。
bindRequiresDn | 必須、かつ `TRUE` である必要があります。 OpenLDAP のバインド時には、ユーザ名が DN 形式であることが必須だからです。
baseDn | すべてのサーバでこのオプションは必須です。 認証対象となるアカウントが位置する DN を指すようにします。
accountCanonicalForm | オプションで、デフォルト値は 4 (`alice@foo.net` のようなプリンシパル形式) です。これは、ユーザがバックスラッシュ形式の名前 (`FOO\alice` など) を使用する場合には望ましくありません。バックスラッシュ形式の名前の場合は 3 を使用します。
accountDomainName | 必須です。accountCanonicalForm が 2 の場合は不要ですが、これはおすすめしません。
accountDomainNameShort | AD とともに使用するのでなければこれは必須ではありません。 それ以外の場合、もし accountCanonicalForm 3 を使用するのなら このオプションは必須で、 accountDomainName に対応する短縮名を指定しなければなりません (たとえば accountDomainName が `foo.net` なら accountDomainNameShort の適切な値は FOO となるでしょう)。