第 34章PHP による HTTP 認証

PHP による HTTP 認証のフックは、 Apache モジュールとして実行した時のみ 有効で、CGI 版では利用できません。Apache モジュール上の PHP スクリプトに おいては、 header() 関数を使用して "Authentication Required" メッセージをクライアントブラウザに 送ることが可能です。 これにより、クライアントブラウザではユーザー名とパスワードの入力要求 ウインドウがポップアップ表示されます。一度、ユーザーがユーザー名と パスワードを入力すると、PHP スクリプトを含むその URL は、次回以降、 定義済みの変数 PHP_AUTH_USER と、 PHP_AUTH_PW と、 PHP_AUTH_TYPE にそれぞれユーザー名、 パスワード、認証型が代入された状態で呼ばれます。 定義済みの変数は、配列 $_SERVER および $HTTP_SERVER_VARS  でアクセス可能です。 "Basic" 認証および "Digest" 認証 (PHP 5.1.0 以降) の両者がサポートされています。詳細は、 header() を参照ください。

PHP バージョンに関する注意: $_SERVER のような オートグローバル は、 PHP 4.1.0 以降で利用可能となりました。 $HTTP_SERVER_VARS は、PHP 3以降で利用可能です。

ページ上でクライアント認証を強制するスクリプトの例を以下に示します。

例 34-1. Basic HTTP 認証の例

<?php
if (!isset( $_SERVER [ 'PHP_AUTH_USER' ])) {
    
header ( "WWW-Authenticate: Basic realm=\"My Realm\"" );
    
header ( "HTTP/1.0 401 Unauthorized" );
    echo
"ユーザーがキャンセルボタンを押した時に送信されるテキスト\n" ;
    exit;
} else {
    echo
"<p>こんにちは、 { $_SERVER [ 'PHP_AUTH_USER' ]} さん。</p>" ;
    echo
"<p>あなたは、 { $_SERVER [ 'PHP_AUTH_PW' ]} をパスワードとして入力しました。</p>" ;
}
?>

例 34-2. Digest HTTP 認証の例

この例は、シンプルな Digest HTTP 認証スクリプトをどの様に実装するか を示しています。 その他情報については、 RFC 2617 を読んでください。

<?php
$realm
= 'Restricted area' ;

//user => password
$users = array( 'admin' => 'mypass' , 'guest' => 'guest' );


if (empty(
$_SERVER [ 'PHP_AUTH_DIGEST' ])) {
    
header ( 'HTTP/1.1 401 Unauthorized' );
    
header ( 'WWW-Authenticate: Digest realm="' . $realm .
           
'",qop="auth",nonce="' . uniqid (). '",opaque="' . md5 ( $realm ). '"' );

    die(
'ユーザーがキャンセルボタンを押した時に送信されるテキスト' );
}


// PHP_AUTH_DIGEST 変数を精査する
if (!( $data = http_digest_parse ( $_SERVER [ 'PHP_AUTH_DIGEST' ])) ||
    !isset(
$users [ $data [ 'username' ]]))
    die(
'誤った証明書です!' );


// 有効なレスポンスを生成する
$A1 = md5 ( $data [ 'username' ] . ':' . $realm . ':' . $users [ $data [ 'username' ]]);
$A2 = md5 ( $_SERVER [ 'REQUEST_METHOD' ]. ':' . $data [ 'uri' ]);
$valid_response = md5 ( $A1 . ':' . $data [ 'nonce' ]. ':' . $data [ 'nc' ]. ':' . $data [ 'cnonce' ]. ':' . $data [ 'qop' ]. ':' . $A2 );

if (
$data [ 'response' ] != $valid_response )
    die(
'誤った証明書です!' );

// OK, 有効なユーザー名とパスワードだ
echo 'あなたは次のユーザーとしてログインしています: ' . $data [ 'username' ];

// http auth ヘッダをパースする関数
function http_digest_parse ( $txt )
{
    
// データが失われている場合への対応
    
$needed_parts = array( 'nonce' => 1 , 'nc' => 1 , 'cnonce' => 1 , 'qop' => 1 , 'username' => 1 , 'uri' => 1 , 'response' => 1 );
    
$data = array();

    
preg_match_all ( '@(\w+)=(?:([\'"])([^\2]+)\2|([^\s,]+))@' , $txt , $matches , PREG_SET_ORDER );

    foreach (
$matches as $m ) {
        
$data [ $m [ 1 ]] = $m [ 3 ] ? $m [ 3 ] : $m [ 4 ];
        unset(
$needed_parts [ $m [ 1 ]]);
    }

    return
$needed_parts ? false : $data ;
}
?>

互換性に関する注意: HTTPヘッダ行をコーディングする際には注意を要します。全てのクライアントへの 互換性を最大限に保証するために、キーワード "Basic" には、 大文字の"B"を使用して書くべきです。realm文字列は(一重引用符ではなく) 二重引用符で括る必要があります。また、 HTTP/1.0 401 ヘッダ行のコード 401 の前には、 1つだけ空白を置く必要があります。 認証パラメータは、上のダイジェスト認証の例にあるように カンマ区切りで指定しなければなりません。

単に PHP_AUTH_USER および PHP_AUTH_PW を出力するのではなく、ユーザー名とパスワードの有効性をチェックしたいと 思うかもしれません。 その場合、クエリーをデータベースに送るか、ある dbm ファイル中の ユーザーを調べるといったことをすることになるでしょう。

バグのある Internet Explorer ブラウザには注意してください。このブラ ウザは、ヘッダの順序に関してとてもうるさいようです。今のところ、 HTTP/1.0 401 ヘッダの前に WWW-Authenticate ヘッダを送るのが効果があるようです。

PHP 4.3.0 以降、誰かが従来の外部機構による認証を行ってきたページの パスワードを暴くようなスクリプトを書くことを防ぐために、 特定のページに関して外部認証が可能でかつ セーフモード が有効の場合、 PHP_AUTH 変数はセットされません。 この場合、外部認証されたユーザーかどうかを確認するために REMOTE_USER 変数、すなわち、 $_SERVER['REMOTE_USER'] を使用することができます。

設定上の注意: PHP は、外部認証が動作しているかどうかの判定を AuthType ディレクティブの有無で行います。

しかし、上記の機能も、認証を要求されないURLを管理する人が同じサーバー にある認証を要するURLからパスワードを盗むことを防ぐわけではありませ ん。

サーバーからレスポンスコード 401 を受けた際に、Netscape Navigatorおよび Internet Explorer は共にローカルブラウザのウインドウ上の認証キャッシュを 消去します。この機能により、簡単にユーザーを"ログアウト"させ、強制的に ユーザー名とパスワードを再入力させることができます。この機能は、 "タイムアウト" 付きのログインや、"ログアウト" ボタンに適用されています。

例 34-3. 新規に名前 / パスワードを入力させる HTTP 認証の例

<?php
function authenticate () {
    
header ( 'WWW-Authenticate: Basic realm="Test Authentication System"' );
    
header ( 'HTTP/1.0 401 Unauthorized' );
    echo
"このリソースにアクセスする際には有効なログインIDとパスワードを入力する必要があります。\n" ;
    exit;
}

if (!isset(
$_SERVER [ 'PHP_AUTH_USER' ]) ||
    (
$_POST [ 'SeenBefore' ] == 1 && $_POST [ 'OldAuth' ] == $_SERVER [ 'PHP_AUTH_USER' ])) {
    
authenticate ();
} else {
    echo
"<p>Welcome: { $_SERVER [ 'PHP_AUTH_USER' ]} <br>" ;
    echo
"Old: { $_REQUEST [ 'OldAuth' ]} " ;
    echo
"<form action=' { $_SERVER [ 'PHP_SELF' ]} ' METHOD='POST'> \n " ;
    echo
"<input type='hidden' name='SeenBefore' value='1'>\n" ;
    echo
"<input type='hidden' name='OldAuth' value=' { $_SERVER [ 'PHP_AUTH_USER' ]} '> \n " ;
    echo
"<input type='submit' value='Re Authenticate'>\n" ;
    echo
"</form></p>\n" ;
}
?>

この動作は、HTTP Basic 認証の標準に基づいていません。よって、この機能に 依存しないように注意する必要があります。Lynx によるテストの結果、 Lynx は、認証証明書を 401 サーバー応答によりクリアしないことが明らかに なっています。このため、back を押してから foward を再度押すことにより 証明書の要件が変更されない限りリソースをオープンすることができます。 しかし、ユーザは '_' キーを押すことにより認証情報をクリアすることが可能です。

PHP4.3.3 までは、Microsoft の IIS サーバーと CGI 版の PHP の組み合わせでは、 この機能は、IIS の制約により使用することができなかったことにも 注意してください。PHP 4.3.3 以降においてこの機能を使用するには、 IIS の設定の "ディレクトリセキュリティ" の "編集" ボタンを押して "匿名アクセス" のみをオンにしてください。その他のフィールドは オフのままにしてください。

他の制限としては、IIS モジュール (ISAPI) と PHP 4 を使用している場合に、 PHP_AUTH_* 変数が使用できないことがあります。 しかし、代わりに HTTP_AUTHORIZATION を使うことができます。 例えば、次のようなコードを考慮してください。 list($user, $pw) = explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));

IIS に関する注意:: IIS上 で HTTP 認証を使用する場合、PHP の cgi.rfc2616_headers ディレクティブは 0 (デフォルト値) にセットされて いなければなりません。

注意: セーフモード が有効の場合、 WWW-Authenticate ヘッダの realm 部にスクリプトの uid が追加されます。