HTTP Authentication

この頁では、HTTPにおけるアクセス認証について、その方式および関連するパラメータなどについて解説します。

HTTPアクセス認証とは

HTTPは、FTPなどとは異なり、基本的に、アクセス時にサーバ側との認証手続きを行う必要はありません。 これは、「通信手続きの簡略化によってファイル配布を容易にする」というHTTPの元々の設計コンセプトによるものであり、それ故にこれだけHTTPが広く利用されるようになったのですが、そうして一般的に利用されるようになると、今度は特定のファイルアクセスに対して制限を行いたいという希望が出てくるようになりました。

そこで、HTTPの開発者は、新たにアクセス認証について規定するために、HTTP認証: 基本アクセス認証及びダイジェストアクセス認証という仕様書を発行しました。 その中のsection 1.2には、HTTPアクセス認証の枠組みが記述されています。

HTTP は、サーバがクライアントにリクエストを誰何{challenge} するため、あるいはクライアントが認証情報を提供するために使用する事ができる単純な誰何-応答{challenge-response} 認証メカニズムを提供する。 これは、認証スキームを識別するための拡張可能である大文字・小文字を区別しないトークンと、スキームを通じて認証を行うために必要なパラメータを含むコンマで区切られた属性-値という対のリストを使用する。

 auth-scheme    = token
 auth-param     = token "=" ( token | quoted-string )

401 (Unauthorized) レスポンスメッセージは、オリジンサーバがユーザエージェントの認証を誰何するために使用される。 このレスポンスは、リクエストされるリソースに効力がある最低一つの challenge を含む WWW-Authenticate ヘッダフィールドを含まなければならない。 407 (ProxyAuthentication Required) レスポンスメッセージは、プロクシがユーザエージェントの認証を誰何するために使用され、リクエストされるプロクシに効力がある最低一つの challenge を含む Proxy-Authenticate ヘッダフィールドを含まなければならない

 challenge   = auth-scheme 1*SP 1#auth-param

注意: ユーザエージェントは、複数の challenge や WWW-Authenticate ヘッダフィールドが供給される場合、challenge の内容はそれ自身にコンマで区切られた認証パラメータのリストが含んでいるかもしれないので、WWW-Authenticate や Proxy-Authenticate 各ヘッダフィールドを解析するのには特別な注意を払う必要があるであろう。

「誰何(すいか){challenge}」という単語は耳慣れないと思いますが、国語辞書によると「相手が何者かわからないときに、呼びとめて問いただすこと」とあります。 要するに、サーバが(サーバから見て)得体の知れないクライアントからの要求を受けた時に、その要求に応じてよい相手かどうかを判断するためのやり取りを、ここでは「誰何」と呼び、それを実現するためのHTTPにおける仕組みを「HTTPアクセス認証」と呼んでいるということになります。

HTTPアクセスの流れを図で表すと、以下のようになります。

図 アクセス認証が必要な領域へリクエストした場合のシーケンス

図に示す通り、クライアントからのリクエストに対し、サーバは一旦401レスポンスを返します(プロクシに対する認証の場合は、407レスポンスを返します)。 その結果を受け、クライアントはサーバからの「誰何」に答えるためのキーワードを含めて再度リクエストを行い、その結果が妥当であれば初めてサーバからリソースを得られるという流れになっています。

HTTPアクセス認証の方式

HTTPアクセス認証では、誰何を行うやり方を「(認証)スキーム」と言い、基本認証、およびダイジェスト認証という2つの認証スキームが規定されています。

(※) 現在、上記以外のアクセス認証方式として、“HTTP Mutual認証”という方式が産業技術総合研究所を中心に研究され、IETFに提案されています。

基本認証

基本認証は、HTTP/1.0から存在する認証方法です。 RFC 2617のsection 2をご覧下さい。

"基本{Basic}" 認証スキームは、クライアントは各々の realm についてユーザ ID とパスワードをもって自身を認証しなければならないというモデルに基づいている。 realm 値は、そのサーバ上の他の realm との同等性の比較のみのために存在しうる非空白文字列であるとみなされるべきである。 サーバは、その保護された Request-URI の空間のためのユーザ ID とパスワードが正しいと証明できる場合にのみ、リクエストを処理するであろう。 オプショナルな認証パラメータは存在しない。

(中略)

保護空間内の URI について認証されていないリクエストを受信した上で、オリジンサーバは以下のような challenge を返す事ができる

 WWW-Authenticate: Basic realm="WallyWorld"

ここで "WallyWorld" は、Request-URI の保護空間を識別するためにサーバによって割り当てられた文字列である。 プロクシは、Proxy-Authenticate ヘッダフィールドを使って同じ challenge を返す事ができる。

認証を受信するために、クライアントは証明書中にて base64 エンコードされた、単一のコロン (":") 文字にて区切られた userid と password の文字列を送信する。

 basic-credentials = base64-user-pass
 base64-user-pass  = <76 文字/行とは制限されていない、
                  user-pass の base64 エンコーディング>
 user-pass   = userid ":" password
 userid      = *<":" を含まない TEXT>
 password    = *TEXT

userid は大文字・小文字を区別するであろう。

ユーザエージェントが userid "Aladdin" と password "open sesame" を送信する場合、以下のヘッダフィールドを使用するであろう:

 Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

クライアントは、その Request-URI の path 領域中の最後の記号要素の深さと同等、あるいはそれより深い所 (訳注: 要するに現在のディレクトリ以下) は、現在の challenge の Basic realm 値によって指定される保護空間内にあるという事を仮定すべきである。 クライアントは、その空間にあるリソースへのリクエストに対応する Authorization ヘッダを、サーバから別の challenge を受信する事無く、先んじて送信してもよい。 同様に、クライアントがプロクシにリクエストを送る場合、プロクシサーバから別の challenge を受信する事無く Proxy-Authorization ヘッダフィールド中の userid と password を再使用してもよい。 基本認証に関連したセキュリティについての考察は section 4 を参照。

アクセスが制限された領域へアクセスされると、まずHTTPサーバはアクセス制限されているということをクライアントに伝えるために401(Authorization Required)レスポンスを返します。 ここで、クライアントは既にサーバ側から割り振られているユーザID(userid)とパスワード(password)をBase64エンコーディングして、Authorizationヘッダに含めて、リクエストを再発行します。

ところが、基本認証はそのセキュリティに問題があります。 RFC 2617のsection 4.1をご覧下さい。

基本認証スキームは、転送媒体として物理層ネットワークを通じて平文を転送するので、セキュアなユーザ認証方法でも、エンティティを保護する方法でもない。 HTTP は、基本認証にセキュリティや (ワンタイムパスワードを使うスキームのような) その価値を増すために使用される追加的認証スキームや暗号化メカニズムを禁止していない。

基本認証の最も重大な欠点は、物理層ネットワークを実質的に平文であるユーザのパスワードを転送してしまっている事である。 これはダイジェスト認証が取り組んでいる問題である。

基本認証は必然的に平文であるパスワードの転送を含んでいるので、個人情報や価値のある情報を保護するためには (追加的セキュリティ無しで) 使用されるべきではない

下図をご覧下さい。 これは、Wiresharkというパケット解析用ソフトによって、基本認証を含む通信を収集し、それを解析したものです。

図 パケットキャプチャされた基本認証

図中のAuthorizationヘッダのご覧ください。 一目瞭然、ユーザIDとパスワードが誰が見てもわかってしまいます。 そもそも、Base64エンコーディングとは「アルファベット、数字、及び一部の記号を用いて、任意の文字を表現するための符号化方法」であり、暗号化するものではありません。 そのため、もしネットワーク上でAuthorizationヘッダが盗み見られてしまったら、簡単にユーザ名やパスワードが盗まれてしまうことになってしまいます。 したがって、たとえばSSLのような、セキュリティを向上させる技術とともに使用されるのでなければ、基本認証はユーザ認証手段としては安全なものではないということを強く理解しておく必要があります。

(※) ここで最も危険なことは、HTTP上で通信が漏れるということ以上に、多くの人は往々にして「パスワードを使いまわす」ということです。 すなわち、一旦パスワードが悪意を持つ人に知られてしまうと、この通信以外のあらゆるコンピュータ上の活動で損害をこうむる危険性があるということを認識する必要があります。

ダイジェスト認証

ダイジェスト認証は、基本認証に置き換わるものとして、HTTP/1.1で新たに提案された認証方法です。 RFC 2617のsection 3.1.2, 3.1.4をご覧下さい。

基本アクセス認証のように、ダイジェストスキームは単純な誰何-応答パラダイムに基づくものである。 ダイジェストスキームは、nonce 値を使用して誰何する。 妥当{valid} なレスポンスは、ユーザ名、パスワード、与えられる nonce 値、HTTP メソッド、リクエストされる URI のチェックサム (既定では、MD5 チェックサム) を含んでいる。 この方法では、パスワードが明文中に送信される事は無い。 ちょうど基本スキームを用いる時のように、ユーザ名とパスワードはこの文書によって述べられてない幾つかのやり方によって前持って取り決められているはずである。

この文書に記述されるダイジェスト認証スキームは、多くの既知の限界に苦しめられる。 これは基本認証の置換を意図したものであり、またそれ以外の何物でもない。 これはパスワードに基づいたシステムであり、(サーバ側での) 全てのあらゆるパスワードシステムと同じ問題に苦しめられる。 特に、ユーザとサーバとの間でユーザのパスワードを確立するための初期のセキュアなる取り決めに対する対策は、このプロトコルの中で示されない。

ユーザと実装者は、このプロトコルが Kerberos 程にも、また client-side 秘密鍵スキーム程にもセキュアではないという事に気づいているべきである。 それであっても、このプロトコルは、何もしないよりも、また一般に telnet や ftp と共に使用されるものよりも、また基本認証よりは良い。

ダイジェスト認証が基本認証と一番異なる点は、「認証用のHTTPヘッダ中にパスワードそのものは含まない」という点ですRFC 2617のsection 3.2.1をご覧下さい。

サーバは、アクセス制限されたオブジェクトへのリクエストを受信したが、受け入れ可能な Authorization ヘッダが送られない場合、"401 Unauthorized" ステータスコードと、上に定義される枠組毎に WWW-Authenticate ヘッダを返すが、この時ダイジェストスキームは以下の様に利用される:

 challenge        =  "Digest" digest-challenge

 digest-challenge  = 1#( realm | [ domain ] | nonce |
                     [ opaque ] |[ stale ] | [ algorithm ] |
                     [ qop-options ] | [auth-param] )

 domain            = "domain" "=" <"> URI ( 1*SP URI ) <">
 URI               = absoluteURI | abs_path
 nonce             = "nonce" "=" nonce-value
 nonce-value       = quoted-string
 opaque            = "opaque" "=" quoted-string
 stale             = "stale" "=" ( "true" | "false" )
 algorithm         = "algorithm" "=" ( "MD5" | "MD5-sess" | token )
 qop-options       = "qop" "=" <"> 1#qop-value <">
 qop-value         = "auth" | "auth-int" | token

上記に使用される指示子の値の意味は以下の通りである:

realm

ユーザが使用するユーザ名とパスワードを知るためにユーザに表示されるための文字列。 この文字列には少なくとも認証を実行するホストの名前を含むべきであり、またアクセスを許可するユーザを追加的に列挙する事ができる。 それは例えば、"registered_users@gotham.news.com" のようになるであろう。

nonce

401 レスポンスが返される毎に一意に生成されるべきサーバによって指定されたデータ文字列。 この文字列は base64 あるいは 16 進データである事が推奨される。 特に、この文字列はクォート文字列としてヘッダライン中にて渡されるので、ダブルクォート文字の使用は許されない。

nonce の内容は実装依存である。 その実装の品質は良い選択に依存する。 例えば、nonce は以下の base 64 エンコーディングから成るものとする事ができる

 time-stamp H(time-stamp ":" ETag ":" private-key)

ここで time-stamp はサーバによって生成された時刻あるいはその他の繰り返さない値、ETag はリクエストエンティティに関連した HTTP ETag ヘッダの値、private-key はサーバのみが知るデータである。 この形式のnonce において、サーバはクライアント認証ヘッダを受信後にハッシュ部分を再計算し、もしそれがそのヘッダからの nonce と一致しないか、あるいはその time-stamp 値が十分に最近のものでない場合、そのリクエストを拒否する。 このようにして、サーバは nonce の有効な時間を制限する事ができる。 ETag を含む事によって、リソースの更新バージョンを繰り返しリクエストする事を防ぐ。 (注: nonce にクライアントの IP アドレスを含む事で、元々それを持つ同じクライアントへ nonce の再使用を制限する能力をサーバに提供するように見えるであろう。 しかし、それはプロクシを複数持ち、単一ユーザからのリクエストはしばしばそのプロクシ中の異なるプロクシを通り抜けてくるような環境においては破綻する。 更に、それが IP アドレススプーフィングを困難にするわけでもない。)

実装は、繰り返し攻撃から防御するために、以前使用した nonce やダイジェストを受け入れないとする事ができる。 あるいは、実装は、POST や PUT リクエストのために一回きりのnonce やダイジェストを使う、また GET リクエストのために一回きりのtime-stamp を使うとする事ができる。 そこに含まれる問題についての詳細についてはこの文書の section 4 を見よ。

nonce は、クライアントにとってそれ自体は読んでも意味のわからない{opaque} ものである。

ここで、特に注目すべきパラメータはrealmnonceで、クライアントはこの値をサーバに送り返すことで認証を行います。 RFC 2617のsection 3.2.2をご覧下さい。

クライアントは Authorization ヘッダラインを転送する時に、リクエストを再試行するはずである。 それは上記の枠組によって定義され、以下のように利用される。

 credentials      = "Digest" digest-response
 digest-response  = 1#( username | realm | nonce | digest-uri
                 | response | [ algorithm ] | [cnonce] |
                 [opaque] | [message-qop] |
                     [nonce-count]  | [auth-param] )

 username         = "username" "=" username-value
 username-value   = quoted-string
 digest-uri       = "uri" "=" digest-uri-value
 digest-uri-value = request-uri   ; HTTP/1.1 にて記述
 message-qop      = "qop" "=" qop-value
 cnonce           = "cnonce" "=" cnonce-value
 cnonce-value     = nonce-value
 nonce-count      = "nc" "=" nc-value
 nc-value         = 8LHEX
 response         = "response" "=" request-digest
 request-digest = <"> 32LHEX <">
 LHEX             =  "0" | "1" | "2" | "3" |
                     "4" | "5" | "6" | "7" |
                     "8" | "9" | "a" | "b" |
                     "c" | "d" | "e" | "f"

opaque, algorithm 各フィールド値はリクエストされているエンティティにおける WWW-Authenticate レスポンスヘッダにて提供されるものでなければならない。

response

以下に定義される方法で計算される 32 字の 16 進文字列。 これによってそのユーザがパスワードを知っている事を証明する。

username

指定された realm 中のユーザ名。

digest-uri

Request-Line の Request-URI にある URI; プロクシは転送において Request-Line を変更できるので、ここにコピーしておくのである。

ダイジェスト認証で通信するデータ上では、パスワードそのものは現れません。 代わりに、一方向関数であるMD5を用いて暗号化された文字列であるnonceを利用しており、仮にそれが盗み見られたとしても、そこからユーザIDやパスワードを復号化させることは、基本認証と比較して大変難しいとされています。(※) 下図は、Wiresharkを利用してダイジェスト認証を解析したものですが、ご覧の通りヘッダを見てもパスワードがわかりません。

図 パケットキャプチャされたダイジェスト認証

(※) 「nonceからパスワードを割り出す」ことは難しいですが、「nonceそのものを使いまわしてダイジェスト認証自身を乗っ取る」ということは可能です。 そのため、認証後に渡されるデータに対する安全性が保証されていると言い切ることは難しいのが現状です。 したがって、ダイジェスト認証は、あくまでも「基本認証の代替」として使用されるべきものであり、安全なユーザ認証手段を提供したいのであれば、やはりSSLのような通信路そのもののセキュリティを向上させる技術とともに併用されるべきです。

HTTPアクセス認証のためのHTTPヘッダ

HTTPアクセス認証は、以下のHTTPヘッダをやり取りすることで実現しています。

Authorization

アクセス認証が必要な領域内にリクエストを行う際には、Authorizationヘッダに必要な情報を含め、リクエストを行う必要があります。

サーバに認証を受けようとするユーザエージェントは、必ずというわけではないが、通常は 401 レスポンスの後、リクエストに Authorization リクエストヘッダフィールドを含める。 Authorization フィールド値は、リクエストされたリソースのある領域へのユーザエージェントの認証情報を含む credentials からなる。

 Authorization  = "Authorization" ":" credentials

ユーザエージェントは、401レスポンスを受け取った場合、そのレスポンスに含まれるWWW-Authenticateを元に、保護された領域{realm}に入るための認証を受けるためのパスワードをこのヘッダに含めます。 この時、ヘッダ内にどのような値を含めるかは、認証方式によって異なります。 詳しくは、HTTPアクセス認証の方式を参照ください。

WWW-Authenticate

アクセス認証が必要な領域内にリクエストを行った時に、「Authorizationヘッダがない」あるいは「Authorizationヘッダの内容では認証できない」場合、401レスポンスとともに、WWW-Authenticateヘッダが返されます。

WWW-Authenticate レスポンスヘッダフィールドは、401 (Unauthorized) レスポンスメッセージ中に含まれていなければならない。 このフィールド値は、その Request-URI に適用できる認証スキームとパラメータを示す最低一つの challenge から成る。

 WWW-Authenticate  = "WWW-Authenticate" ":" 1#challenge

誰何{challenge}については、HTTPアクセス認証とはを参照ください。

アクセス認証が必要な領域にリクエストを行った場合、多くのユーザエージェントは401レスポンスを表示せずに、認証パラメータを入力する画面を表示します。 そして、そこに入力された情報を元に、Authorizationヘッダを作成し、再びリクエストを実行します。

参照文献

Webリソース