Laravel『UnauthorizedHttpException』の原因と対処法

Laravel『UnauthorizedHttpException』の原因と対処法

Laravelで出る「Unauthorized HTTP Exception」は、主に Symfony の例外(Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException)として投げられ、認証情報が不足している・形式が違う・トークンが無効・ガード設定がズレている、といった理由で「認証できない」状態を示すことが多い。発生箇所は、API認証(Bearerトークン、JWT、Sanctum/Passport)、Basic認証、独自ミドルウェア、外部サービスの署名検証などさまざま。ポイントは「401(未認証)なのか」「403(認可拒否)なのか」を分け、どの認証方式・どのガードで失敗しているかを特定すること。

エラーの出方と発生条件(典型例)

よくある形。

Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
Unauthenticated.
Invalid token.
The token has been blacklisted.

発生条件の典型:
・Authorizationヘッダ(Bearer)が付いていない/空
・トークンの形式が違う(Bearer ではなく別形式)
・期限切れ・署名不正・失効済みのトークン
・APIなのにwebガードで認証しようとしている(guard/middlewareのズレ)
・SanctumでSPA認証のつもりなのに、ステートレスAPIとして叩いている(Cookie/CSRF不足)
・プロキシ/ロードバランサでAuthorizationヘッダが落ちている
・例外的に「認可」ではなく「認証」で落ちているのに403扱いにしている

まず切り分け:401(未認証)と403(認可拒否)

UnauthorizedHttpException は基本的に「認証できない」側(401)で出ることが多い。
・401:ログインしていない/トークンが無い/無効
・403:ログインはできているが権限がない(AuthorizationExceptionなどが多い)
APIで「権限がない」と「ログインしていない」を混ぜると調査が長引くので、例外の型・HTTPステータスを揃えるのが重要。

原因1:Authorizationヘッダ(Bearer)が付いていない/落ちている

クライアント側の付け忘れが最頻。

# NG(Authorizationが無い)
curl -X GET https://example.com/api/me

# OK(Bearer付き)
curl -H "Authorization: Bearer YOUR_TOKEN" https://example.com/api/me

プロキシ配下でヘッダが落ちるケースもある(Nginx/ApacheでAuthorizationをPHPに渡していない等)。
Laravel側で「受け取れているか」をログで確認すると早い。

logger()->info('auth header', [
  'authorization' => request()->header('Authorization'),
]);

原因2:ガード設定のズレ(web/api/sanctum等を取り違えている)

同じルートでも、どのガードで認証しているかで必要な情報が変わる。
典型:
・APIを叩いているのに auth:web が付いている
・Sanctumのつもりが auth:api(別ドライバ)になっている
ルートで明示しているミドルウェアを確認する。

// routes/api.php(例)
Route::middleware('auth:sanctum')->get('/me', function () {
    return auth()->user();
});

Guardが合っていない場合は、意図する方式に合わせて middleware を揃える。

// 例:トークンAPIなら sanctum / passport / jwt など設計に合わせる
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/me', ...);
});

原因3:トークンが無効(期限切れ・署名不正・失効)

JWTやPassport等では、期限切れ・署名検証失敗・ブラックリスト入りで UnauthorizedHttpException が出ることがある。
発生条件:
・トークンのexp切れ
・秘密鍵/署名キーの不一致(環境差・ローテーション)
・ログアウト等で失効済み
対処の方向性:
・トークンを再発行する導線(リフレッシュトークン/再ログイン)
・環境ごとのキー管理を統一(.env差、config:cache差)
・時刻ズレ(NTP不整合)も疑う(特にコンテナ/VM)

原因4:SanctumのSPA認証でCookie/CSRFが送れていない

Sanctumを「SPA(Cookie)認証」として使う場合、単純なBearerだけではなく、同一ドメイン/サブドメイン、CORS、credentials、CSRF取得などが成立していないと未認証になる。
発生条件の典型:
・別ドメインから叩いているのに credentials を付けていない
・CSRF Cookieを取得していない(/sanctum/csrf-cookie)
・SESSION_DOMAIN や SANCTUM_STATEFUL_DOMAINS の設定不備
フロントからCookie送信する例(概念)。

// fetch例(Cookieを送る)
await fetch('https://api.example.com/sanctum/csrf-cookie', {
  credentials: 'include',
});

const res = await fetch('https://api.example.com/api/me', {
  credentials: 'include',
  headers: { 'X-Requested-With': 'XMLHttpRequest' },
});

「Sanctumでトークン方式(personal access token)」を使う場合は Bearer を付ける運用になるため、SPA方式と混在しないよう整理する。

原因5:例外の投げ方が“未認証”になっている(独自ミドルウェア/署名検証)

Webhookや社内APIで、署名(HMAC)検証や独自トークン検証に失敗したときに UnauthorizedHttpException を投げているケース。
発生条件:
・署名ヘッダが無い/不一致
・リプレイ対策のtimestampが古い
Laravel側の例(概念)。

use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

public function handle($request, \Closure $next)
{
    $sig = $request->header('X-Signature');
    if (!$sig || $sig !== hash_hmac('sha256', $request->getContent(), config('app.webhook_secret'))) {
        throw new UnauthorizedHttpException('hmac', 'Invalid signature');
    }
    return $next($request);
}

この場合は「何が足りないと落ちるか」をAPI仕様として固定し、ヘッダ名・計算式・エンコーディングを揃えるのが重要。

サンプル:APIで401をJSONで返す(未認証時の応答を統一)

API利用だとHTMLリダイレクトよりJSONが欲しいケースが多い。

// app/Exceptions/Handler.php(例)
use Illuminate\Auth\AuthenticationException;

protected function unauthenticated($request, AuthenticationException $exception)
{
    if ($request->expectsJson()) {
        return response()->json([
            'message' => 'Unauthenticated',
        ], 401);
    }

    return redirect()->guest(route('login'));
}

UnauthorizedHttpException(Symfony)を独自に投げている場合も、APIとしての応答形式を揃えると保守が楽になる。

チェックリスト(上から順に確認する)

1) 401(未認証)か403(認可)かを確認したか(例外型とHTTPステータス)
2) Authorization: Bearer がリクエストに付いているか(プロキシ配下で落ちていないか)
3) どのガード/ミドルウェアで認証しているか(auth:web / auth:api / auth:sanctum など)
4) トークンの期限切れ・署名キー不一致・失効がないか(環境差・時刻ズレも含む)
5) SanctumのSPA方式なら Cookie/CSRF/credentials/CORS/設定(SESSION_DOMAIN, STATEFUL_DOMAINS)を満たしているか
6) 独自認証(署名検証等)なら、必要ヘッダと計算方式がクライアントと一致しているか