Laravel Sanctum セットアップしてみる

### 概要

next.js・nginx・laravel・mysql環境で開発環境を立ち上げた際、初めてLaravel Sanctumを使ってみることにした。その際の手順を記します。

### 環境

```json
        "php": "^8.2",
        "laravel/framework": "^12.0",
        "laravel/sanctum": "^4.0",
```

## Sanctum の認証方式

Sanctum には **「SPA認証（クッキー方式）」** と **「Token認証」** の2つの方法がある。

| 認証方式 | 説明 | 使用する機能 |
| --- | --- | --- |
| **SPA認証（クッキー方式）** | フロントエンドとAPIが同じオリジン（`localhost:3000` → `localhost:8000` など）で動く場合に最適。ブラウザのセッションを利用する | `sanctum/csrf-cookie` |
| **Token認証** | APIをモバイルアプリや外部サービスから使う場合に適している。ユーザーごとに発行したアクセストークンを使って認証する | `createToken()` |

**今回の実装では、Token 認証を採用。**

理由は、今後勉強のため外部APIサービスを導入する可能性があるから。

例えば、**Stripe などの決済API、他のバックエンドサービスとの連携、モバイルアプリからのAPI利用** を想定すると、Token認証の方が適しているため、今回はこの方式を選択した。

### Sanctum のセットアップ手順

1. **Sanctum をインストール**
    
    ```bash
    composer require laravel/sanctum
    ```
    
2. **Sanctum の公開**
    
    ```bash
    php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
    ```
    
    Sanctum の設定ファイルを **`config/sanctum.php` にコピー** する処理
    
3. **マイグレーションの実行**
    
    ```bash
    php artisan migrate
    ```
    
4. **`App\Models\User` に `HasApiTokens` トレイトを追加**
    
    ```php
    use Laravel\Sanctum\HasApiTokens;
    
    class User extends Authenticatable
    {
        use HasApiTokens, HasFactory, Notifiable;
    }
    ```
    
5. Sanctum のクッキー認証を有効にするための CORS 設定
    
    /config/cors.php
    
    ```php
    <?php
    
    return [
        'paths' => ['api/*'], 
        'allowed_methods' => ['*'],
        'allowed_origins' => ['http://localhost:3000'],
        'allowed_origins_patterns' => [],
        'allowed_headers' => ['*'],
        'exposed_headers' => [],
        'max_age' => 0,
        'supports_credentials' => false,
    ];
    ```
    
    この設定ファイルは
    
    **CORS（Cross-Origin Resource Sharing）**
    
    のルールを管理するもの。
    
    フロントエンド（Next.js）とバックエンド（Laravel API）が
    
    **異なるオリジン（ドメイン）**
    
    で動作するとき、APIリクエストを許可するかどうかを決める。
    
    ## **🔥 各設定の意味**
    
    | 設定キー | 役割 | 設定値 | 説明 |
    | --- | --- | --- | --- |
    | `'paths' => ['api/*']` | **CORSを適用するAPIのパス** | `['api/*']` | `api/` 以下のすべてのAPIリクエストにCORSを適用する |
    | `'allowed_methods' => ['*']` | **許可するHTTPメソッド** | `['*']` | `GET`, `POST`, `PUT`, `DELETE` などすべてのリクエストを許可 |
    | `'allowed_origins' => ['http://localhost:3000']` | **許可するオリジン（アクセス元のドメイン）** | `['http://localhost:3000']` | Next.js（フロントエンド）の開発サーバーからのリクエストを許可 |
    | `'allowed_origins_patterns' => []` | **正規表現で許可するオリジン** | `[]` | 特定のパターンを許可したい場合に使う（今回は未使用） |
    | `'allowed_headers' => ['*']` | **許可するリクエストヘッダー** | `['*']` | すべてのヘッダー（`Authorization`, `Content-Type` など）を許可 |
    | `'exposed_headers' => []` | **クライアントがアクセスできるレスポンスヘッダー** | `[]` | フロントエンド側で見えるレスポンスヘッダー（今回は未使用） |
    | `'max_age' => 0` | **プリフライトリクエストのキャッシュ時間** | `0` | `0` にすると毎回プリフライトリクエストを送る |
    | `'supports_credentials' => false` | **クッキーを含めたリクエストを許可するか？** | `false` | Token認証ではクッキーを使わないため `false` |
6. 仮でAPI設定
    
    /config/auth.php
    
    ```powershell
        'guards' => [
            'web' => [
                'driver' => 'session',
                'provider' => 'users',
            ],
            'sanctum' => [
                'driver' => 'sanctum',
                'provider' => 'users',
            ],    
        ],
    ```
    
    bootstrap/app.php
    
    ```php
    <?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', // ✅ これを追加して `routes/api.php` を有効化！
            commands: __DIR__.'/../routes/console.php',
            health: '/up',
        )
        ->withMiddleware(function (Middleware $middleware) {
            //
        })
        ->withExceptions(function (Exceptions $exceptions) {
            //
        })->create();
    
    ```
    
    Laravel 11 から `routes/api.php` がデフォルトでなくなったので、新規作成する必要がある
    
    **routes/api.phpに仮でAPIを設定**
    
    ```php
    <?php
    
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Route;
    use App\Http\Controllers\AuthController;
    
    Route::post('/login', [AuthController::class, 'login']);
    Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum');
    
    Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
        return $request->user();
    });
    
    ```
    
    app/Http/Controllers/AuthController.php
    
    ```php
    <?php
    
    namespace App\Http\Controllers;
    
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Auth;
    use App\Models\User;
    
    class AuthController extends Controller
    {
        public function login(Request $request)
        {
            $credentials = $request->validate([
                'email' => 'required|email',
                'password' => 'required'
            ]);
    
            if (!Auth::attempt($credentials)) {
                return response()->json(['message' => 'Unauthorized'], 401);
            }
    
            $user = Auth::user();
            $token = $user->createToken('auth-token')->plainTextToken;
    
            return response()->json(['token' => $token, 'user' => $user]);
        }
    
        public function logout(Request $request)
        {
            $request->user()->tokens()->delete();
            return response()->json(['message' => 'Logged out']);
        }
    }
    ```
    
7. テストしてみる
    
    適当にテストユーザーを作成
    
    database/seeders/DatabaseSeeder.php
    
    ```php
    <?php
    
    namespace Database\Seeders;
    
    use App\Models\User;
    // use Illuminate\Database\Console\Seeds\WithoutModelEvents;
    use Illuminate\Database\Seeder;
    use Illuminate\Support\Facades\Hash;
    
    class DatabaseSeeder extends Seeder
    {
        /**
         * Seed the application's database.
         */
        public function run(): void
        {
            // User::factory(10)->create();
    
            User::factory()->create([
                'name' => 'Test User',
                'email' => 'test@example.com',
                'password' => Hash::make('password'),
            ]);
        }
    }
    
    ```
    
    ```powershell
    php artisan db:seed
    ```
    
    tinkerで確認
    
    ```powershell
    Psy Shell v0.12.7 (PHP 8.3.17 — cli) by Justin Hileman
    > $user = \App\Models\User::first();
    $user->createToken('test-token')->plainTextToken;
    = "*********************"
    ```
    
    トークン作成はOK。
    
    リクエストしてみる
    
    ```powershell
    curl -X POST http://localhost:8000/api/login \
         -H "Content-Type: application/json" \
         -d '{"email": "test@example.com", "password": "password"}'
    
    ```
    
    レスポンス
    
    ```powershell
    {
    	"token": "*********************************",
    	"user": {
    		"id": 1,
    		"name": "Test User",
    		"email": "test@example.com",
    		"email_verified_at": "2025-03-08T05:42:05.000000Z",
    		"created_at": "2025-03-08T05:42:05.000000Z",
    		"updated_at": "2025-03-08T05:42:05.000000Z"
    	}
    }
    ```
    
    認証成功！
    
    認証確認
    
    ```powershell
    % curl -X GET http://localhost:8000/api/user \
         -H "Authorization: Bearer {token}"
    ```
    
    結果
    
    ```powershell
    {
    	"id": 1,
    	"name": "Test User",
    	"email": "test@example.com",
    	"email_verified_at": "2025-03-08T05:42:05.000000Z",
    	"created_at": "2025-03-08T05:42:05.000000Z",
    	"updated_at": "2025-03-08T05:42:05.000000Z"
    }
    ```
    
    OK！