HTTP 401 と 403 の違い・使い分け
レビューでよく指摘されるのが 401 Unauthorized と 403 Forbidden の 取り違えです。RFC 9110 の定義に沿えば迷いません。
The canonical rule: 401 means "who are you?" (authentication), 403 means "I know who you are, but you may not" (authorization).
TL;DR
- 401: 認証されていない / 認証失敗。
WWW-Authenticateヘッダ必須 - 403: 認証済みだが権限なし。再認証しても解決しない
- リソースの存在を隠したい場合は 404 を返す
1. 正しい使い分け表 / When to use
| 状況 / Situation | 返すコード / Status | 補足 / Notes |
|---|---|---|
| ログインしていない | 401 | WWW-Authenticate: Bearer |
| トークン期限切れ | 401 | error="invalid_token" |
| トークン署名不正 | 401 | 認証自体が失敗 |
| ログイン済み・権限不足 | 403 | ロール外のリソース |
| 料金プラン未加入 | 403 (または 402) | 402 は実質未定着 |
| private リポジトリ | 404 | 存在を隠す設計 |
2. RFC 9110 の定義 / RFC definition
一次情報は RFC 9110 の401 Unauthorized(15.5.2節)と403 Forbidden(15.5.4節)です。実装やレビューで判断が割れたときは、この定義に戻ると整理できます。
401 Unauthorized: "the request has not been applied because it lacks valid authentication credentials for the target resource."
403 Forbidden: "the server understood the request but refuses to authorize it."
401 では認証方式を示すため WWW-Authenticate を 必ず付ける義務があります。 付けないと仕様違反です。
3. 実装例 / Examples
Express (Node.js)
// 401 — 認証必要
res.set("WWW-Authenticate", 'Bearer realm="api"').status(401).json({ error: "unauthenticated" });
// 403 — 権限なし
res.status(403).json({ error: "forbidden", reason: "role:admin required" });FastAPI (Python)
from fastapi import HTTPException
raise HTTPException(
status_code=401,
detail="Invalid token",
headers={"WWW-Authenticate": "Bearer"},
)Spring Security (Java)
@ExceptionHandler(AuthenticationException.class)
ResponseEntity<?> handleAuth(AuthenticationException e) {
return ResponseEntity.status(401)
.header("WWW-Authenticate", "Bearer realm="api"")
.body(Map.of("error", "unauthenticated"));
}
@ExceptionHandler(AccessDeniedException.class)
ResponseEntity<?> handleForbidden(AccessDeniedException e) {
return ResponseEntity.status(403).body(Map.of("error", "forbidden"));
}4. 間違いやすいパターン / Anti-patterns
- 403 に
WWW-Authenticateを付ける(RFC 上は 401 用) - トークン期限切れで 403 を返す(再ログインで解決するので 401 が正)
- ブラウザのログインダイアログを避けるためにすべて 403 にする(クライアントのリトライ戦略が崩れる)
401 に対して無条件リトライする
期限切れや無効なトークンを付けたまま同じリクエストを繰り返しても、認証状態は変わりません。サーバー負荷を増やすだけでなく、レート制限やアカウント保護に引っかかる原因にもなります。401 を受けたクライアントはリトライを止め、保存済み認証情報を破棄してログイン画面へ誘導します。
const response = await fetch("/api/profile", { headers: authHeaders });
if (response.status === 401) {
localStorage.removeItem("accessToken");
window.location.assign("/login?reason=session-expired");
return;
}
if (response.status === 403) {
showMessage("この操作を行う権限がありません");
}403 は再ログインで直るとは限らないため、必要なロールや申請方法を案内するほうが適切です。401 と 403 を分けることは、HTTP上の正しさだけでなくユーザー体験にも直結します。
5. よくある質問 / FAQ
期限切れトークンには 401 と 403 のどちらを返す?
有効な認証資格情報がない状態なので 401 です。Bearer 認証では WWW-Authenticate に認証方式やエラー情報を示し、クライアントが再ログインやトークン更新を判断できるようにします。
ログイン済みでも 401 を返すことはある?
送信されたトークンが失効・改ざん・対象API向けでないなど、認証に失敗した場合は 401 です。「画面上でログイン済み」かではなく、今回の要求に有効な認証資格情報があるかで判断します。
403 を受けたら再ログインすべき?
通常は不要です。認証済みで権限が足りない状態なので、権限申請や別アカウントの利用を案内します。
6. English summary
Per RFC 9110, return 401 when the client isn't authenticated (no token, expired token, invalid signature) — and include a WWW-Authenticate header so clients know what scheme to use. Return 403 when the client is authenticated but lacks permission; re-authenticating won't help. If you need to hide the existence of a resource (private repos, internal records), return 404to avoid leaking metadata.
7. 主要サービスでの実装差 / How real services differ
RFC 9110 は「どちらを返すべきか」の指針であって強制ではないため、実際のサービス・ミドルウェアごとに流儀が異なります。設計時は仕様だけでなく、利用しているスタックの挙動も確認してください。
| サービス / Service | 挙動 / Behavior |
|---|---|
| nginx | auth_basicディレクティブでの認証失敗は401、denyディレクティブによるIP制限などのアクセス拒否は403を返す。同じ「アクセス不可」でも認証か認可かでディレクティブが分かれている |
| AWS S3 | 未認証リクエストで存在しないキーを指定すると、バケットポリシー次第で403(オブジェクトの有無を教えない)を返すことが多い。存在確認をさせないための意図的な設計 |
| GitHub API | 認証情報が無効・欠落なら401。認証済みだが権限不足の場合は403だが、非公開リポジトリへの未認可アクセスは403ではなく404を返し存在自体を隠す |
| Cloudflare | WAF(Webアプリケーションファイアウォール)がリクエストをブロックした場合は403。認証の有無に関わらずルールでブロックされた旨を示す |
この表からわかるのは、「401/403の二択」だけでなく「本当に存在を教えてよいか」という第三の判断軸(404で隠す)を各サービスが独自に組み込んでいる点です。詳細は次の見出しで扱います。
8. WWW-Authenticate ヘッダの実例 / WWW-Authenticate examples
RFC 9110 Section 11.6.1 は、401 Unauthorized を返すサーバーに対して WWW-Authenticate ヘッダの送出を要求しています。クライアントはこのヘッダを見て、どの認証方式を使うべきかを判断します。
Basic 認証の場合
WWW-Authenticate: Basic realm="api", charset="UTF-8"Bearer トークン(OAuth 2.0)の場合
Bearer トークンの詳細なエラー表現は RFC 6750(OAuth 2.0 Bearer Token Usage)が定義しています。error と error_description 属性を付けることで、クライアントに失敗理由を伝えられます。
WWW-Authenticate: Bearer realm="api", error="invalid_token", error_description="The access token expired"トークン自体が送られていない場合は error 属性を省略し、単に Bearer realm="api" とするのが一般的です(RFC 6750 Section 3)。error を付けるのは「トークンはあるが無効」なケースに限られます。
9. curl での切り分け手順 / Diagnosing with curl
401 と 403 のどちらが返っているか、また認証情報の有無でどう遷移するかは curl で機械的に確認できます。
# 1. 認証情報なし → 401 が期待値
curl -i https://api.example.com/private
# 2. 無効な認証情報を付ける → 401(署名/トークン不正)
curl -i -H "Authorization: Bearer invalid-token" https://api.example.com/private
# 3. 有効な認証情報だが権限不足のリソース → 403 が期待値
curl -i -H "Authorization: Bearer $VALID_TOKEN" https://api.example.com/admin-only各レスポンスの1行目(ステータスライン)と WWW-Authenticate ヘッダの有無を見比べてください。401 なのに WWW-Authenticate が付いていない、あるいは権限不足なのに401が返っている場合は実装側の不備です。パターン3で401が返る場合は、認証・認可のチェックが分離されていない可能性があります(Spring Security の例と同じ原因)。
10. 404 で隠すべきかの設計判断 / When to hide with 404
403 はリクエストを理解した上での拒否なので、レスポンスを受け取った時点で「リソースが存在すること」自体は伝わってしまいます。存在の秘匿が必要なリソース(他人の下書き、非公開リポジトリ、他ユーザーの個人データなど)では、403ではなく404を返す設計が使われます。
- GitHub の非公開リポジトリ: 権限がない第三者には404を返し、リポジトリの存在自体を知らせない
- AWS S3: バケットポリシー次第で、未認可アクセスに403ではなく「オブジェクトなし」相当の応答を返す設定が可能
- SNSの非公開投稿・下書き: 投稿者以外には404を返し、URLの推測による存在確認(存在有無のプロービング)を防ぐ
判断基準はシンプルで、「リソースが存在すると知られること自体がリスクか」です。単なる権限不足(例: 管理画面へのアクセス)なら403で問題ありませんが、プライバシーに関わるリソースは404を検討してください。
よくある質問 / FAQ
- 401 と 403 はどう使い分ける?
- 401 は認証が必要または失敗している時、403 は認証は成功しているが権限不足で拒否された時です。401 には WWW-Authenticate ヘッダが必須。
- When to use 401 vs 403?
- 401 means authentication failed or is required; 403 means the client is authenticated but not authorized to access the resource. 401 responses must include a WWW-Authenticate header.
- 404 で隠すのは正解?
- リソースの存在を知られたくない場合は 404 を返すのが一般的(例: GitHub の private repo)。403 だと存在が漏れます。
- ログイン済みでも 401 を返すことはある?
- あります。送信されたトークンが失効・改ざん・対象API向けでないなど、今回の要求の認証に失敗した場合は 401 を返します。
- 403 を受けたら再ログインすべき?
- 通常は不要です。認証済みで権限が足りない状態なので、権限申請や別アカウントの利用を案内します。
- Spring Securityで401と403を正しく分けるには?
- AuthenticationEntryPointで401(未認証)、AccessDeniedHandlerで403(権限不足)をそれぞれ実装します。設定を分けないとデフォルトで両方403になることがあります。
- How do I return 401 vs 403 correctly in Spring Security?
- Implement AuthenticationEntryPoint for 401 (unauthenticated) and AccessDeniedHandler for 403 (forbidden) separately — without explicit configuration, Spring Security can return 403 for both cases.