# Laravel Sanctum 使って API トークン認証と SPA 認証

Laravel は Auth 認証システムとして、passport と sanctum を提供しています。Laravel Passport は OAuth2 認証ベースで、Laravel Sanctum は API トークンベースの軽量な認証システムになります。
SPA やモバイルアプリケーションを認証したり、API トークンを発行したりする場合は、Laravel Sanctum を使うので、資料まとめました。

Laravel Sanctum とは

Laravel Sanctum(サンクタム、聖所)は Laravel が提供する軽量な認証システムで、SPA やモバイルアプリなどで API とやり取りするための認証を提供します。API トークンを DB に保存し、有効な API トークンを含む必要がある Authorization ヘッダを介して受信 HTTP リクエストを認証する機能を提供します。Laravel Blade で開発するなら Laravel Passport がありますが、Laravel を API サーバーとして使う場合の認証システムは Laravel Sanctum になります。

# API トークン機能と SPA 認証機能

Laravel Sanctum は API トークン機能と SPA 認証機能という2つの機能があります。

API トークン機能は API トークンを Authorization ヘッダを介して受信 HTTP リクエストを認証します。各ユーザーが複数の API トークンを生成でき、これらのトークンには特定の機能やスコープを付与することが可能です。
SPA 認証機能はトークンの代わりにクッキーベースのセッション認証サービスを使用します。WEB 認証ガードを利用して、CSRF 保護、セッション認証の利点が提供できるだけでなく、XSS を介した認証資格情報の漏洩を保護します。

Laravel Sanctum は、受信リクエストが自身の SPA フロントエンドから発信された場合にのみクッキーを使用して認証を試みます。Sanctum が受信 HTTP リクエストを調べるとき、最初に認証クッキーをチェックし、存在しない場合は、Sanctum は有効な API トークンの Authorization ヘッダを調べます。

# インストール

パッケージインストール

composer require laravel/sanctum

# Sanctum の設定ファイルが config ディレクトリの下に外だし

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

# データベースのマイグレーションを実行

Sanctum は API トークンを格納するための 1 つのデータベーステーブルを作成

php artisan migrate

Sanctum のデフォルトマイグレーションを使用しない場合は、App\Providers\AppServiceProvider クラスの register メソッドで Sanctum::ignoreMigrations メソッドを呼び出す必要があります。

次のコマンドを実行して、デフォルトマイグレーションをエクスポートできます。

php artisan vendor:publish --tag=sanctum-migrations

# ミドルウエアグループ編集

SPA 認証を使用する場合、app/Http/Kernel.php(Laravel 11 以前)または bootstrap/app.php(Laravel 11 以降)にある api ミドルウエアグループを編集します。

# Laravel 11 以前の場合

app/Http/Kernel.php

'api' => [
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    'throttle:api',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],

# Laravel 11 以降の場合

bootstrap/app.php

->withMiddleware(function (Middleware $middleware) {
    $middleware->statefulApi();
})

# API トークン認証

SPA シングルページアプリケーションでの開発なら、API トークン認証より SPA 認証機能がおすすめです。

# トークンの発行

Laravel\Sanctum\HasApiTokens トレイトを使用して API トークン発行

use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
}
use Illuminate\Http\Request;

Route::post('/tokens/create', function (Request $request) {
    $token = $request->user()->createToken($request->token_name);

    return ['token' => $token->plainTextToken];
});

トークンにスコープ(権限)を付与する場合:

$token = $request->user()->createToken(
    $request->token_name,
    ['server:update', 'server:delete'] // スコープを指定
);

# ルートに Middleware 設定

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

API トークンを使用する場合、リクエストヘッダーに以下のように設定します:

Authorization: Bearer {トークン}

# トークン削除

// 全トークンの削除
$user->tokens()->delete();

// 現在のリクエストの認証に使用されたトークンを取り消す
$request->user()->currentAccessToken()->delete();

// 特定のトークンを取り消す
$user->tokens()->where('id', $tokenId)->delete();

# トークンの有効期限

デフォルトでは、Sanctum トークンに有効期限がなく、トークンの取り消しによってのみ無効化されます。

sanctum の設定ファイルで有効期限設定できます。

'expiration' => 60,

有効期限過ぎたトークンを一括削除する

コマンドで 24 時間以上有効期限過ぎたものを一括削除できます。

php artisan sanctum:prune-expired --hours=24

スケジュール設定で 24 時間以上有効期限過ぎたものを一括削除できます。

$schedule->command('sanctum:prune-expired --hours=24')->daily();

# SPA 認証

セッションベースの認証サービスを使用するため、web ガードを使います。つまり、login ルートは api ではなく web ルートにおいた方がいいです。

# CORS 設定

異なるサブドメインからアクセスできるように CORS 設定する必要があります。

config/cors.php

'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,

# フロント側 axios withCredentials を true に設定

const api = axios.create({
  baseURL: "http://localhost",
  withCredentials: true,
});

# セッションクッキードメイン設定

ここで設定した値は、Set-Cookie の domain 属性に設定されます。
config/session.php

// 'domain' => '.domain.com',
'domain' => env('SESSION_DOMAIN'),

# CSRF 保護

SPA を認証ログインする前には、最初に/sanctum/csrf-cookieにリクエストして、CSRF 保護を初期化する必要があります。

このリクエスト中に、Laravel は現在の CSRF トークンを含む XSRF-TOKEN クッキーをセットします。このトークンは、後続のリクエストへ X-XSRF-TOKEN ヘッダで渡す必要があります。これは、Axios や Angular HttpClient などの一部の HTTP クライアントライブラリでは自動的に行います。JavaScript HTTP ライブラリで値が設定されていない場合は、このルートで設定された XSRF-TOKEN クッキーの値と一致するように X-XSRF-TOKEN ヘッダを手作業で設定する必要があります。

# ログイン処理

フロント側

// login
const login = async () => {
  try {
    // CSRF クッキーを取得
    await api.get("/sanctum/csrf-cookie");

    // ログインリクエスト
    const response = await api.post("/login", {
      email,
      password,
    });

    console.log(response.data);
  } catch (error) {
    console.error("ログインエラー:", error);
  }
};

// logout
const logout = async () => {
  try {
    await api.post("/logout");
    console.log("ログアウト成功");
  } catch (error) {
    console.error("ログアウトエラー:", error);
  }
};

/sanctum/csrf-cookie ルートにリクエストを送信しているため、JavaScript HTTP クライアントが XSRF-TOKEN クッキーの値を X-XSRF-TOKEN ヘッダで送信する限り、後続のリクエストは自動的に CSRF 保護を受けます。
axios がwithCredentials: true と設定する場合、XSRF-TOKEN をリクエスト時に送信してくれます。

ログイン Controller

LoginController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;

class LoginController extends Controller
{
    /**
     * ログイン処理
     *
     * @param  Request  $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function login(Request $request)
    {
        $credentials = $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required'],
        ]);

        if (Auth::attempt($credentials, $request->boolean('remember'))) {
            $request->session()->regenerate();

            return response()->json([
                'user' => Auth::user(),
                'message' => 'ログインに成功しました'
            ]);
        }

        throw ValidationException::withMessages([
            'email' => ['認証情報が正しくありません。'],
        ]);
    }

    /**
     * ログアウト処理
     *
     * @param  Request  $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout(Request $request)
    {
        Auth::logout();

        $request->session()->invalidate();

        $request->session()->regenerateToken();

        return response()->json([
            'message' => 'ログアウトしました'
        ]);
    }
}

# Postman で試す Sanctum SPA 認証

# Cookies 設定

Cookies > Manage Cookies > Domains Allowlist のメニューから設定できます。

フロントエンドのドメインを追加します。ローカルなので、localhost になります。

# Login の仕組み

  1. まず CSRF クッキーを取得するリクエストを作成します。

    • GET http://localhost/sanctum/csrf-cookie

    Pre-request Script を設定:

pm.sendRequest(
  {
    url: "http://localhost/sanctum/csrf-cookie",
    method: "GET",
  },
  function (error, response, { cookies }) {
    if (!error) {
      pm.environment.set("xsrf-token", cookies.get("XSRF-TOKEN"));
    }
  }
);
  1. ログインリクエストを作成します。

    • POST http://localhost/login

    Body (raw JSON):

{
  "email": "test@example.com",
  "password": "password"
}

Headers:

{
  "X-XSRF-TOKEN": "{{xsrf-token}}",
  "Accept": "application/json",
  "Content-Type": "application/json"
}

X-XSRF-TOKEN: NaN を Key と Value として設定します。

# ユーザー情報取得

ログインして CSRF 初期化と session を作成済み状態でユーザー情報取得できます。

  • GET http://localhost/api/user

Headers

{
  "Accept": "application/json",
  "Referer": "http://localhost"
}

# まとめ

Laravel Sanctum は以下の用途に適しています:

  • SPA 認証: 同一ドメインまたはサブドメイン内で動作する SPA に対してセッションベースの認証を提供
  • API トークン認証: モバイルアプリケーションやサードパーティサービス向けの API トークン認証
  • 軽量な認証: Passport よりもシンプルで軽量な認証システムが必要な場合

注意点として、Sanctum の SPA 認証は同一トップレベルドメイン内のサブドメイン間での使用が前提となっています。異なるドメイン間での認証には、API トークンベースの認証や OAuth/OIDC の利用を検討する必要があります。

2022-11-28

同じタグを持つ記事をピックアップしました。