Ext JS に関しては,新しい記事は Sunvisor Lab. ExtJS 別館 にあります。そちらもよろしくお願いいたします。

LDAP認証とActive Directory

Webアプリを作る上では避けられないのがユーザー認証です。アプリケーションを利用できる人を制限したり,人によって利用できる機能を制限したりするためにはユーザー認証は必須になります。

CakePHP(1.1)のリファレンスガイドの付録Bには

現在のところ私たちは,ユーザ認証システムはアプリケーションごとに仕様が異なると考えて います。あるものはハッシュでパスワードを暗号化,別のものは LDAP 認証などという具合,そしてほぼすべてのアプリケーションの User モデルについて,少しずつ違いがある,といったことがあります。それで今のところは,あなた次第,ということにしておきます。

とあります。そういろいろな仕様があると思います。僕の開発するアプリケーションで必要となる認証方法は,DBにユーザー情報を持っておいて,それ でログインするというものと,LDAPサーバー(Windows Server 2003 のActive Directory)の認証とを組み合わせたものです。

  1. ユーザー情報はMySQLのテーブルに持つ
  2. DB認証のパスワードはハッシュで暗号化して保存する
  3. DB認証のパスワードが空の場合はLDAPで認証する
  4. DBにユーザー情報がない場合LDAPで認証できたらDBにユーザー情報を自動作成できる

このような仕様のユーザー認証を作成するのを目標にしたいと思います。

XAMPPでLDAPを利用できるようにする

最初に開発環境でLDAPが利用できるようにする必要があります。xamppでLDAPを利用できるようにするためには,php.iniの変更が必 要です。コマンドラインのためには,c:\xampp\php\php.iniを,Apacheのためにはc:\xampp\apache\bin \php.iniを編集します。

;extension=php_ldap.dll

エディタで上記の行を検索して,この行のコメントを外して保存します。Apacheを再起動します。

LDAP Models in CakePHP

http://bakery.cakephp.org/articles/view/ldap-models-in-cakephp にpsychicことJohn David Anderson氏が書いている記事が大変参考になりましたのでその概要を日本語で書いてみます。

多くの組織では今日クライアントや従業員の情報を保存するのにLDAPを利用しています。このチュートリアルは,アプリケーションからCakePHPのModelとしてそのデータを利用する方法を紹介します。

L is for Lightweight

それほどLDAPに詳しくない場合でも,LDAPへの接続・問い合わせ・データの取得にはどうしたらようかを理解する助けになるいくつかのポイントがあります。LDAPは本当にかなり軽量です,そしてその動作の基本を理解することは,大いにためになるでしょう。
LDAPはツリー状の構造に保存されます。ツリーの中でノードを特定するには親ノードを特定する構文を使います。LDAPツリーのルートは,通常,,「cn=cakephp,cn=org」のように収容されるドメイン名にちなんで名付けられてます。次のような表現は

ou=People,cn=example,cn=com 

LDAPツリーのルートにある,"People"というOU(organizatinal unit)をあらわします。

LDAPには多くのタイプのエントリーがありますが,LDAPツリーの最も一般的な用途の一つは個人情報を保存することです。個人レコードをLDAPサーバに問い合わせたとき,そのレコードはツリー形式で返ります。PHPでは,ネストした配列で返ります。

D is for Down and Dirty

はじめにテーブルを使用しないCakePHPモデルを作成します。そのためにはModelクラスの$useTableプロパティを使います。また,LDAPの接続設定を定義するいくつかのクラス変数を宣言します。

<?php  
class LdapUser extends AppModel 
{ 
    var $name = 'LdapUser'; 
    var $useTable = false; 
 
    var $host       = 'ldap.example.com'; 
    var $port       = 389; 
    var $baseDn = 'dc=example,dc=com'; 
    var $user       = 'cn=admin,dc=example,dc=com'; 
    var $pass       = 'password'; 
 
    var $ds; 
} 
?>

LDAPに詳しくない人のために,これらのセッティングについて説明させてください。hostとportは,そのまんまです。baseDn は,LDAPツリーのルートがどこにあるかを指定します。baseDNを一般公開していないサーバーの場合は,管理者に確かめる必要があります。
$userと$passは,ログインに必要な認証情報です。$ds変数は,接続したLDAPサーバーへのリンクを保存します。

次に,ModelがCakePHPによってロードされるときにLDAPサーバーに接続するようにコンストラクタを修正する必要があります。

 

<?php  
//ldap_user.php (partial) 
 
function __construct() 
{ 
    parent::__construct(); 
    $this->ds = ldap_connect($this->host, $this->port); 
    ldap_set_option($this->ds, LDAP_OPT_PROTOCOL_VERSION, 3); 
    ldap_bind($this->ds, $this->user, $this->pass); 
} 
 
function __destruct() 
{ 
    ldap_close($this->ds); 
} 
?>

A is for Application

セットアップが完了した今,Modelに2,3のアプリケーションに役立つ関数を作成することができます。最初の業務予定は,多分,一定の条件に よってデータを取ってくる関数を作成することでしょう。私が作成した最初のものは,ある属性の値に基づくレコードのセットを返すfindAll()関数で した。

 

<?php  
function findAll($attribute = 'uid', $value = '*', $baseDn = 'ou=People,dc=example,dc=com') 
{ 
    $r = ldap_search($this->ds, $baseDn, $attribute . '=' . $value); 
 
    if ($r) 
    { 
        //if the result contains entries with surnames, 
        //sort by surname: 
        ldap_sort($this->ds, $r, "sn"); 
 
        return ldap_get_entries($this->ds, $r); 
    } 
} 
?>

この関数はデフォルトでは与えられたユーザ名と値に基づくレコードセット("People"というOU下のLDAPツリー上にあるものすべて)をとって来ます。またレコードの姓(sn)によってソートされて返されます。
コントローラでは,多くのやり方で利用できます,:

 

<?php  
//Get all users: 
$this->LdapUser->findAll('uid', '*'); 
 
//Get users from a specific group inside the 'People' group: 
$this->LdapUser->findAll('uid', '*', 'ou=Client Company,ou=People,cn=example,cn=com'); 
 
//Get a specific user: 
$this->LdapUser->findAll('uid', 'jsmith'); 
?>

もう一つの使い勝手の良い関数は auth()関数です。
これは特にイントラネットアプリケーションで便利です。ユーザーはたぶん既にLDAPをメールなどのサービスの認証に使用しています。新しいCakePHPによるイントラネットアプリケーションを使うのにそれらと同じユーザ名とパスワードとを使わない手はありません。
この関数はコンポーネントかコントローラにあるべきかもしれませんが,ModelにはLDAP接続情報が既にありますので,私は認証関数をこのModelクラスに置くことにしました。

注:認証をする際には,セキュアな(SSL)LDAP接続をするために異なるポートで接続させたいかもしれません。

 

<?php  
function auth($uid, $password) 
{ 
    $result = $this->findAll('uid', $uid); 
 
    if($result[0]) 
    { 
        if (ldap_bind($this->ds, $result[0]['dn'], $password)) 
            { 
                return true; 
            } 
            else 
            { 
                return false; 
            } 
    } 
    else 
    { 
        return false; 
    } 
} 
?>

簡単ですね。ユーザーを見つけるのにfindAll()関数を使います。このユーザ情報はLDAPサーバがユーザを特定するのに使用するDN(また はdistinguished name)を含んでいます。このステップを取る理由は,Bobのログイン情報を'dn=Bob Smith,ou=MyCompany,dc=example,dc=com'と持つより'bobsmith'と持った方が簡単だからです。

コントローラでこれを利用するのは次のように簡単です。

$this->LdapUser->auth($u, $p);

Active Directoryの利用

psychicが提供してくれた情報で,CakePHPの中にうまくLDAPを取り込めそうだと言うことがわかりました。しかし僕のように Active DirectoryをLDAPサーバーとして利用する場合には,このままではうまく動かない部分があります。

Active Directory(以降AD)サーバにldap関数を使って接続するときには,いくつか注意すべき点があります。

uidではなくsAMAccountNameを使う

認証(authentication)の研究(1)で 紹介したpsychicの記事にあるfindAll関数では$attribute引数の初期値に'uid'という値が使われています。。 Linux+OpenLDAPで運用されているLDAPサーバーの場合は,アカウント名を示す属性はuidなのですが,ADのLDAPディレクトリの場合 にはこの属性はありません。ADで同様な意味を持つ属性は,sAMAccountNameです。cnが使えそうに思うかもしれませんが,これは使えませ ん。ADのサーバー上でウィザードを使ってユーザーを新規作成した場合には,cnには姓名がセットされてしまうからです。相手がADサーバーの場合は sAMAccountNameを使います。

パスワードが空だとldap_bindはtrueを返す

次に,これはちょっとハマったのですが,ldap_bind関数でADに接続するとき,パスワードが空欄の場合はldap_bindは一応成功して しまうという点に注意が必要です。そんな場合はldap_bindの戻り値にtrueが返ってきますが,それ以降のldapに対する処理 ldap_searchなどは失敗します。ldap_bindを使ってユーザー名とパスワードの組み合わせが正しいかどうか認証をする場合に は,ldap_bindで認証する前に空のパスワードの場合をはじいておかなければいけません。

AD認証の手順

ADは匿名bindを許しませんので,認証手順としては次のようになります。

  1. ユーザー名とパスワードをチェックしていずれかが空欄の場合は認証失敗とします。
  2. bind用のユーザー名とパスワードでADにbindします。ldap_bind
  3. ADのsAMAccount属性を検索して与えられたユーザー名があるかどうか調べます。ldap_search
  4. あった場合は,そのアカウントのdnを調べます。ldap_search, ldap_first_entry, ldap_getdn
  5. dnとパスワードでldapにバインドしてみます。ldap_bind

これらの処理が通れば,ADにより認証されたということになります。

基本的な疑問

ここまでの実験をいろいろやっていまして,一つ疑問に思うことがあります。CakePHPではcore.phpで定義されているDEBUG定数の 値,画面へのデバッグ情報の表示を制御していますが,ldap_bindで認証処理を行うと,認証できなかった場合にかならずエラーが表示されてしまうの で,うまく認証できなかった場合の動作確認がちゃんとできないんですよね。DEBUG定数に優先するようなエラー処理を書くことってできないのでしょう か。これって基本的なことで,とても恥ずかしい諮問かもしれません。

トラックバック


URL から "-MoIyadayo" を削除してトラックバックを送信してください。
トラックバックは承認後に表示されます。