PHPからActive Directoryに認証・パスワード変更する方法 | A Day In The Boy's Life

A Day In The Boy's Life

とあるエンジニアのとある1日のつぶやき。

IDやパスワードを一元管理するために、Active Directoryと連携したいというケースがあったりしますが、ADへ認証やパスワード変更などをPHPから行うためのメモです。


LDAPを通して簡単に認証やパスワードなどの属性値変更が行えます。
PHPは5.3系を使っています。

Windows側はWindows2008およびWindows2012で動作確認しています。



Active Directoryへ認証するPHPプログラム


これは特に難しいことなく、PHPのldap_bind を使えば認証が行えます。


<?php

$host = "ldaps://192.168.0.100";
$ldapConn = ldap_connect($host);

// userprincipalnameを指定
$userId = "username@testdomain.test";
$userPass = "HogeHoge12";

ldap_set_option($ldapConn, LDAP_OPT_PROTOCOL_VERSION, 3);
if (ldap_bind($ldapConn, $userId, $passwd)) {
    echo "ログイン成功" . PHP_EOL;
} else {
    echo "ログイン失敗" . PHP_EOL;
}
ldap_close($ldapConn);

ポイントとしては、ユーザー名にはuserprincipalnameを指定すること(dnを指定してもできますが)ぐらいで後は認証するldap_bindに引数を渡して結果がTRUE/FALSEで返ってきます。



Active Directoryのパスワードを変更するPHPプログラム


お次はAD上のパスワードを変更する方法ですが、前提としてパスワード変更する際にはAD上にSSL証明書を用意し、LDAPSで接続する必要があります。

SSL証明書は正規のものではなく自己証明書でもかまいませんが、その場合は接続するサーバー側で証明書の検証を無効にするように設定しておく必要があります。


TLS_REQCERT never

上記の設定を、/etc/openldap/ldap.confあたり(環境によっては複数あったりしますが)に記載しておきます。


パスワード変更するための具体的なプログラムは下記の通りです。


<?php

$host      = "ldaps://192.168.0.100";
// 管理権限を持つユーザー名
$adminId   = "administrator@testdomain.test";
// 上記ユーザーのパスワード
$adminPass = "adminpassword";

// パスワード変更対象のユーザー名
$userId   = "username@testdomain.test";
// 変更するパスワード
$userPass = "HogeHoge12345";

// 管理者権限を持つユーザーでADへ接続
$ldapConn = ldap_connect($host);
ldap_set_option($ldapConn, LDAP_OPT_PROTOCOL_VERSION, 3);
if (!ldap_bind($ldapConn, $adminId, $adminPass)) {
    echo "ログイン失敗" . PHP_EOL;
    exit;
}

// ユーザーのDNを取得する
$baseDn = "OU=Employee,OU=Users,DC=testdomain,DC=test";
$filter   = array("dn");
$ls       = ldap_search($ldapConn, $baseDn, "userprincipalname=$userId", $filter);
$userInfo = ldap_get_entries($ldapConn, $ls);

// 設定するパスワードはダブルクォートで括ったものをUTF-16に変換する
// unicodePwdはパスワードを格納しているエントリ名
$entry["unicodePwd"] = mb_convert_encoding("\"" . $userPass . "\"", "UTF-16LE");

if (ldap_mod_replace($ldapConn, $userInfo[0]['dn'], $entry)) {
    echo "変更成功" . PHP_EOL;
} else {
    echo "変更失敗" . PHP_EOL;
}
ldap_close($ldapConn);

パスワードを変更するプログラムは、少し複雑にはなりますが予約すると下記の流れで組んでいます。

(その他の属性の変更も基本同じ流れでいけるはず)


1. パスワードを変更できる管理権限を持つユーザー(ここではadministrator)でADへ接続
2. 変更したいユーザーのDNを取得するためにADを検索
3. 変更したいパスワードをダブルクォーテーションで囲ったものをUTF-16LEで変換
4. 変更したいユーザーのパスワード属性(unicodePwd)に2.のパスワードを変更


2.の処理でDNを検索するのは回りくどいように思えますが、ldap_mod_replace(またはldap_modify)で変更したいユーザー属性やオブジェクトを変更したい場合、DNを指定する必要があるためにそうしています(ldap_bindはuserprincipalnameの指定でもいけるのに何故・・・)


3.の処理はWindows側の仕様のようで、本来であればダブルクォーテーションで囲った平文パスワードをUTF-16LEに変更し、それをBase64エンコードすることになるのですが、Base64へのエンコードはLDAP関数側で自動的にやってくれるので省略可能です(逆にBase64にエンコードしてしまうと二重にエンコードされます)。
この辺の話は、下記に情報があります。


Active Directory: AD LDS における unicodePwd 属性値の謎


また、実運用するならエラーハンドリングをきちんと書いたほうが良いと思いますし、LDAPに渡すフィルタの値などはエスケープ処理をきちんとしておいたほうがよさそうです。

PHPにはldap_escape という関数ができているようですが、PHP5.6以上じゃないと使えません。
これ以下のバージョンを使っている場合は自前で用意するしかなさそうです。


下記のRFCの要約を見る限りは、「* ( ) \ NULL」は円マークでエスケープしたあとにASCII値に変更しろって書いていますので、その辺の処理を組み込む必要がありそうです。


付録 A:RFC2254 - LDAP 検索フィルタ


あと、余談の余談にはなりますけど、PHPのldap_mod_replace のマニュアルを見ていると、設定するパスワードの最後に「\000」をつけていたりしていて、これなんだろうとか思ってましたがどうもNULL文字つけて文字の区切りをはっきりさせているのでしょうか。


$newPassword = "MyPassword";
$newPassword = "\"" . $newPassword . "\"";
$len = strlen($newPassword);
for ($i = 0; $i < $len; $i++)
        $newPassw .= "{$newPassword{$i}}\000";
$newPassword = $newPassw;

この処理はいるのだろうか(無くても動く)と思ったり。