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 から安全に使えるようにしたものです。流れは次の通りです。
- SPA は最初に
GET /sanctum/csrf-cookieを叩き、XSRF-TOKENクッキーを受け取る - ログインリクエスト(
POST /loginなど)を送る際、その値をX-XSRF-TOKENヘッダに乗せる - 認証が通るとセッションクッキーが発行される
- 以降の 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;
}
確認方法
実装が正しく動いているかは、ブラウザの開発者ツールで順に確認できます。
GET /sanctum/csrf-cookieのレスポンスでSet-Cookie: XSRF-TOKEN=...が返っているか- ログインリクエストのヘッダに
X-XSRF-TOKENが付いているか - ログイン成功後、
Cookieにセッションクッキー(laravel_sessionなど)が保存されているか 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 耐性の面でもシンプルに安全側へ倒せるのが利点です。
