HTTPの認証フレームワークについて整理してみました。
HTTP認証フレームワークとは?
HTTP認証とは、クライアントがサーバーに保護されたリソース(access protected resources)を要求する際に、自分が誰であるかを証明する
ための手続きです。
HTTP認証の流れは次の通りです
- クライアントが認証情報なしでリクエストを送信
GET /protected/resource HTTP/1.1
Host: example.com
2.サーバーが401 Unauthorizedレスポンスと共に、WWW-Authenticateヘッダーで認証スキームを提示
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="User Visible Realm"
3.クライアントが適切なAuthorizationヘッダーを付けて再リクエスト
GET /protected/resource HTTP/1.1
Host: example.com
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
4.サーバーが認証情報を検証し、リソースを返すか拒否する
- 認証成功時
HTTP/1.1 200 OK
(要求されたリソースを返す)
- 認証失敗時
HTTP/1.1 401 Unauthorized
(認証に失敗した場合)
- 認証には成功したがアクセス権がない場合
HTTP/1.1 403 Forbidden
(アクセス拒否)
認証スキームとは?
クライアントがサーバーに認証情報を送信する際、どのような方式で送るかを定めたプロトコル上のルールです。
前述のように、HTTPリクエストで認証を行う際には、次のような形式でヘッダーが構成されます。
Authorization: <スキーム> <認証情報>
-
Authorization
→ 認証情報を含むHTTPヘッダー -
スキーム
→ 認証スキームの名称(使用される方式名) -
認証情報
→ エンコードされた認証情報(ID:パスワード)
このように記述されます。
ここからは代表的な3つのスキーム、
Basic、Digest、Bearer について解説していきます。
Basic認証
Authorization: Basic <base64でエンコードされたID:パスワード>
認証の流れはどうなっているか?
- ユーザーからユーザーIDとパスワードを取得します
id: haroya
pw: haroya123!@#
2.ユーザーID、コロン(:)1つ、そしてパスワードを連結して user-pass を構成します
<ユーザー名>:<パスワード>
例:
<haroya>:<haroya123!@#>
3.user-pass をオクテット列にエンコードします
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
- 必ず
UTF-8
を使用すること
なぜ UTF-8
の使用が推奨されるのか(仕様上は必須ではない)
以前の仕様では、user-pass をオクテット列に変換する際に使用する文字エンコーディング方式が明確に定義されていませんでした。
HTTP Basic認証は1990年代初頭に導入されたもので、当時は主に英語のみが使用されていたため、多言語対応の必要がなかったのです。
しかし、現在では多様な言語(ハングル、日本語など)や特殊文字(例:€)の使用が一般的になっており、
すべてのブラウザやサーバーが同じルールで処理する必要が出てきました。
この認証方式の元の定義では、user-pass をオクテット列に変換する際の文字エンコーディング方式が指定されていませんでした。
実際には、多くの実装が ISO-8859-1 や UTF-8 を選択していました。
下位互換性の理由から、この仕様ではデフォルトのエンコーディングは依然として未定義ですが、
US-ASCII と互換性のあるエンコーディングでなければならない という条件が設けられています。
4.UTF-8バイトシーケンスを Base64 でエンコードして、US-ASCII 文字列を生成します
例:
aGFyb3lhOmhhcm95YTEyMyFAIw==
5.この値を Authorization ヘッダーに含めてサーバーに送信します
GET /api/resource HTTP/1.1
Host: api.example.com
Authorization: Basic aGFyb3lhOmhhcm95YTEyMyFAIw==
認証スコープ(Authentication Scope)と認証情報の再利用
認証情報の再利用(Reusing Credentials)
一度認証されたリクエストのパスにおいて、「最後のスラッシュ(/)以前」までが認証スコープ(authentication scope)となります。
そのパスを 接頭辞(prefix-match) として持つURIには、同じ認証情報を再利用することができます。
認証スコープはURLパスの「ディレクトリ単位」で定義され、
プロトコル(http と https)が異なる場合は別スコープと見なされます。
http://example.com/docs/index.html → 認証済み
以下のようなパスにも、認証情報は再利用可能です:
http://example.com/docs/
http://example.com/docs/test.doc
http://example.com/docs/?page=1
同じパスおよびプロトコルであれば、認証は許可されるべきです。
しかし、以下のような場合は認証情報の再利用はできません:
http://example.com/other/ (パスが異なる)
https://example.com/docs/ (プロトコルが異なる)
セキュリティ上の考慮事項(Security Considerations)
-
平文での送信に伴うリスク
パスワードが平文(plaintext)で送信され、ネットワーク上で暗号化されずにそのまま露出されてしまいます。
そのため、HTTPSなしでBasic認証を使用するのは非常に危険です。 -
パスワードの再利用によるリスク
ユーザーが複数のサイトで同じパスワードを使用している場合、
1つでも漏洩すると他のサイトもすべて突破されるリスクがあります。
特に、ユーザーが自分でパスワードを設定できる場合、このリスクはさらに高まります。 -
偽装サーバー(スプーフィング)攻撃に対する脆弱性
攻撃者が偽のサーバーを立てて、ユーザーのパスワードを盗み取る可能性があります。 -
パスワードの保存方法に関するリスク
パスワードを平文やソルトなしのハッシュで保存すると、
情報が流出した際に全ユーザーが危険に晒されます。
Password Hashing Competition などで推奨される強力なハッシュアルゴリズムの使用が望まれます。
-
UTF-8と正規化(NFC)に関連するセキュリティ問題
正規化された文字(例:悪意のあるUnicode)により、文字列比較、検証、衝突の問題が発生する可能性があります。
Digest認証
GET /protected/resource HTTP/1.1
Host: example.com
Authorization: Digest username="haroya",
realm="MyProtectedArea@example.com",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
uri="/protected/resource",
qop=auth,
algorithm=SHA-256,
nc=00000001,
cnonce="0a4f113b",
response="calculated_response_hash_value_here",
opaque="5ccc069c403ebaf9f0171e9517f40e41"
Digest認証は、サーバーが送信した乱数(nonce)とクライアントの秘密情報(ID・パスワード)をハッシュして、チャレンジ-レスポンスを交換する方式です。
パスワードを平文で絶対に送信しないという点が、Basic認証との最大の違いです。
どのように認証を行うのか
Digest認証のチャレンジ
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Digest
realm="example.com",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
algorithm=SHA-256,
qop="auth,auth-int",
opaque="5ccc069c403ebaf9f0171e9517f40e41"
- realm:認証領域(ログイン画面に表示される文字列)
- nonce:サーバーが各応答ごとに生成する一意な乱数 → リプレイ攻撃の防止
- algorithm:ハッシュアルゴリズム・セッション変形(例:SHA-256 / SHA-256-sess)
- qop:保護の品質(quality of protection) → auth(認証)、auth-int(認証+完全性)
-
opaque:サーバーが任意で指定し、クライアントがそのまま返す値(追加の真正性確認用)
(option)domain、stale、charset、userhash:保護パス、nonceの期限切れ通知など、必要に応じて含まれる
- ユーザーからユーザーIDとパスワードを取得します
id: haroya
pw: haroya123!@#
2.該当のIDとパスワード、およびチャレンジ情報を用いてハッシュ値を計算します。
ここでの H() 関数は、algorithm
フィールドに指定されたハッシュ関数を使用します(この例では SHA-256)。
- HA1(最初のハッシュ):
H(username:realm:password)
SHA-256("haroya:example:haroya123!@#")
- HA2(2番目のハッシュ):
H(HTTP-method:digest-URI)(qop="auth"の場合)または
H(HTTP-method:digest-URI:H(entity-body))(qop="auth-int"の場合)
保護されたURIに対するリクエストがGETメソッドであると仮定すると、digest-URIはクライアントが実際にリクエストするリソースのパスになります。
SHA-256("GET:/protected/resource")
3.Response(クライアントの応答)
H(HA1:nonce:NC:CNonce:qop:HA2) (qopが指定されている場合)
H(HA1:nonce:HA2) (qopが指定されていない場合)
-
NC(Nonce Count):同じ nonce を使用したリクエストの回数を16進数で表した値です。初期値は
00000001
であり、同じ nonce を再利用するたびに1ずつ増やす必要があります。これはリプレイ攻撃の防止に役立ちます。 - CNonce(Client Nonce):クライアントが生成するランダムな文字列で、レスポンスの一意性を高めます
nc=00000001:1回目の送信時
nc=00000002:2回目の送信時(+1)
各リクエストごとに CNonce は変更されます。
cnonce="d41d8cd98f00b204e9800998ecf8427e":1回目のリクエスト
cnonce="e4d909c290d0fb1ca068ffaddf22cbd0":2回目のリクエスト
4.クライアントは、計算されたハッシュ値を含む Authorization ヘッダーをサーバーに再送信します
Authorization: Digest
username="haroya",
realm="example.com",
nonce="[サーバーが送信した nonce]",
uri="/protected/resource",
algorithm=SHA-256,
response="[計算された応答ハッシュ値(ステップ3を参照)]",
qop="auth",
nc=00000001,
cnonce="[クライアントが生成した cnonce 値]",
opaque="[サーバーが送信した opaque 値]"
セキュリティ考慮事項(Security Considerations)
-
辞書攻撃・ブルートフォース攻撃
ユーザーが辞書にある単語や弱いパスワードを選ばないようにし、可能であれば128ビット以上のエントロピーを持つパスワードの使用を促す必要がある。 -
平文の露出
Digest認証自体は本文やヘッダーの機密性を保証しない → 機密データには TLS の使用が必須。 -
複数スキームの混在
複数の認証スキームを同時に提供すると、最も弱いスキームのレベルまでセキュリティが低下する。 -
MITM・プロキシ改ざん
クライアントは最も強力なスキームのみを使用し、HTTPの代わりに TLS(HTTPS)チャネルの使用が推奨される。 -
リプレイ(再送)攻撃
nonce に IP・タイムスタンプ・ETag などを含めるか、ワンタイム nonce を発行する。
Bearer認証
Authorization: Bearer <アクセストークン>
Bearer認証は、OAuth 2.0で使用される認証方式で、リクエスト時に所持者(bearer)トークン
を提示することで認証を行う方式です。
トークンを持っている者であれば誰でもリソースにアクセスできるため、bearer(所持者)
という名前が付けられています。
トークンさえあれば誰でも認証されてしまう方式のため、必ず HTTPS を使用する必要があります。
どのように認証を行うか?
3つの方法があります。
Authorizationヘッダー(推奨方式)
- 最も安全で標準的かつ推奨される方式
- トークンがHTTPヘッダーに含まれ、URLに露出しない
Authorization: Bearer {access_token}
HTTP本文パラメータ
- POSTやPUTなどのリクエストの本文に含めて送信します
POST /resource HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
access_token={access_token}
URIクエリパラメータ
- アドレスバーのクエリストリングに含めて送信します
GET /resource?access_token={access_token} HTTP/1.1
Host: server.example.com
URIクエリパラメータ方式を使うべきでない理由
- ログやブラウザの履歴にトークンが露出してしまいます
認証成功・失敗時のレスポンス
失敗時
リソースサーバーは、クライアント認証が欠如しているか失敗した場合、以下のような WWW-Authenticate
ヘッダーを含める必要があります。
推奨事項ではありますが、必須ではありません。
WWW-Authenticate: Bearer realm="example"
- 失敗の理由に応じて、
error
、error_description
、error_uri
プロパティを追加することができます。
scope
プロパティによって、必要な権限の範囲を通知することも可能です。
主なエラーコード
-
invalid_request
:不正なリクエスト -
invalid_token
:期限切れまたは無効なトークン -
insufficient_scope
:権限不足
成功時
トークン発行時は、通常JSON形式でレスポンスが返されます。
{
"access_token": "{access_token}",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA"
}
Refresh tokenはなぜJWTを使わないのか?
リフレッシュトークンは、その役割上、JWTを使って情報を含めるよりも
(また、状態を持たないJWTを使うのであれば、そもそもJWTを使う意味があるのかという根本的な疑問が生じる)
セキュリティ上の利点がある ランダムな文字列Opaque Token
として実装するのが一般的です。
セキュリティ考慮事項(Security Considerations)
アクセストークンは、偽造・盗難・リダイレクト・再利用などに対して脆弱です。
そのため、以下のような推奨事項があります。
-
Bearerトークンは保護すること
クライアント実装は、Bearerトークンが意図しない第三者に漏洩しないように保証する必要があります。 -
TLS証明書チェーンを検証し、常にTLS(HTTPS)を使用すること
-
認証トークンはクッキーに保存せず、Authorizationヘッダーで送信すること
クロスサイトリクエストフォージェリ(CSRF)
CSRF(Cross-Site Request Forgery)は、ユーザーが意図しない行動を攻撃者により実行させられる攻撃です。
認証情報がクッキーに保存されている場合、ユーザーが悪意のあるサイトを訪問すると、そのサイトはユーザーの知らないうちに正規サービスへのリクエストを送信できます。
このとき、ブラウザは保存されているクッキーを自動的に送信してしまうため、サーバーはそれを正当なユーザーからのリクエストとして処理してしまいます。
そのため、認証トークンはクッキーではなく、JavaScriptでリクエストを送る際にのみ含まれる Authorization
ヘッダーを通じて送信することが安全です。
この方法は、CSRF攻撃を効果的に防止することができます。
-
アクセストークンの有効期限は短く設定する(short-lived)
-
スコープを制限した(scoped)トークンを発行する
例えばGoogleでは、認可された範囲に制限されたスコープを発行しています。
- トークンをURLで送信しないこと
参考資料