Laravel SanctumでSPA認証を実装する基礎

概要

Laravelでフロントエンドを React / Vue などの SPA として分離すると、最初に悩むのが「認証をどう持たせるか」です。Laravel Sanctum はこの用途のために用意された軽量な認証パッケージで、同一トップレベルドメイン上の SPA に対してはクッキーベースのセッション認証を、モバイルアプリや外部連携にはトークン認証を、同じパッケージで提供します。

この記事では、SPA 向けのクッキーベース認証(いわゆる「SPA認証」)を、動作原理から実装・確認方法まで入門者向けに整理します。

環境

  • PHP 8.2 以上
  • Laravel 11.x(Sanctum は同梱)
  • フロントエンドは同一トップレベルドメイン上で動作する SPA(例: app.example.com と api.example.com)
  • 開発時は localhost:5173(Vite)と localhost:8000(Laravel)を想定

バージョンによってインストール手順やデフォルトのミドルウェア構成が異なります。本記事は Laravel 11 系を前提にしています。

SPA認証の仕組みを先に理解する

Sanctum の SPA 認証は、新しい独自プロトコルではありません。Laravel の標準的なセッション認証(クッキー + CSRF)を、SPA から安全に使えるようにしたものです。流れは次の通りです。

  1. SPA は最初に GET /sanctum/csrf-cookie を叩き、XSRF-TOKEN クッキーを受け取る
  2. ログインリクエスト(POST /login など)を送る際、その値を X-XSRF-TOKEN ヘッダに乗せる
  3. 認証が通るとセッションクッキーが発行される
  4. 以降の API リクエストはクッキーで認証される

ポイントは、トークンを自前で保存・付与しないことです。JWT を localStorage に置く方式と違い、クッキーは HttpOnly にできるため、XSS でトークンを抜かれるリスクを抑えられます。CSRF 対策はダブルサブミットクッキー方式で担保します。

インストール

Laravel 11 では Sanctum を API 用にインストールするコマンドが用意されています。

php artisan install:api

これで config/sanctum.php、マイグレーション、関連設定が追加されます。手動で追加する場合は次の通りです。

composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

発生しがちな問題

SPA 認証でよくあるつまずきは、だいたい次の3つに集約されます。

  • ログインは成功するのに、次のリクエストで 401 Unauthenticated になる
  • 419 CSRF token mismatch が返る
  • クッキーがブラウザに保存されない

いずれも「設定の組み合わせ」が原因で、一つでも欠けると認証が成立しません。順に潰していきます。

原因と対応方法

1. ステートフルドメインの登録

Sanctum は「どのドメインからのリクエストをセッション認証(ステートフル)として扱うか」をホワイトリストで管理します。ここに SPA のホストが入っていないと、クッキーがあってもトークン認証扱いになり、セッションが効きません。

SANCTUM_STATEFUL_DOMAINS=localhost:5173,app.example.com
SESSION_DOMAIN=.example.com

localhost と localhost:5173 は別物として扱われるため、ポート番号まで一致させます。

2. セッション・CORSの設定

SPA が別オリジンから叩くため、クッキーを跨いでやり取りできるよう CORS を設定します。config/cors.php で supports_credentials を true にするのが肝心です。

// config/cors.php
return [
    'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout'],
    'allowed_methods' => ['*'],
    'allowed_origins' => ['http://localhost:5173', 'https://app.example.com'],
    'allowed_headers' => ['*'],
    'supports_credentials' => true, // これが無いとクッキーが送受信されない
];

allowed_origins にワイルドカード * は使えません(supports_credentials = true と併用不可のため)。オリジンは明示します。

3. フロントエンド側の設定

fetch / axios でクレデンシャル(クッキー)を送る設定が必須です。axios の例です。

import axios from 'axios';

const api = axios.create({
  baseURL: 'https://api.example.com',
  withCredentials: true,          // クッキーを送受信する
  withXSRFToken: true,            // axios 1.x 以降は明示が必要な場合あり
});

export default api;

実装例

ルート定義

// routes/web.php — ログイン/ログアウトはセッションを扱うため web ミドルウェア側に置く
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

Route::post('/login', function (Request $request) {
    $credentials = $request->validate([
        'email' => ['required', 'email'],
        'password' => ['required'],
    ]);

    if (! Auth::attempt($credentials, $request->boolean('remember'))) {
        return response()->json(['message' => '認証に失敗しました'], 422);
    }

    $request->session()->regenerate(); // セッション固定攻撃対策
    return response()->json(['message' => 'ok']);
});

Route::post('/logout', function (Request $request) {
    Auth::guard('web')->logout();
    $request->session()->invalidate();
    $request->session()->regenerateToken();
    return response()->json(['message' => 'ok']);
});
// routes/api.php — 認証が必要な API は auth:sanctum で保護する
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', fn (Request $request) => $request->user());
});

フロントエンドのログインフロー

import api from './api';

async function login(email, password) {
  // 1. CSRFクッキーを取得
  await api.get('/sanctum/csrf-cookie');

  // 2. ログイン(X-XSRF-TOKENはaxiosが自動付与)
  await api.post('/login', { email, password });

  // 3. 認証済みエンドポイントを叩けるようになる
  const { data } = await api.get('/user');
  return data;
}

確認方法

実装が正しく動いているかは、ブラウザの開発者ツールで順に確認できます。

  1. GET /sanctum/csrf-cookie のレスポンスで Set-Cookie: XSRF-TOKEN=... が返っているか
  2. ログインリクエストのヘッダに X-XSRF-TOKEN が付いているか
  3. ログイン成功後、Cookie にセッションクッキー(laravel_session など)が保存されているか
  4. GET /user が 200 でユーザー情報を返すか

注意点

  • SANCTUM_STATEFUL_DOMAINS と SESSION_DOMAIN の整合が最重要。ポート番号まで一致させる
  • 本番は必ず HTTPS 前提。SESSION_SECURE_COOKIE=true を設定し、SameSite 属性も確認する
  • SPA が API と完全に別ドメイン(トップレベルドメインが異なる)の場合、クッキー方式は使いづらい。その場合はトークン認証(createToken)を検討する
  • supports_credentials と allowed_origins: ['*'] は併用できない

まとめ

Sanctum の SPA 認証は「新しい仕組み」ではなく、Laravel のセッション認証を SPA から安全に使うための薄い層です。つまずきの大半は次の3点に集約されます。

  • ステートフルドメインの登録(ポートまで一致)
  • CORS の supports_credentials: true
  • フロント側の withCredentials: true

この3つを押さえれば、401 / 419 のほとんどは解消します。トークンを自前で持たずクッキーに任せられるため、XSS 耐性の面でもシンプルに安全側へ倒せるのが利点です。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。