EC studio EC studio 技術ブログ

2008年03月31日投稿者:山本 正喜

PHPでお手軽ダイジェスト認証 (Digest認証)

前回のお手軽ベーシック認証に引き続き、
今回はPHPで簡単にダイジェスト認証をかける関数を作成してみました。

ベーシック認証はとてもシンプルな認証方式のため、
パスワードがそのままHTTPのネットワークを流れてしまい、
盗聴などセキュリティ上の危険性があります。

ダイジェスト認証はその点を改善する為に作られた認証方式で、
パスワードをハッシュ化(復元不可能な形式)して送信する為、
例えパケットを盗聴されたとしてもパスワードの特定は
とても困難になります。

関数本体のコード:

  1. /**
  2. * ダイジェスト認証をかける
  3. *
  4. * @param array $auth_list ユーザー情報(複数ユーザー可) array("ユーザ名" => "パスワード") の形式
  5. * @param string $realm レルム文字列
  6. * @param string $failed_text 認証失敗時のエラーメッセージ
  7. */
  8. function digest_auth($auth_list,$realm="Restricted Area",$failed_text="認証に失敗しました"){
  9.     if (!$_SERVER['PHP_AUTH_DIGEST']){
  10.         $headers = getallheaders();
  11.         if ($headers['Authorization']){
  12.             $_SERVER['PHP_AUTH_DIGEST'] = $headers['Authorization'];
  13.         }
  14.     }
  15.    
  16.     if ($_SERVER['PHP_AUTH_DIGEST']){
  17.         // PHP_AUTH_DIGEST 変数を精査する
  18.         // データが失われている場合への対応
  19.         $needed_parts = array(
  20.                     'nonce' => true,
  21.                     'nc' => true,
  22.                     'cnonce' => true,
  23.                     'qop' => true,
  24.                     'username' => true,
  25.                     'uri' => true,
  26.                     'response' => true
  27.                     );
  28.         $data = array();
  29.        
  30.         $matches = array();
  31.         preg_match_all('/(\w+)=("([^"]+)"|([a-zA-Z0-9=.\/\_-]+))/',$_SERVER['PHP_AUTH_DIGEST'],$matches,PREG_SET_ORDER);
  32.        
  33.         foreach ($matches as $m){
  34.             if ($m[3]){
  35.                 $data[$m[1]] = $m[3];
  36.             }else{
  37.                 $data[$m[1]] = $m[4];
  38.             }
  39.             unset($needed_parts[$m[1]]);
  40.         }
  41.        
  42.         if ($needed_parts){
  43.             $data = array();
  44.         }
  45.        
  46.         if ($auth_list[$data['username']]){
  47.             // 有効なレスポンスを生成する
  48.             $A1 = md5($data['username'].':'.$realm.':'.$auth_list[$data['username']]);
  49.             $A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
  50.             $valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2);
  51.            
  52.             if ($data['response'] != $valid_response){
  53.                 unset($_SERVER['PHP_AUTH_DIGEST']);
  54.             }else{
  55.                 return $data['username'];
  56.             }
  57.         }
  58.     }
  59.    
  60.     //認証データが送信されているか
  61.     header('HTTP/1.1 401 Authorization Required');
  62.     header('WWW-Authenticate: Digest realm="'.$realm.'", nonce="'.uniqid(rand(),true).'", algorithm=MD5, qop="auth"');
  63.     header('Content-type: text/html; charset='.mb_internal_encoding());
  64.    
  65.     die($failed_text);
  66. }

ベーシック認証の時と比べると格段に複雑になりますね。

ダイジェスト認証のアルゴリズムを簡単に言うと、

1.クライアントがサーバーにアクセス
2.サーバーはランダムな文字列をクライアントに返す
3.クライアントがサーバーから送られたランダムな文字列と、
   IDとパスワードなどを組み合わせたハッシュ値を送信する
4.サーバーが送られたハッシュ値が一致するか検証する

こうなります。(SSL通信と似てますね)

クライアントとサーバー間のネットワークでは、
ランダムな文字列とハッシュ値、IDだけが通信されるので、
セキュリティ上安全性が高くなります。

この関数の使い方は、ベーシック認証の時と全く同じです。
関数を読み込んでから、

  1. //ダイジェスト認証をかける
  2. digest_auth(array("masaki" => "password"));
  3.  
  4. echo "認証を通過しました!";

とするだけでOKです。
(文字化けする方はmb_internal_encodingの設定を確認してください)

引数には、array("ユーザー名" => "パスワード"); の形式で
認証可能なユーザー情報を渡してください。

↓こんな感じで複数ユーザーも可能です。

  1. digest_auth(array(
  2.         "masaki" => "password1",
  3.         "ono" => "password2",
  4.         "kawamura" => "password3"
  5.         ));
  6. echo "認証を通過しました!";

ユーザー情報はただの連想配列なので、キーを追加するだけでユーザーを追加することができます。

では、実際にちゃんと通信がハッシュ化されているか検証してみましょう。

認証ダイアログの表示
----------------------------------------------------------
GET /digestauth.php HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; ja; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive

HTTP/1.x 401 Authorization Required
Date: Mon, 31 Mar 2008 07:43:09 GMT
Server: Apache/2.2.6 (Win32) DAV/2 mod_ssl/2.2.6 OpenSSL/0.9.8e mod_autoindex_color PHP/5.2.4
X-Powered-By: PHP/5.2.4
WWW-Authenticate: Digest realm="Restricted Area", nonce="1873247f0960d8883a4.80119030", algorithm=MD5, qop="auth"
Content-Length: 27
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8
----------------------------------------------------------

ユーザーID、パスワード入力後 (masaki / password)
----------------------------------------------------------
GET /digestauth.php HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; ja; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Authorization: Digest username="masaki", realm="Restricted Area", nonce="1873247f0960d8883a4.80119030", uri="/digestauth.php", algorithm=MD5, response="0d54f4134b674d5cec6c6027dfd0d093", qop=auth, nc=00000001, cnonce="5eb87c5316a97971"

HTTP/1.x 200 OK
Date: Mon, 31 Mar 2008 07:43:13 GMT
Server: Apache/2.2.6 (Win32) DAV/2 mod_ssl/2.2.6 OpenSSL/0.9.8e mod_autoindex_color PHP/5.2.4
X-Powered-By: PHP/5.2.4
Content-Length: 2
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: text/html
----------------------------------------------------------

大丈夫そうですね!

HTTP通信上にパスワードの文字列がやりとりされていないことが
確認できると思います。

ご活用ください ;)


関連した記事:
投稿者
人気のエントリー
カテゴリー
最近のエントリー
アーカイブ
Copyright© ChatWork, All Rights Reserved. secured by ESET.