モダンなWeb開発において、LaravelをバックエンドAPIとして使用し、フロントエンドにNext.js、Nuxt.js、React、Vue.jsを採用する「ヘッドレス構成」や「SPA(シングルページアプリケーション)構成」、さらにはiOS/Androidモバイルアプリとの連携が標準的なアーキテクチャとなっています。
このような構成で最も重要かつ設計に迷いやすいのが「認証(Authentication)」の実装です。
Laravelには、SPAおよびAPI向けの軽量で強力な認証パッケージとして Laravel Sanctum(サンクタム) が用意されています。しかし、Sanctumには以下の2つの異なる認証方式が備わっており、それぞれの違いや正しい設定方法を理解していないと、CSRFエラーやCORSエラーなどのトラブルに悩まされがちです。
- SPA認証(Cookie・セッションベース):同一ドメインまたはサブドメインで動くReact/Vue/Next.jsなどのSPA向け。CookieとCSRF保護を活用し、XSS攻撃に強いセキュアな認証を実現。
- APIトークン認証(Bearerトークンベース):iOS/Android等のモバイルアプリ、外部サードパーティ連携向け。シンプルなステートレスな暗号化文字列トークン(Bearer)を発行・検証。
この記事では、Laravel Sanctumのインストールから、SPA認証(Cookieベース)とAPIトークン認証(Bearer)の双方の完全な構築手順、フロントエンド通信例、権限管理(Abilities)、そして現場で頻発するエラーのトラブルシューティングまで、余すところなく徹底解説します!
1. Sanctumの2つの認証方式の違いと選び方
実装に入る前に、まず「SPA認証」と「APIトークン認証」の仕組みの違いと使い分けを整理しておきましょう。
| 比較項目 | SPA認証(Cookieベース) | APIトークン認証(Bearer) |
|---|---|---|
| 主な対象 | Webブラウザ(Next.js / Vue / React SPA) | モバイルアプリ(iOS/Android)、外部APIクライアント |
| 認証情報の保持 | HttpOnly なセッションCookie |
Authorization: Bearer <token> ヘッダー |
| ステート性 | ステートフル(Laravelのセッションを使用) | ステートレス(DBテーブルでトークン照合) |
| CSRF保護 | 必須(/sanctum/csrf-cookie で初期化) |
不要(ヘッダーで送信するためCSRF耐性あり) |
| セキュリティ(XSS) | JavaScriptからCookieが読めないため安全 | 端末側のセキュアストレージ(Keychain等)に保存 |
| ドメイン要件 | 同一トップレベルドメイン(サブドメイン共有必須) | クロスオリジン・ドメインの制約なし |
Webブラウザの
localStorage や sessionStorage にBearerトークンを平文保存すると、クロスサイトスクリプティング(XSS)脆弱性が発生した際に悪意あるJavaScriptによってトークンを容易に盗まれてしまいます。そのため、Webブラウザ向けのSPAでは JavaScriptからアクセスできない
HttpOnly Cookie を使用するSanctumのSPA認証がベストプラクティス とされています。
2. Laravel Sanctumのインストールと初期セットアップ
まずはLaravelプロジェクトにSanctumを導入し、共通の初期セットアップを行います。
Step 1: パッケージのインストール(Laravel 11 / 12)
Laravel 11以降では、API機能とSanctumをワンコマンドでセットアップできる php artisan install:api コマンドが用意されています。
# Laravel 11以降の場合(推奨)
php artisan install:api
このコマンドを実行すると、以下の処理が自動で行われます:
laravel/sanctumパッケージのインストールroutes/api.phpの作成(存在しない場合)bootstrap/app.phpへのAPIルーティング・ミドルウェア登録personal_access_tokensマイグレーションの実行
※Laravel 10以前、または手動でインストールする場合は以下の手順を実行します:
# Composerでパッケージをインストール
composer require laravel/sanctum
# 設定ファイルとマイグレーションの公開
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
# マイグレーションの実行
php artisan migrate
Step 2: Userモデルに HasApiTokens トレイトを追加
ユーザーモデルでトークン発行機能や認証判定を利用できるよう、App\Models\User クラスに HasApiTokens トレイト(Trait)を付与します。
// app/Models/User.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens; // ← 追加
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable; // ← HasApiTokens を追加
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}
3. 【方式1】SPA認証(Cookie・セッションベース)の構築手順
Next.js、Nuxt.js、Vue、Reactなどのフロントエンドと連携する「CookieベースのSPA認証」を構築します。
Step 1: バックエンド設定(Laravel側)
1. bootstrap/app.php の設定(Laravel 11 / 12)
Laravel 11以降では、bootstrap/app.php 内で $middleware->statefulApi() を呼び出すことで、SPAからのCookieベース認証を有効化します。
// bootstrap/app.php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
// SanctumのSPAステートフルミドルウェアを有効化
$middleware->statefulApi();
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
※Laravel 10以前の場合は、app/Http/Kernel.php の $middlewareGroups['api'] に \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class を追加します。
2. .env と config/sanctum.php の設定
SPAが動作するフロントエンドのオリジン(ドメインおよびポート)を、ステートフルドメインとして登録します。
# .env
APP_URL=http://localhost:8000
FRONTEND_URL=http://localhost:3000
# セッションCookieを共有するドメイン(同一サブドメイン運用時は .example.com のように指定)
SESSION_DOMAIN=localhost
# SPAがアクセスしてくるドメインとポート(カンマ区切りで複数指定可能)
SANCTUM_STATEFUL_DOMAINS=localhost:3000,127.0.0.1:3000
3. CORS設定(config/cors.php)
フロントエンドからのリクエストでCookieをやり取りできるように、CORS設定を行います。
// config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout'],
'allowed_methods' => ['*'],
'allowed_origins' => [
env('FRONTEND_URL', 'http://localhost:3000'),
],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
// Cookie送信を許可するために必ず true に設定
'supports_credentials' => true,
];
Step 2: 認証エンドポイントの実装
SPA認証では、Web用のセッション認証ルート(routes/web.php)を使用するのが標準的です。
// routes/web.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use Illuminate\Validation\ValidationException;
// ログイン
Route::post('/login', function (Request $request) {
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (! Auth::attempt($credentials, $request->boolean('remember'))) {
throw ValidationException::withMessages([
'email' => ['メールアドレスまたはパスワードが正しくありません。'],
]);
}
$request->session()->regenerate();
return response()->json(['message' => 'ログインに成功しました', 'user' => Auth::user()]);
});
// ログアウト
Route::post('/logout', function (Request $request) {
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return response()->json(['message' => 'ログアウトしました']);
});
// 認証済みユーザー情報取得(routes/api.php)
// routes/api.php
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Step 3: フロントエンド(Next.js / Axios)での通信フロー
SPA認証を行う場合、フロントエンド側は以下の4つのステップで通信します。
GET /sanctum/csrf-cookieを呼び出して CSRF Cookie(XSRF-TOKEN)をブラウザにセットPOST /loginで認証情報を送信してログイン(セッションCookieが発行される)- 以降の
/api/*リクエストはCookieが自動送信され、auth:sanctumで認証される POST /logoutでログアウトし、セッションを破棄
Axiosを使用したフロントエンド実装例(TypeScript / React / Next.js)は以下のようになります:
// lib/axios.ts
import axios from 'axios';
const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json',
},
// Cookieをクロスオリジンリクエストで送信するために必須
withCredentials: true,
withXSRFToken: true, // Axios v1.6+ でCSRFトークンを自動ヘッダー付与
});
export default apiClient;
// services/authService.ts
import apiClient from '@/lib/axios';
// 1. CSRFクッキー初期化 & ログイン
export async function login(credentials: { email: string; password: string }) {
// まずCSRF Cookieを取得してブラウザにセット
await apiClient.get('/sanctum/csrf-cookie');
// ログインリクエスト
const response = await apiClient.post('/login', credentials);
return response.data;
}
// 2. ログイン中ユーザーの取得
export async function getAuthUser() {
const response = await apiClient.get('/api/user');
return response.data;
}
// 3. ログアウト
export async function logout() {
await apiClient.post('/logout');
}
4. 【方式2】APIトークン認証(Bearerトークン)の実装手順
続いて、iOS/Androidアプリやサードパーティ向けに、ステートレスな Bearerトークン を発行・検証する実装方法を解説します。
Step 1: トークン発行・失効コントローラーの作成
php artisan make:controller Api/AuthController を実行し、トークン管理コントローラーを作成します。
// app/Http/Controllers/Api/AuthController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
/**
* APIトークンを発行(ログイン)
*/
public function issueToken(Request $request)
{
$request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
'device_name' => ['required', 'string', 'max:255'],
]);
$user = User::where('email', $request->email)->first();
// 認証判定
if (! $user || ! Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['認証情報が一致しません。'],
]);
}
// 権限(Abilities)を指定してトークンを生成
// $user->createToken('端末名', ['権限リスト'])
$token = $user->createToken($request->device_name, ['post:create', 'post:read'])->plainTextToken;
return response()->json([
'token_type' => 'Bearer',
'access_token' => $token,
'user' => [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
],
]);
}
/**
* 現在使用中のトークンを破棄(ログアウト)
*/
public function revokeCurrentToken(Request $request)
{
// リクエストで使用されたAccessTokenを削除
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'トークンを無効化しました']);
}
/**
* ユーザーのすべてのトークンを一括破棄
*/
public function revokeAllTokens(Request $request)
{
// 全端末のトークンを一括削除
$request->user()->tokens()->delete();
return response()->json(['message' => 'すべての端末からログアウトしました']);
}
}
Step 2: APIルートの定義(routes/api.php)
トークン発行用の公開エンドポイントと、auth:sanctum で保護されたエンドポイントを定義します。
// routes/api.php
use App\Http\Controllers\Api\AuthController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
// 公開ルート(トークン発行)
Route::post('/tokens/create', [AuthController::class, 'issueToken']);
// 認証必須ルート(Bearerトークンが必要)
Route::middleware('auth:sanctum')->group(function () {
// ユーザー情報取得
Route::get('/user', function (Request $request) {
return $request->user();
});
// 現在のトークン失効(ログアウト)
Route::post('/tokens/revoke', [AuthController::class, 'revokeCurrentToken']);
// 全トークン失効
Route::post('/tokens/revoke-all', [AuthController::class, 'revokeAllTokens']);
});
Step 3: トークンの権限(Abilities)制御
Sanctumでは、トークンごとに細かな権限(Abilities)を割り当て、APIエンドポイントごとにアクセス制御を行うことができます。
1. 権限ミドルウェアの利用
Laravel 11以降では、以下のミドルウェアエイリアスを使ってルートを保護できます:
ability:scope1,scope2:指定したいずれかの権限を持っているか検証(OR条件)abilities:scope1,scope2:指定したすべての権限を持っているか検証(AND条件)
// routes/api.php
use App\Http\Controllers\Api\PostController;
Route::middleware(['auth:sanctum', 'abilities:post:create'])->group(function () {
Route::post('/posts', [PostController::class, 'store']);
});
Route::middleware(['auth:sanctum', 'ability:post:read,post:create'])->group(function () {
Route::get('/posts', [PostController::class, 'index']);
});
2. コントローラー内での権限判定
// コントローラー内で動的に権限判定を行う場合
if ($request->user()->tokenCan('post:delete')) {
// 削除権限がある場合の処理
}
Step 4: APIリクエストの実行例
1. トークン発行リクエスト(cURL)
curl -X POST http://localhost:8000/api/tokens/create \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"email": "user@example.com", "password": "password123", "device_name": "iPhone 15"}'
レスポンス例:
{
"token_type": "Bearer",
"access_token": "1|qX7JmO...9kL2p",
"user": {
"id": 1,
"name": "山田 太郎",
"email": "user@example.com"
}
}
2. 認証が必要なAPIへのリクエスト
取得したトークンを Authorization: Bearer <access_token> ヘッダーにセットしてリクエストします。
curl -X GET http://localhost:8000/api/user \
-H "Accept: application/json" \
-H "Authorization: Bearer 1|qX7JmO...9kL2p"
5. トークンの有効期限とセキュリティ設定
デフォルトではSanctumのAPIトークンに有効期限はありませんが、セキュリティ要件に応じて config/sanctum.php で有効期限(分単位)を設定できます。
// config/sanctum.php
return [
// トークンの有効期限(分単位)。例: 1440 = 24時間, null = 無期限
'expiration' => 1440,
// ...
];
有効期限が切れた古いトークンを定期的にデータベースから削除するには、以下のArtisanコマンドをスケジューラーに登録します。
# 期限切れトークンの削除コマンド
php artisan sanctum:prune-expired
6. よくあるトラブルと解決策
Sanctumの導入時に開発者が直面しやすい代表的なエラーと対処法をまとめました。
① 419 CSRF Token Mismatch / Page Expired エラー
SPA認証で最も多いトラブルです。以下の3点を確認してください:
GET /sanctum/csrf-cookieを先に呼んでいるか:ログインAPIを叩く前に、必ずCSRF Cookie初期化エンドポイントを呼び出す必要があります。SANCTUM_STATEFUL_DOMAINSにポート番号が含まれているか:開発環境でlocalhost:3000を使用している場合、ポート番号の記述が必須です。- Axiosの
withCredentials: true設定:Cookieがバックエンドに送られていないと419エラーになります。
詳しい解説と診断フローは、Laravel「419 Page Expired」エラーの原因と解決策 をご参照ください。
② CORSエラー(Access to XMLHttpRequest has been blocked)
config/cors.phpのsupports_credentialsがtrueになっているか確認。allowed_originsにフロントエンドの正確なURL(プロトコルhttp:///https://を含む)が記述されているか確認。allowed_originsに*(ワイルドカード)を指定しながらsupports_credentials: trueにすることは仕様上禁止されています。
③ 401 Unauthorized(認証されない)
- APIトークン認証の場合:ヘッダーのスペルが
Authorization: Bearer <token>と正しく指定されているか確認。 - SPA認証の場合:ブラウザのDevToolsで
laravel_sessionCookieおよびXSRF-TOKENが送信されているか確認。 - Webサーバー(Apache/Nginx)が
Authorizationヘッダーを破棄していないか確認(SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1等の設定)。
まとめ:適切な認証方式を選んで堅牢なAPIを構築しよう
Laravel Sanctumは、SPAとモバイルアプリの両方を1つのシンプルなパッケージで保護できる非常に優れた認証ソリューションです。
- Web SPA(React/Vue/Next.js):
HttpOnlyCookieを用いた SPA認証 を採用し、XSSリスクを排除。 - モバイル・外部API:ステートレスな Bearer APIトークン を発行し、Abilitiesで柔軟に権限管理。
- Laravel 11/12対応:
php artisan install:apiとbootstrap/app.phpの$middleware->statefulApi()でスマートに設定。 - トラブル予防:
SANCTUM_STATEFUL_DOMAINS、SESSION_DOMAIN、withCredentialsの3大設定を確実に揃える。
Laravel 11で刷新されたディレクトリ構造や bootstrap/app.php の詳細設定については、Laravel 11の変更点総まとめ|スリム化されたディレクトリ構造・Kernel廃止とbootstrap/app.phpの設定・移行ポイントを徹底解説 をご覧ください。
他の認証パッケージ(Laravel Breeze、Jetstream、Fortify、Passport)との違いや選び方については、Laravel認証の違いを徹底比較!Breeze・Sanctum・Jetstream・Fortifyの選び方とおすすめ でも詳しく解説しています。あわせてチェックして、プロジェクトに最適な認証基盤を構築してください!

コメント