Laravel Sanctum完全攻略|SPA認証(Cookie)とAPIトークン認証(Bearer)の実装手順を徹底解説

Laravel入門基本文法・構文ガイド実装・応用テクニック運用・保守・セキュリティ

モダンなWeb開発において、LaravelをバックエンドAPIとして使用し、フロントエンドにNext.js、Nuxt.js、React、Vue.jsを採用する「ヘッドレス構成」や「SPA(シングルページアプリケーション)構成」、さらにはiOS/Androidモバイルアプリとの連携が標準的なアーキテクチャとなっています。

このような構成で最も重要かつ設計に迷いやすいのが「認証(Authentication)」の実装です。

Laravelには、SPAおよびAPI向けの軽量で強力な認証パッケージとして Laravel Sanctum(サンクタム) が用意されています。しかし、Sanctumには以下の2つの異なる認証方式が備わっており、それぞれの違いや正しい設定方法を理解していないと、CSRFエラーやCORSエラーなどのトラブルに悩まされがちです。

🔑 Laravel Sanctumが提供する2つの認証方式:

  1. SPA認証(Cookie・セッションベース):同一ドメインまたはサブドメインで動くReact/Vue/Next.jsなどのSPA向け。CookieとCSRF保護を活用し、XSS攻撃に強いセキュアな認証を実現。
  2. 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 SPAではBearerトークンをLocalStorageに保存してはいけないのか?

Webブラウザの localStoragesessionStorage に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つのステップで通信します。

🔄 SPA認証の通信シーケンス:

  1. GET /sanctum/csrf-cookie を呼び出して CSRF Cookie(XSRF-TOKEN)をブラウザにセット
  2. POST /login で認証情報を送信してログイン(セッションCookieが発行される)
  3. 以降の /api/* リクエストはCookieが自動送信され、auth:sanctum で認証される
  4. 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.phpsupports_credentialstrue になっているか確認。
  • allowed_origins にフロントエンドの正確なURL(プロトコル http:// / https:// を含む)が記述されているか確認。
  • allowed_origins*(ワイルドカード)を指定しながら supports_credentials: true にすることは仕様上禁止されています。

③ 401 Unauthorized(認証されない)

  • APIトークン認証の場合:ヘッダーのスペルが Authorization: Bearer <token> と正しく指定されているか確認。
  • SPA認証の場合:ブラウザのDevToolsで laravel_session Cookieおよび XSRF-TOKEN が送信されているか確認。
  • Webサーバー(Apache/Nginx)が Authorization ヘッダーを破棄していないか確認(SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 等の設定)。

まとめ:適切な認証方式を選んで堅牢なAPIを構築しよう

Laravel Sanctumは、SPAとモバイルアプリの両方を1つのシンプルなパッケージで保護できる非常に優れた認証ソリューションです。

📌 今回のポイントまとめ:

  • Web SPA(React/Vue/Next.js)HttpOnly Cookieを用いた SPA認証 を採用し、XSSリスクを排除。
  • モバイル・外部API:ステートレスな Bearer APIトークン を発行し、Abilitiesで柔軟に権限管理。
  • Laravel 11/12対応php artisan install:apibootstrap/app.php$middleware->statefulApi() でスマートに設定。
  • トラブル予防SANCTUM_STATEFUL_DOMAINSSESSION_DOMAINwithCredentials の3大設定を確実に揃える。

Laravel 11で刷新されたディレクトリ構造や bootstrap/app.php の詳細設定については、Laravel 11の変更点総まとめ|スリム化されたディレクトリ構造・Kernel廃止とbootstrap/app.phpの設定・移行ポイントを徹底解説 をご覧ください。

他の認証パッケージ(Laravel Breeze、Jetstream、Fortify、Passport)との違いや選び方については、Laravel認証の違いを徹底比較!Breeze・Sanctum・Jetstream・Fortifyの選び方とおすすめ でも詳しく解説しています。あわせてチェックして、プロジェクトに最適な認証基盤を構築してください!

レン (Wren)

こんにちは。レンです。

Laravelのコードの森に住んでいる、小さな案内役です。
ルーティングの枝やクラスの影を歩きながら、コードの流れや仕組みを眺めています。

このサイトでは、Laravelの基本から実装のコツまで、開発で役立つポイントを静かに整理しています。
難しいことを増やすのではなく、コードの見通しが少し良くなるヒントを届けるのが役目です。

「この処理はどこに書くのがいいのか」
「Laravelではどう考えると整理できるのか」

そんな疑問に、小さなメモを残すような気持ちで記事を書いています。

コードを書いている途中で迷ったとき、
このサイトが少し立ち止まって整理できる場所になればうれしいです。

レン (Wren)をフォローする

コメント