HTTP認証フレームワーク Basic、Digest、Bearer に関するまとめ



HTTPの認証フレームワークについて整理してみました。

# HTTP認証フレームワークとは？

HTTP認証とは、クライアントがサーバーに保護されたリソース（access protected resources）を要求する際に、`自分が誰であるかを証明する`ための手続きです。

:::note info
### HTTP（Hypertext Transfer Protocol）
HTTPは、インターネット上でウェブブラウザとウェブサーバーが通信する方法を定めたプロトコル（ルールの集合）です。
:::

## HTTP認証の流れは次の通りです

1. クライアントが認証情報なしでリクエストを送信  

```
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  
(アクセス拒否)
```

:::note info
### チャレンジ-レスポンス（challenge-response）  
HTTP認証はチャレンジ-レスポンス方式に基づいて動作します。サーバーが認証が必要なリソースに対してまず`チャレンジ`を送り、クライアントがそれに`レスポンス`することで認証情報を含む再リクエストを送信します。このパターンは **Basic、Digest、Bearer** などの様々な認証スキームで共通して使用されます。
:::

# 認証スキームとは？

クライアントがサーバーに認証情報を送信する際、どのような方式で送るかを定めたプロトコル上のルールです。

https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml

前述のように、HTTPリクエストで認証を行う際には、次のような形式でヘッダーが構成されます。

```
Authorization: <スキーム> <認証情報>
```

- `Authorization` → 認証情報を含むHTTPヘッダー  
- `スキーム` → 認証スキームの名称（使用される方式名）  
- `認証情報` → エンコードされた認証情報（ID:パスワード）

このように記述されます。

ここからは代表的な3つのスキーム、  
**Basic**、**Digest**、**Bearer** について解説していきます。

# Basic認証

https://datatracker.ietf.org/doc/html/rfc7617

```
Authorization: Basic <base64でエンコードされたID:パスワード>
```

## 認証の流れはどうなっているか？

:::note info 
### Basicチャレンジ  
```
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="example"

```
- Basicスキームでは `realm` だけが提示されます 
- `nonce` などの追加パラメータはありません  
— ヘッダーブロックの後に空行（CRLF）があることを示すと、「HTTPレスポンス形式」であることがより明確になります。
:::



1. ユーザーからユーザーIDとパスワードを取得します

```
id: haroya
pw: haroya123!@#
```

:::note info 
### IDには `:` を含めてはいけません

最初のコロン（:）以降の文字列はパスワードとして認識されます。

```
my:ID:password
```
このような場合、  
`my` はユーザーIDとして認識され、  
`ID:password` はすべてパスワードとして扱われます。

つまり、ユーザーIDに `:` が含まれていると、**正確な区別ができなくなり** 認証エラーが発生する可能性があります。

> 多くのクライアントはこのチェックを行わないため、サーバー側がユーザー入力の一部をパスワードと誤認してしまうことがあります。
:::


2.ユーザーID、コロン（:）1つ、そしてパスワードを連結して user-pass を構成します

```
<ユーザー名>:<パスワード>
```
例：
```
<haroya>:<haroya123!@#>
```

3.user-pass をオクテット列にエンコードします

```
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
```

- 必ず `UTF-8` を使用すること

:::note info
### なぜ `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 などで推奨される強力なハッシュアルゴリズムの使用が望まれます。

https://password-hashing.net

- **UTF-8と正規化（NFC）に関連するセキュリティ問題**  
正規化された文字（例：悪意のあるUnicode）により、文字列比較、検証、衝突の問題が発生する可能性があります。


# Digest認証

https://datatracker.ietf.org/doc/html/rfc7616

```
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認証との最大の違いです。


## どのように認証を行うのか

:::note info
### 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の期限切れ通知など、必要に応じて含まれる
:::

1. ユーザーからユーザー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回目のリクエスト
```
:::note info
### サーバーはどのように NC（Nonce Count）値を検証するのか？

1. NC値の保存：サーバーは各ユーザーと nonce の組み合わせごとに、最後に受け取った NC値を保存します。  
2. 新しいリクエストの検証：新しいリクエストが届いた際、サーバーは次の点を確認します：  
   - 受け取った NC 値が保存されている前回の NC 値より大きいかどうか  
   - 通常は1ずつ増えるべきですが、厳密には「前回より大きい」だけで十分です  
:::

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（所持者)`という名前が付けられています。

:::note warn
トークンさえあれば誰でも認証されてしまう方式のため、**必ず HTTPS を使用する必要があります**。
:::

## どのように認証を行うか？

:::note info
### OAuth2.0  
Bearerトークンは単に与えられるものではなく、OAuth 2.0プロトコルで定義された複数の`認可グラントフロー（Authorization Grant Flow)`を正常に完了することで取得できます。

OAuth2.0については、次回整理する予定です。
:::

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
```
:::note warn
**URIクエリパラメータ方式を使うべきでない理由** 
- ログやブラウザの履歴にトークンが露出してしまいます  
:::

## 認証成功・失敗時のレスポンス

### 失敗時  

リソースサーバーは、クライアント認証が欠如しているか失敗した場合、以下のような `WWW-Authenticate` ヘッダーを含める必要があります。

> 推奨事項ではありますが、必須ではありません。

```
WWW-Authenticate: Bearer realm="example"
```
- 失敗の理由に応じて、`error`、`error_description`、`error_uri` プロパティを追加することができます。

`scope` プロパティによって、必要な権限の範囲を通知することも可能です。

:::note info
**主なエラーコード**  
- `invalid_request`：不正なリクエスト  
- `invalid_token`：期限切れまたは無効なトークン  
- `insufficient_scope`：権限不足  
:::


### 成功時

トークン発行時は、通常JSON形式でレスポンスが返されます。

```
{
  "access_token": "{access_token}",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA"
}
```

:::note info
Refresh tokenはなぜJWTを使わないのか？

リフレッシュトークンは、その役割上、JWTを使って情報を含めるよりも  
（また、状態を持たないJWTを使うのであれば、そもそもJWTを使う意味があるのかという根本的な疑問が生じる）  
セキュリティ上の利点がある ランダムな文字列`Opaque Token`として実装するのが一般的です。
:::


:::note info
### なぜ有効期限とRefreshトークンを発行するのか？

- アクセストークンは認証情報そのものであるため、盗まれると誰でもAPIを使用できてしまいます。  
  そのため、有効期限を短く設定し、万が一の漏洩時のリスクを軽減します

- Refreshトークンにより、アクセストークンが期限切れになっても、ユーザーが再ログインせずに新しいトークンを取得できるようになります。  
  このRefreshトークンには認証情報が直接含まれておらず、サーバー側で必要に応じて無効化できるように設計されています
:::


## セキュリティ考慮事項（Security Considerations）

アクセストークンは、偽造・盗難・リダイレクト・再利用などに対して脆弱です。  
そのため、以下のような推奨事項があります。

- **Bearerトークンは保護すること**  
クライアント実装は、Bearerトークンが意図しない第三者に漏洩しないように保証する必要があります。

- **TLS証明書チェーンを検証し、常にTLS（HTTPS）を使用すること**

- **認証トークンはクッキーに保存せず、Authorizationヘッダーで送信すること**

:::note info
### クロスサイトリクエストフォージェリ（CSRF）

CSRF（Cross-Site Request Forgery）は、ユーザーが意図しない行動を攻撃者により実行させられる攻撃です。  
認証情報がクッキーに保存されている場合、ユーザーが悪意のあるサイトを訪問すると、そのサイトはユーザーの知らないうちに正規サービスへのリクエストを送信できます。  
このとき、ブラウザは保存されているクッキーを自動的に送信してしまうため、サーバーはそれを正当なユーザーからのリクエストとして処理してしまいます。

そのため、認証トークンはクッキーではなく、JavaScriptでリクエストを送る際にのみ含まれる `Authorization` ヘッダーを通じて送信することが安全です。  
この方法は、CSRF攻撃を効果的に防止することができます。
:::

- **アクセストークンの有効期限は短く設定する（short-lived）**

- **スコープを制限した（scoped）トークンを発行する**

例えばGoogleでは、認可された範囲に制限されたスコープを発行しています。  

https://developers.google.com/identity/protocols/oauth2/scopes?hl=ko

- **トークンをURLで送信しないこと**

### 参考資料

https://auth-wiki.logto.io/ko/opaque-token

https://www.tosspayments.com/blog/articles/dev-2