Basic 認証

3 行まとめ
Section titled “3 行まとめ”- Basic 認証は HTTP 認証フレームワーク (RFC 9110 §11) の上に乗った認証スキームのひとつで、スキーム自体の仕様は RFC 7617。
- サーバーが
401 Unauthorized+WWW-Authenticate: Basic realm="..."でチャレンジし、クライアントがAuthorization: Basic base64(user-id ":" password)で応答する。ただそれだけ。 - Base64 は符号化であって暗号化ではない。平文と同じなので TLS が事実上の必須要件。仕様上も「安全なチャネル外での使用は非推奨」と明記されている。
HTTP 認証フレームワークの中の位置づけ
Section titled “HTTP 認証フレームワークの中の位置づけ”「Basic 認証」という単体の仕組みがあるわけではない。HTTP には汎用の認証フレームワークがあり、そこに差し込むスキームとして Basic / Digest / Bearer / Negotiate などが定義されている、という構造を押さえるのが先。
flowchart TB
subgraph FW["HTTP 認証フレームワーク (RFC 9110 §11)"]
direction TB
C["チャレンジ<br/>401 + WWW-Authenticate"]
R["クレデンシャル送信<br/>Authorization ヘッダ"]
C --> R
end
FW --> S1["Basic (RFC 7617)"]
FW --> S2["Digest (RFC 7616)"]
FW --> S3["Bearer (RFC 6750)"]
FW --> S4["Negotiate / NTLM など"]
style S1 fill:#4054b2,color:#fff
フレームワークが決めているのは「ヘッダ名」「ステータスコード」「スキーム名 パラメータ という文法」まで。中身の作り方はスキーム側の仕様が決める。Basic の場合は「ユーザー名とパスワードをコロンで連結して Base64」がその中身にあたる。
| 役割 | オリジンサーバー向け | プロキシ向け |
|---|---|---|
| チャレンジ | 401 Unauthorized + WWW-Authenticate |
407 Proxy Authentication Required + Proxy-Authenticate |
| クレデンシャル | Authorization |
Proxy-Authorization |
| ホップ単位か | エンドツーエンド | ホップバイホップ |
典型的な「認証情報を持たずにアクセス → 401 で弾かれる → ダイアログ入力 → 再送」の往復。
sequenceDiagram
autonumber
participant U as 利用者
participant B as ブラウザ
participant S as サーバー
B->>S: GET /private/ (Authorization なし)
S-->>B: 401 Unauthorized<br/>WWW-Authenticate: Basic realm="Staging"
B->>U: 認証ダイアログを表示 (realm を提示)
U->>B: user-id と password を入力
Note over B: base64("demo:P@ssw0rd")<br/>= "ZGVtbzpQQHNzdzByZA=="
B->>S: GET /private/<br/>Authorization: Basic ZGVtbzpQQHNzdzByZA==
S->>S: デコードして照合
alt 照合成功
S-->>B: 200 OK (コンテンツ)
Note over B: 同じ保護空間には以後<br/>先回りして Authorization を付ける
else 照合失敗
S-->>B: 401 Unauthorized (再チャレンジ)
end
ここで重要なのは 2 往復目以降。一度成功したクライアントは、同じ保護空間(後述)に対しては 401 を待たずに Authorization を先付けする。つまり実運用でのリクエストのほとんどは、いきなり Authorization 付きで飛んでくる。
ヘッダの中身を分解する
Section titled “ヘッダの中身を分解する”チャレンジ側: WWW-Authenticate
Section titled “チャレンジ側: WWW-Authenticate”WWW-Authenticate: Basic realm="Staging Area", charset="UTF-8"realm(必須): 保護空間につける名前。ブラウザのダイアログにそのまま表示されるので、利用者向けの説明文になる。逆に言うと機密情報を書いてはいけない。charset(任意): RFC 7617 で追加されたパラメータ。指定できる値はUTF-8のみ(大文字小文字は区別しない)。非 ASCII のパスワードを扱うときにクライアントへエンコードを伝える。- Basic スキームで定義されているパラメータはこの 2 つだけ。
realm がブラウザのどこに出るかは、実際のダイアログを見ると一発で分かる。

ここに表示されるのはサーバーが決めた文字列なので、「社内 VPN 用」「請求管理システム」のような 内部情報を書くと、認証を通していない相手にそのまま見える。逆に「Staging Area」のように、 何の資格情報を求めているかだけが伝わる文言にする。
クレデンシャル側: Authorization
Section titled “クレデンシャル側: Authorization”Authorization: Basic ZGVtbzpQQHNzdzByZA==生成規則は次のとおり。
user-pass = user-id ":" passwordcredentials = "Basic" SP base64(user-pass)変換の各段階を実物の値で追う手順は データフローを追う にまとめてある。
手元で確かめる
Section titled “手元で確かめる”# 生成printf 'demo:P@ssw0rd' | base64# => ZGVtbzpQQHNzdzByZA==
# デコード(誰でもできる、という事実の確認)echo 'ZGVtbzpQQHNzdzByZA==' | base64 -d# => demo:P@ssw0rd# -u を付けると curl は最初のリクエストから Authorization を付ける(先回り送信)curl -v -u demo:'P@ssw0rd' https://example.com/private/
# ヘッダを自分で組み立てる場合curl -v -H "Authorization: Basic $(printf 'demo:P@ssw0rd' | base64)" \ https://example.com/private/
# チャレンジの中身だけ見たいときcurl -sI https://example.com/private/ | grep -i www-authenticateデータフローを追う
Section titled “データフローを追う”Basic 認証で曖昧になりがちなのは「どのデータが、どの形で、どこを通り、どこで平文に戻るか」。ここだけは実物の値で完全に追えるようにしておく。
1. 入力値がヘッダになるまで
Section titled “1. 入力値がヘッダになるまで”user-id = demo / password = P@ssw0rd を例に、変換の各段階を実物で示す。
| # | 段階 | 値 | 長さ |
|---|---|---|---|
| 1 | 入力 | user-id demo / password P@ssw0rd |
4 / 8 文字 |
| 2 | コロンで連結 | demo:P@ssw0rd |
13 文字 |
| 3 | UTF-8 バイト列 | 64 65 6d 6f 3a 50 40 73 73 77 30 72 64 |
13 バイト |
| 4 | Base64 符号化 | ZGVtbzpQQHNzdzByZA== |
20 文字 |
| 5 | ヘッダ行 | Authorization: Basic ZGVtbzpQQHNzdzByZA== |
— |
flowchart TB
U1["user-id<br/>demo"] --> J["連結<br/>demo:P@ssw0rd"]
U2["password<br/>P@ssw0rd"] --> J
J --> BY["UTF-8 バイト列<br/>64 65 6d 6f 3a 50 40 ..."]
BY --> B64["Base64<br/>ZGVtbzpQQHNzdzByZA=="]
B64 --> H["Authorization: Basic<br/>ZGVtbzpQQHNzdzByZA=="]
H --> W["TLS で暗号化して送信"]
style U2 fill:#7a2222,color:#fff
style W fill:#1f5c33,color:#fff
Base64 は 3 バイトを 4 文字に変換する。13 バイトは 3 で割り切れないため、末尾に = が 2 つ付く。== で終わるのは暗号っぽさの証拠ではなく、元の長さが 3 の倍数でなかったことを示すだけである。
printf 'demo:P@ssw0rd' | xxd -g1# 00000000: 64 65 6d 6f 3a 50 40 73 73 77 30 72 64 demo:P@ssw0rd
printf 'demo:P@ssw0rd' | base64# ZGVtbzpQQHNzdzByZA==2. サーバー側はまったく逆の順にほどく
Section titled “2. サーバー側はまったく逆の順にほどく”flowchart TB
H["Authorization ヘッダ"] --> SP["スキームと値に分割<br/>Basic / ZGVtbzpQQHNzdzByZA=="]
SP --> DE["Base64 デコード<br/>demo:P@ssw0rd"]
DE --> CS["最初のコロンで分割"]
CS --> ID["user-id<br/>demo"]
CS --> PW["password<br/>P@ssw0rd"]
ID --> LK["利用者を検索"]
PW --> CMP["保存済みハッシュと<br/>定数時間で照合"]
LK --> CMP
CMP --> OK["認証結果"]
style DE fill:#7a2222,color:#fff
赤で示したデコード直後が最も危険な一点。ここでメモリ上に平文のパスワードが現れるので、この値をログ・例外メッセージ・トレースに載せてはいけない。エラー時に「送られてきたヘッダをそのままダンプする」実装が事故の典型例。
3. どこで暗号化され、どこで平文に戻るか
Section titled “3. どこで暗号化され、どこで平文に戻るか”flowchart TB
subgraph CL["クライアント"]
A["ブラウザ<br/>平文のパスワードを保持"]
end
subgraph NET["インターネット"]
B["TLS 暗号化された経路<br/>盗聴者にはヘッダも見えない"]
end
subgraph EDGE["TLS 終端 (CDN / ロードバランサ)"]
C["ここで復号される<br/>Authorization が平文になる"]
end
subgraph ORIGIN["オリジン側"]
D["リバースプロキシ<br/>nginx auth_basic"]
E["アプリケーション"]
end
F["アクセスログ / APM / トレース"]
A --> B --> C --> D --> E
C -.->|"設定次第で記録される"| F
D -.->|"設定次第で記録される"| F
E -.->|"設定次第で記録される"| F
style B fill:#1f5c33,color:#fff
style C fill:#7a2222,color:#fff
style F fill:#7a4a12,color:#fff
| 位置 | クレデンシャルの形 | 主なリスク | 対策 |
|---|---|---|---|
| ブラウザのメモリ | 平文 | 共有端末での再利用 | ブラウザを閉じる運用、そもそも Basic を使わない |
| インターネット経路 | TLS 暗号文 | TLS がなければ丸見え | HTTPS を必須にし、HTTP は 301 で追い出す |
| TLS 終端(CDN / LB) | 平文 | 事業者側のログ | ヘッダのログ出力を止める |
| リバースプロキシ | 平文 | アクセスログ、error_log の debug 出力 |
ログフォーマットから除外 |
| アプリケーション | 平文 | 例外レポート、APM のヘッダ収集、リクエストダンプ | 収集対象からマスクする |
4. 認証済みなら、その先へ渡さない
Section titled “4. 認証済みなら、その先へ渡さない”nginx で認証を終えているなら、アプリに Authorization を転送する必要はない。既定では転送されるので、明示的に落とす。
location /private/ { auth_basic "Staging Area"; auth_basic_user_file /etc/nginx/.htpasswd;
proxy_set_header Authorization ""; # 上流には渡さない proxy_set_header X-Auth-User $remote_user; # 必要なら利用者名だけ渡す
proxy_pass http://app_upstream;}log_format no_auth '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent';access_log /var/log/nginx/access.log no_auth;$http_authorization を含むカスタムログフォーマットを使っていないか、error_log ... debug; を本番で有効にしていないか(debug ではヘッダが出る)を点検する。
5. 経路全体を 1 枚で
Section titled “5. 経路全体を 1 枚で”TLS 終端・リバースプロキシ・アプリの 3 段構成で、リクエストがどう流れて何が起きるか。どのホップから平文になるかに注目して読む。
sequenceDiagram
autonumber
participant B as ブラウザ
participant L as LB / TLS 終端
participant N as nginx
participant A as アプリ
B->>L: GET /private/ を TLS で送信
Note over B,L: 経路上は暗号文<br/>ヘッダも本文も見えない
L->>N: 復号後の HTTP<br/>Authorization: Basic<br/>ZGVtbzpQQHNzdzByZA==
Note over L,N: ここから内部は平文<br/>ヘッダがそのまま流れる
N->>N: Base64 デコードし<br/>.htpasswd と照合
alt 照合成功
N->>A: 転送<br/>Authorization は落とし<br/>X-Auth-User を付与
A-->>N: 200 OK
N-->>L: 200 OK
L-->>B: 200 OK
Note over B: 同じ保護空間へは以後先回りで送信
else 照合失敗
N-->>L: 401 + WWW-Authenticate<br/>Basic realm="Staging Area"
L-->>B: 401
Note over N,A: アプリにはリクエストが到達しない
end
認証を前段で終えるとアプリにリクエストが届かないという点は、アプリ側のアクセス解析やレート制限を設計するときに影響する。ステージングの Basic 認証で「アプリのログに何も出ない」のは正常な挙動。
6. クレデンシャルはクライアント側でいつまで残るか
Section titled “6. クレデンシャルはクライアント側でいつまで残るか”stateDiagram-v2
[*] --> None
None : クレデンシャル未保持
Cached : 保護空間ごとに保持
None --> Cached : 401 後に入力して成功
Cached --> Cached : 同一保護空間へ先回り
Cached --> None : タブやブラウザを閉じる
Cached --> None : 再度 401 が返る
Cached --> [*] : ログアウト手段がない
保存先はブラウザのメモリで、Cookie ストアのように利用者から見える形では管理されない。だから「ログアウトボタン」を作れず、確認も削除も利用者には難しい。この一点が Basic 認証を一般利用者向けに使えない最大の理由になる。
Base64 は暗号化ではない
Section titled “Base64 は暗号化ではない”試験でも実務でも最初に問われるのがここ。3 つの違いは「鍵が要るか」と「元に戻せるか」で きれいに分かれる。

| Base64 | 暗号化 | ハッシュ | |
|---|---|---|---|
| 鍵 | 不要 | 必要 | 不要 |
| 元に戻せるか | 誰でも戻せる | 鍵があれば戻せる | 戻せない |
| 目的 | バイナリを ASCII で運ぶ | 秘匿 | 同一性の検証 |
Authorization: Basic ... は平文でパスワードを送っているのと等価。ここから次の帰結が出る。
- TLS なしの Basic 認証は成立しない。 RFC 7617 §4 も、暗号化されていないチャネルでの利用は攻撃者にパスワードを渡すのと同じだと明言している。
- リバースプロキシやアクセスログに残りやすい。
Authorizationヘッダをログ出力する設定にしていると、パスワードがそのままログに落ちる。 - リクエストのたびに毎回送信される。 セッション Cookie と違い、有効期限も失効の仕組みもない。漏れた瞬間から、パスワードを変えるまで使われ続ける。
保護空間と先回り送信
Section titled “保護空間と先回り送信”RFC 9110 は protection space(保護空間)= オリジン + realm と定義している。クライアントはこの単位でクレデンシャルをキャッシュする。
flowchart TD
Start["リクエストを送ろうとしている"] --> Q1{"同じオリジンで<br/>認証に成功した realm の<br/>クレデンシャルを持っているか"}
Q1 -->|"いいえ"| Send1["Authorization なしで送る"]
Q1 -->|"はい"| Q2{"その URI は<br/>認証済み URI と<br/>同じ階層か下位か"}
Q2 -->|"はい"| Send2["先回りして<br/>Authorization を付けて送る"]
Q2 -->|"いいえ"| Send1
Send1 --> R{"応答は 401 か"}
R -->|"はい"| Prompt["realm を見て<br/>ダイアログ表示 / 再送"]
R -->|"いいえ"| Done["完了"]
Send2 --> Done
Basic には Digest のような nonce のやりとりがないため、先回り送信 (preemptive authentication) が仕様上許されているのが特徴。パス階層をヒントに送信範囲を推定するので、/private/ で認証したら /private/docs/ にも自動で付く、という挙動になる。
サーバー側の判定フロー
Section titled “サーバー側の判定フロー”自前で実装するときに書くことになるロジック。
flowchart TD
A["リクエスト受信"] --> B{"Authorization ヘッダあり"}
B -->|"なし"| C["401 + WWW-Authenticate"]
B -->|"あり"| D{"スキームが Basic か<br/>大文字小文字は区別しない"}
D -->|"違う"| C
D -->|"Basic"| E{"Base64 としてデコードできるか"}
E -->|"失敗"| F["400 Bad Request"]
E -->|"成功"| G{"最初のコロンで<br/>分割できるか"}
G -->|"コロンなし"| F
G -->|"できる"| H["user-id と password を取得"]
H --> I{"利用者が存在し<br/>パスワードが一致するか"}
I -->|"不一致"| C
I -->|"一致"| J{"その利用者に<br/>この操作の権限があるか"}
J -->|"ない"| K["403 Forbidden"]
J -->|"ある"| L["200 など通常応答"]
押さえどころは 3 つ。
- スキーム名の比較は大文字小文字を区別しない(
basic,BASICも有効)。 - デコード失敗やコロン欠落は 401 ではなく 400 が素直。認証情報の誤りではなく、リクエストの文法エラーだから。
- 401 と 403 の分岐。認証(誰か)と認可(何をしてよいか)で層が違う。
401 / 403 / 407 の使い分け
Section titled “401 / 403 / 407 の使い分け”| コード | 意味 | 必須ヘッダ | 再送で解決するか |
|---|---|---|---|
401 Unauthorized |
未認証、または認証情報が誤り | WWW-Authenticate が必須 |
する(正しい情報なら通る) |
403 Forbidden |
認証は通ったが権限がない | なし | しない |
407 Proxy Authentication Required |
プロキシに対する未認証 | Proxy-Authenticate が必須 |
する |
他の認証方式との比較
Section titled “他の認証方式との比較”| 方式 | クレデンシャル | 毎回送るもの | 失効・ログアウト | 主な用途 |
|---|---|---|---|---|
| Basic | user-id + password | パスワードそのもの (Base64) | 仕組みがない | 内部ツール、ステージングの目隠し |
| Digest | user-id + password | nonce を使ったハッシュ | 仕組みがない | 平文を流せない環境の互換用途 |
| Bearer | トークン | トークン | 有効期限・失効リストで可能 | API、OAuth 2.0 / OIDC |
| Cookie セッション | ログイン後のセッション ID | セッション ID | サーバー側で破棄できる | 一般的な Web アプリ |
| mTLS | クライアント証明書 | 証明書(TLS 層) | 失効リスト / 短命証明書 | 機器間通信、ゼロトラスト |
Digest は「パスワードを平文で流さない」ための方式だが、MD5 前提の実装が多く、サーバー側にパスワード相当の値を保存する必要がある(HA1 を保存するため、ハッシュ化保存の利点が薄い)。TLS が当たり前になった現在、TLS + Basic のほうが TLS なし Digest より安全という整理になる。
flowchart TD
A["認証方式を選ぶ"] --> B{"ブラウザで使う<br/>一般利用者向けか"}
B -->|"はい"| C{"ログアウトや<br/>パスワード再設定の<br/>UI が要るか"}
C -->|"要る"| D["Cookie セッション<br/>または OIDC"]
C -->|"不要 / 目隠し用途"| E["Basic 認証"]
B -->|"いいえ"| F{"第三者アプリに<br/>権限を委譲するか"}
F -->|"する"| G["OAuth 2.0 + Bearer"]
F -->|"しない"| H{"呼び出し元は<br/>自前のサーバーか"}
H -->|"はい"| I["Bearer / API キー<br/>または mTLS"]
H -->|"いいえ"| E
style E fill:#4054b2,color:#fff
使ってよいケースと使ってはいけないケース
Section titled “使ってよいケースと使ってはいけないケース”Basic 認証は「古くて危険な方式」ではなく、成立条件が狭い方式。条件を満たすなら、追加のライブラリもテーブルもセッションストアも要らない最も安いやり方になる。逆に条件をひとつ外すと、ほかの方式で当然できることが何ひとつできない。
成立の前提条件
Section titled “成立の前提条件”次の 6 つがすべて満たされているかで判断する。ひとつでも欠けたら別の方式にする。
- HTTPS が強制されている(HTTP で 200 を返す経路が残っていない)
- 利用者が少数で、パスワードを人手で発行・配布・変更できる規模
- ログアウトとパスワード再設定の UI が不要
- 認可の粒度が**「入れる / 入れない」で足りる**(ロール別の細かい権限が要らない)
- 漏洩時の復旧が**「パスワードを変えて配り直す」で済む**
- 総当たり対策を前段に置ける(レート制限、IP 制限、fail2ban など)

判断そのものはこの順に辿ると早い。
flowchart TB
S["Basic 認証を使いたい"] --> Q1{"HTTPS が強制されているか"}
Q1 -->|"いいえ"| NG1["使わない<br/>まず TLS を用意する"]
Q1 -->|"はい"| Q2{"利用者は<br/>一般のエンドユーザーか"}
Q2 -->|"はい"| NG2["使わない<br/>Cookie セッション / OIDC"]
Q2 -->|"いいえ"| Q3{"呼び出し元は<br/>人ではなくプログラムか"}
Q3 -->|"はい"| Q4{"高エントロピーな鍵を<br/>環境変数などで持てるか"}
Q4 -->|"はい"| OK1["使える<br/>API キーを Basic で運ぶ"]
Q4 -->|"いいえ"| NG3["使わない<br/>Bearer / mTLS"]
Q3 -->|"いいえ"| Q5{"ロールごとの権限や<br/>監査ログが必要か"}
Q5 -->|"必要"| NG4["使わない<br/>セッション + 認可基盤"]
Q5 -->|"不要"| OK2["使える<br/>目隠しや内部ツール"]
style OK1 fill:#1f5c33,color:#fff
style OK2 fill:#1f5c33,color:#fff
style NG1 fill:#7a2222,color:#fff
style NG2 fill:#7a2222,color:#fff
style NG3 fill:#7a2222,color:#fff
style NG4 fill:#7a2222,color:#fff
使ってよいケース
Section titled “使ってよいケース”| ケース | 成立する理由 | 一緒にやること |
|---|---|---|
| ステージング / プレビュー環境の目隠し | 認証というより第三者とクローラを弾く用途。利用者は開発チームだけ | 本番と別のパスワード、後述の除外設定 |
| 社内の管理画面・監視ダッシュボード | 利用者が少数で、退職時の対応が現実的 | VPN や IP 制限との併用 |
| サーバー間 API / API キーの運搬 | 呼び出し元がプログラムなので、ログアウトも再設定も不要 | 十分に長いランダム鍵、鍵のローテーション |
| CI / CD からのアーティファクト取得 | シークレットストアに置いた値をヘッダに載せるだけ | リポジトリのシークレット管理に任せる |
| 短命な限定公開ページ | 期間が終われば設定ごと消す | 期限を決めて必ず消す |
| ローカルの開発ツール | 外部に露出しない | そのまま本番に持ち込まない |
「API キーを Basic で運ぶ」パターンは現役である。たとえば Stripe の API は、シークレットキーを user-id、パスワードを空にして送る。
# user-id にシークレットキー、password は空(末尾のコロンがそれを表す)curl https://api.stripe.com/v1/charges -u sk_test_xxxxxxxxxxxx:このパターンが成り立つのは、Basic 認証の弱点がすべて無効化されるから。
- ログアウトが要らない(相手はブラウザではない)
- パスワード再設定の UI が要らない(鍵のローテーションで済む)
- 総当たりが効かない(鍵のエントロピーが十分に高い)
- 失効できる(鍵を無効化すればよい。パスワードと違い 1 対 1 で捨てられる)
Prometheus の basic_auth、Elasticsearch の組み込みユーザー、プライベートな npm レジストリの _auth、Docker レジストリのログインなど、機械同士の通信では今も広く使われている。
使ってはいけないケース
Section titled “使ってはいけないケース”| ケース | できないこと |
|---|---|
| 一般利用者向けのログイン | ログアウトできない、パスワード再設定の導線を作れない、2 要素認証を挟めない、ログイン画面をブランドに合わせられない |
| TLS がない経路 | パスワードが平文で流れる。議論の余地がない |
| 第三者アプリへの権限委譲 | 相手にパスワードそのものを渡すことになる。OAuth 2.0 の領域 |
| ロールや権限の制御が必要 | Basic が運ぶのは「誰か」だけ。権限は別の層で持つ必要がある |
| セッション単位の監査が必要 | セッションという概念がなく、いつ開始・終了したかを追えない |
| 共有端末で使う画面 | ブラウザのメモリに残り、閉じるまで次の人が使える |
| 高頻度アクセスの公開 API | リクエストごとにパスワード検証が走る。bcrypt などを正しく使うほど重くなる |
| モバイルアプリの利用者認証 | 端末にパスワードを保存することになる。失効も更新もできない |
| 人の入れ替わりがある本番環境 | 退職者ひとりのために全員のパスワードを変えることになる |
ステージングに置くときの実務的な注意
Section titled “ステージングに置くときの実務的な注意”目隠し用途は最も多いケースだが、Basic 認証はリクエストを区別せず全部弾くので、機械からのアクセスが軒並み壊れる。除外設定を先に洗い出しておく。
| 壊れるもの | 症状 | 対応 |
|---|---|---|
| Webhook の受信 | 外部サービスからの通知が 401 で届かない | 受信パスだけ auth_basic off; |
| ヘルスチェック・監視 | 常に 401 で異常判定になる | /healthz を除外 |
| OGP のクロール | Slack などでリンク展開されない | 仕様として受け入れるか、UA で除外 |
| 決済などのコールバック | 外部からの戻りが弾かれる | コールバックパスを除外 |
| E2E テスト | CI から開けない | CI のシークレットに認証情報を置く |
| 画像・フォントなどの副リソース | 先回り送信が効かない経路で 401 | 同一オリジンに置く(クロスオリジンは CORS も要る) |
逆に、クローラ避けとしては noindex より Basic 認証のほうが確実。noindex は「クロールされたうえで載せない」だが、401 はそもそも中身を渡さない。
server { listen 443 ssl; server_name staging.example.com;
location /private/ { auth_basic "Staging Area"; # realm になる auth_basic_user_file /etc/nginx/.htpasswd; }
# ヘルスチェックだけ認証を外す location = /healthz { auth_basic off; }}# apache2-utils / httpd-tools の htpasswd を使う# -B で bcrypt(-c は新規作成なので 2 人目以降は付けない)htpasswd -B -c /etc/nginx/.htpasswd demohtpasswd -B /etc/nginx/.htpasswd another-user.htpasswdはドキュメントルートの外に置く。中に置くと配信されうる。- nginx が読める形式は crypt / apr1 (MD5) /
{SHA}/ bcrypt。{SHA}はソルトなしの SHA-1 なので使わない。 - 認証を外したい下位パスがある場合は
auth_basic off;を個別のlocationに書く。
Apache
Section titled “Apache”<Directory "/var/www/html/private"> AuthType Basic AuthName "Staging Area" AuthBasicProvider file AuthUserFile /etc/httpd/.htpasswd Require valid-user</Directory>Require valid-user を Require user demo にすれば特定ユーザーのみ、Require group staff + AuthGroupFile でグループ制御になる。
Node.js (Express) — 自前実装するなら
Section titled “Node.js (Express) — 自前実装するなら”import { timingSafeEqual, createHash } from 'node:crypto';
const REALM = 'Staging Area';
// 長さの差から情報が漏れないよう、比較前に固定長へ潰すfunction safeEqual(a, b) { const ha = createHash('sha256').update(a).digest(); const hb = createHash('sha256').update(b).digest(); return timingSafeEqual(ha, hb);}
export function basicAuth(users) { return (req, res, next) => { const header = req.get('authorization') ?? ''; const [scheme, encoded] = header.split(' ');
if (!encoded || scheme.toLowerCase() !== 'basic') { return challenge(res); }
let decoded; try { decoded = Buffer.from(encoded, 'base64').toString('utf8'); } catch { return res.sendStatus(400); }
const sep = decoded.indexOf(':'); // 最初のコロンで分割する if (sep < 0) return res.sendStatus(400);
const userId = decoded.slice(0, sep); const password = decoded.slice(sep + 1); const expected = users[userId];
// 利用者が存在しない場合も同じ経路を通し、処理時間を揃える if (!expected || !safeEqual(password, expected)) { return challenge(res); }
req.user = { id: userId }; next(); };}
function challenge(res) { res.set('WWW-Authenticate', `Basic realm="${REALM}", charset="UTF-8"`); res.status(401).end();}ポイントは 4 つ。
===で比較しない。 文字列比較は先頭から一致を見るため、応答時間からパスワードを 1 文字ずつ推測されうる。timingSafeEqualを使い、長さの差も出さないようハッシュしてから比較する。- 利用者が存在しない場合も同じ分岐を通す。 早期 return すると「そのユーザーは存在しない」が応答時間から分かる。
- 最初のコロンで分割する。
split(':')の結果を素朴に使うとコロン入りパスワードが壊れる。 - 401 には必ず
WWW-Authenticateを付ける。 付け忘れると仕様違反であり、クライアントは再試行の方法を知りようがない。
運用とセキュリティで押さえる点
Section titled “運用とセキュリティで押さえる点”パスワードの保存
Section titled “パスワードの保存”Basic 認証はサーバーに平文と照合できる形が必要……ではない。受け取るのが平文なので、サーバー側はハッシュ化して保存できる(Digest と違う利点)。htpasswd -B の bcrypt や、アプリ側なら Argon2id を使う。
ブルートフォース
Section titled “ブルートフォース”失効の仕組みがなく毎回パスワードが飛んでくるため、総当たりが素直に効く。
- レート制限(nginx なら
limit_req) - fail2ban などによる送信元単位の遮断
- そもそも十分に長いランダムパスワードを使う
Authorization ヘッダ付きリクエストへの応答は、共有キャッシュに保存してはならないのが原則(RFC 9111 §3.5)。public や s-maxage などを明示した場合のみ許される。CDN の前段に Basic 認証を置くときは、この扱いを取り違えると別の利用者に認証済みコンテンツが配られる。
Authorization は CORS のセーフリスト対象外のリクエストヘッダなので、クロスオリジンで付けるとプリフライトが飛ぶ。
Access-Control-Allow-Origin: https://app.example.comAccess-Control-Allow-Headers: AuthorizationAccess-Control-Allow-Credentials: trueAccess-Control-Allow-Origin: * と Allow-Credentials: true は併用できない点にも注意。
ログアウトできない問題
Section titled “ログアウトできない問題”Basic 認証にはログアウトの標準的な手段がない。ブラウザはクレデンシャルをキャッシュし、タブを閉じるまで送り続ける。
flowchart TD
A["ログアウトさせたい"] --> B{"取れる手段"}
B --> C["再度 401 を返して<br/>キャッシュを無効化させる"]
B --> D["ブラウザを閉じてもらう"]
B --> E["そもそも Basic を使わず<br/>セッション方式にする"]
C --> F["ブラウザ実装依存で<br/>確実ではない"]
D --> F
E --> G["確実"]
style G fill:#4054b2,color:#fff
URL に https://user:pass@example.com/ の形で埋め込む手法は、フィッシングに悪用されたため現代のブラウザでは制限・非推奨(サブリソースでは無視され、トップレベル遷移でも挙動がばらつく)。回避策として当てにしない。
API に Basic を使うときのダイアログ抑止
Section titled “API に Basic を使うときのダイアログ抑止”WWW-Authenticate: Basic を返すと、ブラウザからの XHR / fetch でもネイティブの認証ダイアログが出てしまうことがある。API では
- 401 を返すが
WWW-Authenticateを省略する(仕様違反にはなるが実務でよく使われる) - スキーム名を Basic 以外にする
- そもそも Bearer を使う
のいずれかを選ぶ。「ダイアログを出さないために WWW-Authenticate を消す」は仕様違反である、という点は認識したうえでやる。
引っかかりやすいポイント
Section titled “引っかかりやすいポイント”-
401は「未認証」、403は「認可失敗」。名前と意味が逆に見える。 -
401にはWWW-Authenticateが必須。407にはProxy-Authenticateが必須。 - クレデンシャルは
base64(user-id ":" password)。Base64 は暗号化ではない。 - user-id にコロンは使えない。password には使える。
-
charsetパラメータに指定できるのはUTF-8のみ。 -
realmはダイアログに表示される。保護空間の識別子であり、機密を書かない。 - 保護空間 = オリジン + realm。パスではない。
- クライアントは 401 を待たず先回りで送ってよい(Basic の特徴)。
- プロキシ版は
407/Proxy-Authenticate/Proxy-Authorizationの 3 点セット。 - サーバー側の照合はタイミングセーフに。文字列の
===は使わない。 -
Authorization付き応答は共有キャッシュに保存しないのが既定。 -
Authorizationは CORS セーフリスト外 → プリフライトが飛ぶ。 - ログアウトの標準手段がない。
- Basic が成立する条件は TLS 必須・利用者が少数・ログアウト不要・認可が粗い・失効が配り直しで済む・総当たり対策が前段にあるの 6 点。
- API キーを Basic で運ぶのは現役のパターン(相手がプログラムなら弱点が無効化される)。
- ステージングの目隠しに使うと Webhook・ヘルスチェック・OGP クロールが 401 で壊れる。
Basic 認証のクレデンシャルはどう組み立てるか。
user-id と password をコロンで連結した文字列を UTF-8 のバイト列にし、Base64 で符号化する。Authorization: Basic <その値> として送る。
ユーザー名にコロンを含められるか。パスワードはどうか。
ユーザー名は不可。パスワードは可。デコード後の文字列を最初のコロンで分割するため、以降のコロンはすべてパスワード側に含まれる。
401 と 403 の違いは。
401 は「認証されていない、または認証情報が誤っている」状態で、WWW-Authenticate を伴い、正しい情報を送れば通る。403 は「認証は済んでいるが権限がない」状態で、同じ情報を送り直しても通らない。
realm は何のためにあるか。
保護空間に付ける名前。クライアントは「オリジン + realm」の単位でクレデンシャルをキャッシュし、ブラウザは認証ダイアログにこの文字列を表示する。同一サーバー内で認証範囲を分けるために使う。
Base64 を使っているのに安全でないと言われるのはなぜか。
Base64 は鍵を必要としない可逆な符号化で、誰でも復号できる。パスワードを平文で送っているのと等価であり、通信路の暗号化(TLS)がなければそのまま盗聴される。
Digest 認証は Basic より安全か。
平文を流さない点では優れるが、MD5 前提の実装が多く、サーバー側にパスワード相当の値(HA1)を保存する必要があるためハッシュ保存の利点が薄い。TLS 上で使うなら Basic のほうが実装も運用も素直で、実質的な安全性でも劣らない。
プロキシ認証で使うステータスコードとヘッダは。
407 Proxy Authentication Required、Proxy-Authenticate(チャレンジ)、Proxy-Authorization(クレデンシャル)。これらはホップバイホップで、次のプロキシには転送されない。
Basic 認証でログアウトを実装するには。
標準的な手段はない。再度 401 を返してブラウザのキャッシュを無効化させる、ブラウザを閉じてもらう、といった実装依存の回避策しかない。確実にログアウトが必要ならセッション方式や Bearer トークンを使う。
サーバー側でパスワードを照合するときの注意点は。
タイミング攻撃を避けるため、定数時間比較(timingSafeEqual など)を使う。長さの差が漏れないよう事前にハッシュ化して固定長にし、ユーザーが存在しない場合も同じ経路を通して処理時間を揃える。
Basic 認証を使ってよいのはどんなときか。
HTTPS が強制されていて、利用者が人手で管理できる少数に限られ、ログアウトとパスワード再設定の UI が不要で、認可の粒度が「入れる / 入れない」で足り、漏洩時はパスワードを配り直せば復旧でき、総当たり対策を前段に置ける場合。ステージングの目隠し、社内の管理画面、サーバー間 API がこれに当たる。
一般利用者向けのログインに Basic 認証を使えないのはなぜか。
ログアウトできず、パスワード再設定の導線を作れず、2 要素認証を挟めず、ログイン画面の見た目も制御できない。クレデンシャルはブラウザのメモリにあり、利用者からは確認も削除もできない。
API キーを Basic 認証で送るのが実務で許容されるのはなぜか。
相手がブラウザではなくプログラムなので、Basic の弱点がすべて無効化される。ログアウトもパスワード再設定の UI も不要で、鍵のエントロピーが十分に高いため総当たりが効かず、鍵を無効化すれば失効できる。Stripe の API はシークレットキーを user-id、パスワードを空にして送る形をとっている。
ステージング環境に Basic 認証を置くと壊れるものは。
機械からのアクセスが軒並み 401 になる。Webhook の受信、ヘルスチェックと監視、OGP のクロール、外部サービスからのコールバック、E2E テストが代表例。必要なパスは個別に認証を外す。
クロスオリジンの fetch で Authorization を付けると何が起きるか。
Authorization は CORS のセーフリストヘッダではないため、プリフライト (OPTIONS) が発生する。サーバーは Access-Control-Allow-Headers: Authorization を返す必要がある。資格情報付きにする場合は Access-Control-Allow-Credentials: true が必要で、Access-Control-Allow-Origin: * は使えない。