【決定版】401 Unauthorizedと403 Forbiddenの違いと、迷わないAPIエラー設計・フロント実装パターン

この記事は約18分で読めます。
この記事が役立ったらブックマーク! あとで読み返したり、環境構築時のリファレンスに活用できます
B! はてなブックマークに追加

WebアプリケーションやAPIを開発・設計していると、必ず一度は頭を悩ませるのが「このエラー、401 と 403 のどっちを返すべき?」という問題です。
どちらも「リクエストが通らなかった」ことを表すクライアントエラー(400番台)ですが、両者の役割と責務は完全に分かれています。ここを取り違えて適当に実装してしまうと、フロントエンド側の画面遷移制御が破綻したり、セキュリティ上の情報漏洩につながるリスクすらあります。
本記事では、RFC仕様と現場のAPI設計の両面から、401と403の決定的な違いと正しい使い分け方を徹底解説します。

1. なぜ401と403はいつまでも混同されるのか?

最大の元凶は、401のステータスコード名である「Unauthorized」というネーミングの歴史的トラップにあります。
英語の Authorize は本来「認可する・権限を与える」という意味を持ちます。そのため、直訳すると:

  • 401 Unauthorized = 「認可されていない(権限がない)」
  • 403 Forbidden = 「禁止されている(権限がない)」

となり、言葉の定義だけで考えると「どっちも権限エラーじゃないか?」と混乱してしまうのです。
しかし、HTTP仕様上の実態は完全に逆です。

  • 401 Unauthorized = 本来は Unauthenticated(未認証) を指す
  • 403 Forbidden = 本来の Unauthorized(未認可・権限不足) を指す

HTTP/1.0が策定された1990年代初頭、認証(Authentication)と認可(Authorization)の用語の使い分けが現在ほど厳密に統一されていなかった名残で、401に Unauthorized という名前が付けられてしまいました。
まずは「名前の直訳に惑わされない」ことが、この2つを正しく使いこなすための第一歩です。

2. コア概念:「認証(Authentication)」と「認可(Authorization)」の違い

401と403を正しく使い分けるためには、セキュリティの基礎である「認証」と「認可」の境界線を完全に理解しておく必要があります。

  • 認証(Authentication / 略称: AuthN)
    • 問い: 「あなたは何者ですか?(Who are you?)」
    • 目的: アクセスしてきた通信主体の身元(アイデンティティ)を特定・検証すること。
    • 具体例:
      • ログイン画面でID・パスワードを入力して照合する
      • 生体認証(指紋・顔認証)で本人確認を行う
      • リクエストヘッダーのAPIキーやJWTトークンの署名を検証する
  • 認可(Authorization / 略称: AuthZ)
    • 問い: 「あなたに何をする権利がありますか?(What can you do?)」
    • 目的: 身元が判明した特定のユーザーに対して、特定のリソースや操作に対する実行権限を与える(または拒絶する)こと。
    • 具体例:
      • 一般ユーザーには閲覧のみを許可し、管理者(Admin)にのみ削除ボタンを許可する
      • 無料プラン会員には見せない有料限定コンテンツをブロックする
      • ユーザーAがユーザーBの非公開プロフィールを勝手に変更できないように制限する

日常生活に例えると:

  • 認証(AuthN): コンサート会場の入口で「身分証明書」を見せて、本人であることを確認してもらうこと。
  • 認可(AuthZ): 会場に入った後、「一般チケットのリストバンド」では関係者専用のバックステージ(控室)への立ち入りを警備員に止められること。

この「身元確認(認証)」でコケたときに返すのが 401 であり、「身元は分かったが立ち入り権限がない(認可)」ときに返すのが 403 です。

3. 401 Unauthorized の本質と仕様

HTTP仕様(RFC 9110)における定義

RFC 9110において、401は「対象リソースに対して有効な認証資格情報(Credentials)が提示されていないため、リクエストが適用されなかった」ことを示します。
401の最大の特徴は、「正しい認証情報を付与してリクエストを再試行(Retry)すれば成功する可能性がある」という点です。

必須要件:WWW-Authenticate ヘッダー

仕様上、サーバーが 401 Unauthorized を返す際は、レスポンスヘッダーに WWW-Authenticate を含める必要があります。ここには「クライアントがどのように認証を行えばよいか(認証スキームやレルム)」を明記します。

HTTP
HTTP/1.1 401 Unauthorized
Date: Sun, 16 Aug 2026 05:12:45 GMT
WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token expired"
Content-Type: application/json

{
  "error": "token_expired"
}

401を返す代表的なケース

  • Authorization ヘッダーそのものが存在しない(未ログイン状態でのアクセス)
  • 指定された Bearer トークン(JWTなど)の署名が不正・改ざんされている
  • アクセストークンの有効期限が切れている
  • Basic認証のユーザー名・パスワードが間違っている

4. 403 Forbidden の本質と仕様

HTTP仕様(RFC 9110)における定義

RFC 9110において、403は「サーバーはリクエストの内容を理解したが、実行を拒否した」ことを示します。
401との決定的な違いは、「同じ認証情報(資格情報)で何度リクエストを再試行しても、結果は変わらない(拒絶される)」という点です。

403を返す代表的なケース

身元(認証)は正常に確認できているものの、そのリソースに対する操作権限が不足している場合に発生します。

  • ロール・権限不足(RBAC): 一般ユーザー(Role: member)が管理者用エンドポイント(/api/v1/admin/settings)にアクセスした
  • マルチテナント・IDOR対策: 企業Aのユーザーが企業Bの請求データ(/api/v1/invoices/999)を閲覧・更新しようとした
  • 機能制限(ペイウォール): 無料プランのアカウントが有料プラン専用APIを実行した
  • アカウントステータス: 規約違反等によりアカウントが一時停止(BAN)・凍結されている
  • ネットワーク・WAF制限: 接続元IPアドレスがホワイトリスト外、またはWAFルールに検知されて拒否された

5. 実践判定:このシチュエーションは401?それとも403?

現場で遭遇しやすいシチュエーションをマトリクスで整理します。

シナリオ判定理由・解説
未ログインで「マイページ情報取得API」を叩いた401身元が特定できないため。ログイン画面へ誘導すべき状態。
JWTの有効期限が切れていた401認証情報が無効。リフレッシュトークンで再取得を試みるべき状態。
一般ユーザーが「他人のプロフィール更新API」を叩いた403身元は特定できているが、他人のリソースを書き換える権限がないため。
無料会員が「プレミアム限定コンテンツDL API」を叩いた403ユーザー認証は完了しているが、プラン権限が不足しているため。
アカウントが強制退会・凍結処分になっている403認証(誰か)は通るが、サービス利用権限が剥奪されているため。
ログイン後、2段階認証(OTP)が未入力のままAPIを叩いた401「認証プロセスがまだ完了していない」ため。401でOTP入力画面へ促す。

6. API設計とフロントエンド連携のベストプラクティス

401と403のステータスコードを正しく定義できたら、次はクライアント側が迷わず制御できるレスポンス設計とハンドリングの実装を行います。

① エラーレスポンスの構造化(RFC 7807 / RFC 9457 Problem Details)

単にステータスコードを返すだけでなく、機械可読性の高い形式で「なぜ失敗したのか」を返します。標準規格である Problem Details に準拠したレスポンスを返すのが業界標準です。

401(トークン期限切れ)のレスポンス例:

JSON
{
  "type": "https://api.example.com/errors/token-expired",
  "title": "Unauthorized",
  "status": 401,
  "code": "AUTH_TOKEN_EXPIRED",
  "detail": {
    "ja": "アクセストークンの有効期限が切れています。リフレッシュトークンで再取得してください。",
    "en": "The access token has expired. Please refresh your token."
  }
}

403(権限不足)のレスポンス例:

JSON
{
  "type": "https://api.example.com/errors/insufficient-permissions",
  "title": "Forbidden",
  "status": 403,
  "code": "FORBIDDEN_ADMIN_REQUIRED",
  "detail": {
    "ja": "この操作を実行するには管理者(admin)ロールが必要です。",
    "en": "Administrator role (admin) is required to perform this action."
  }
}

② フロントエンド(Axios Interceptor等)のハンドリング切り分け

401と403では、フロントエンドが取るべき事後アクションが明確に異なります。

TypeScript
import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
});

apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    const status = error.response?.status;

    // 【401 Unauthorized の場合 / For 401 Unauthorized】
    // 認証情報が切れているため、トークン更新を試行する
    // Credentials expired; attempt to refresh the access token
    if (status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      try {
        const newAccessToken = await refreshAccessToken();
        originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
        return apiClient(originalRequest); // 元のリクエストを再試行 / Retry original request
      } catch (refreshError) {
        // 更新も失敗したら強制ログアウトしてログイン画面へ
        // If refresh fails, force logout and redirect to login page
        authStore.clearAuth();
        window.location.href = '/login?session_expired=1';
        return Promise.reject(refreshError);
      }
    }

    // 【403 Forbidden の場合 / For 403 Forbidden】
    // トークンは有効なのでログアウトさせず、アクセス拒絶専用のUIを表示する
    // Token is valid; do not log out, show access denied UI instead
    if (status === 403) {
      notification.error({
        title: 'アクセス権限がありません / Access Denied',
        message:
          'この操作を行う権限が付与されていません。管理者に申請してください。 / You do not have permission to perform this action. Please contact an administrator.',
      });
      // または 403 専用エラー画面(/forbidden)へ遷移
      // Or redirect to dedicated 403 error page (/forbidden)
    }

    return Promise.reject(error);
  }
);
  • 401の責務: トークン再取得・ログイン画面へのリダイレクト・認証情報の破棄。
  • 403の責務: トークンは維持したまま、権限不足の通知・専用のアクセス拒否画面への誘導。

③ セキュリティ上の配慮:あえて「404 Not Found」を返すケース

API設計において、「403を返すと逆に脆弱性になる」パターンが存在します。
例えば、ユーザーAが推測で GET /api/projects/987(ユーザーBの非公開プロジェクト)にアクセスしたとします。

  • 403 Forbidden を返した場合: 攻撃者は「ID: 987 という機密プロジェクトが実在するが、自分には権限がないのだな」と把握でき、リソースの列挙(列挙攻撃 / IDOR)につながります。
  • 404 Not Found を返した場合: 「そもそもそんなプロジェクトは存在しない」のか「権限がない」のかを攻撃者が判別できなくなります。

GitHubのプライベートリポジトリやAWSのS3バケットアクセスなどで採用されているパターンです。「リソースの存在自体を秘匿したい場合は、認可エラーでもあえて404を返す」という設計パターンも選択肢として押さえておきましょう。

7. まとめ:迷ったときの判断基準

401と403の使い分けに迷ったら、次のフローチャートに従って判断してください。

Plaintext
Q1. 呼び出し元の「身元(Identity)」は正しく確認できているか?
    Is the caller's identity properly verified?
  ├─ NO(未ログイン / トークン切れ / 署名不正)
  │      (Not logged in / Token expired / Invalid signature)
  │   └── 👉 【401 Unauthorized】
  │
  └─ YES(身元は特定済み / Identity confirmed)
      │
      Q2. そのリソース・操作に対する「権限」を持っているか?
          Does the caller have permission for this resource/action?
        ├─ NO(ロール不足 / 他人のデータ / プラン制限)
        │      (Insufficient role / Someone else's data / Plan limits)
        │   └── 👉 【403 Forbidden】(存在を隠すなら 404 / 404 to hide existence)
        │
        └─ YES
            └── 👉 【200 OK 等の正常レスポンス / 200 OK or other success responses】
  • 判定の最短ルール:
    • 「ログインし直せば解決するか?」 ➔ YESなら 401
    • 「ログインし直しても権限が変わらない限り無理か?」 ➔ YESなら 403

適切なステータスコードと構造化されたエラーレスポンスを返すことで、フロントエンドエンジニアとの連携がスムーズになり、堅牢で使いやすいAPIを構築することができます。

おわりに

401 Unauthorized と 403 Forbidden は、名前の紛らわしさから混同されがちですが、その役割は「認証(誰か)」と「認可(何ができるか)」ではっきりと分かれています。

  • 401 Unauthorized: 「身元が分からない・無効」➔ トークン更新やログイン画面へ誘導する
  • 403 Forbidden: 「身元は確認できたが権限がない」➔ 権限不足の通知や専用画面を表示する(秘匿したい場合は 404)

ステータスコードを適切に使い分け、明確なエラーレスポンスを返すことは、APIの保守性を高めるだけでなく、フロントエンドの実装負担軽減やセキュリティリスクの低減に直結します。
迷ったときは「もう一度ログインし直して解決するか?」を思い出し、ユーザーとフロントエンド開発者の両方に親切なAPI設計を心がけてみてください。

コメント

タイトルとURLをコピーしました