【PHP】Laravel SanctumでAPIトークン認証を実装する方法

対象バージョン: Laravel 13.x / PHP 8.3+ / Laravel Sanctum 4.x

Laravel で作った JSON API を、外部のクライアントやモバイルアプリから安全に呼べるようにするには、リクエストごとに「誰が呼んでいるか」を確かめる仕組みが必要です。Laravel Sanctum は、ユーザーごとに API トークンを発行し、Authorization ヘッダーで送られたトークンを検証する公式パッケージです。OAuth のような認可サーバーを用意しなくても、トークンの発行・権限(abilities)・失効・有効期限までを扱えます。ここでは Laravel 13.x の新規プロジェクトを使い、トークン発行から保護ルート、テストまでを順に組み立てます。

この記事で分かること

  • php artisan install:api で Sanctum を導入する手順
  • HasApiTokens と createToken によるトークン発行
  • auth:sanctum でルートを保護し、curl で動作を確かめる方法
  • abilities(トークンごとの権限)と abilities / ability ミドルウェア
  • トークンの失効、有効期限、期限切れトークンの削除
  • Sanctum::actingAs を使ったテスト
  • API トークン方式と SPA 認証のどちらを選ぶか

対象バージョンと前提

本記事の前提は Laravel 13.x / PHP 8.3+ です。Laravel 13 は 2026年3月17日リリースの現行メジャーで、サポート表上 PHP 8.3〜8.5 を対象とします(Release Notes)。動作確認は laravel/framework v13.34.0、laravel/sanctum v4.3.3、PHP 8.4、SQLite の新規プロジェクトで行いました。

API ルートの基本(routes/api.php、/api 接頭辞、JSON レスポンス)は LaravelでRESTful APIを作成する方法 を前提にします。セッションを使ったログインや Gate・Policy による認可は Laravelで認証と認可を実装する方法 で扱っています。


Sanctum の2つの認証方式

Sanctum には、目的の異なる2つの機能があります(Laravel Sanctum)。

方式 仕組み 主な用途
API トークン DB に保存したトークンを Authorization: Bearer ヘッダーで検証 外部サービス、モバイルアプリ、CLI など
SPA 認証 トークンを使わず、Laravel のセッション Cookie で認証 同じトップレベルドメインで動く自社の SPA

公式ドキュメントは、自社(ファーストパーティ)の SPA の認証に API トークンを使うべきではないとしています。自社 SPA には SPA 認証を使います。どちらか一方だけを使っても構いません。本記事では API トークン方式を扱い、SPA 認証は最後に選び方だけを紹介します。


Sanctum を導入する

新規プロジェクトでは API ルートが初期状態で用意されていないため、install:api を実行します。

php artisan install:api

このコマンドは Sanctum をインストールし、routes/api.php を作成し、トークン保存用の personal_access_tokens テーブルのマイグレーションを追加します。実行中にマイグレーションも走り、最後に「User モデルへ HasApiTokens トレイトを追加してください」という案内が表示されます。

案内どおり、app/Models/User.php に Laravel\Sanctum\HasApiTokens を追加します。

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    // 既存の属性やキャストはそのまま残します
}

HasApiTokens を追加すると、User モデルで createToken メソッドや、発行済みトークンを取得する tokens リレーションが使えるようになります。


トークンを発行するエンドポイント

公式ドキュメントのモバイルアプリ向けの例にならい、メールアドレス・パスワード・デバイス名を受け取ってトークンを返すルートを routes/api.php に作ります。

<?php

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Route;
use Illuminate\Validation\ValidationException;

Route::post('/tokens', function (Request $request) {
    $request->validate([
        'email' => 'required|email',
        'password' => 'required',
        'device_name' => 'required',
    ]);

    $user = User::where('email', $request->email)->first();

    if (! $user || ! Hash::check($request->password, $user->password)) {
        throw ValidationException::withMessages([
            'email' => ['The provided credentials are incorrect.'],
        ]);
    }

    $token = $user->createToken(
        $request->device_name,
        ['orders:read'],
        now()->plus(days: 30),
    );

    return response()->json(['token' => $token->plainTextToken], 201);
});

createToken の引数は、順にトークン名、abilities の配列、有効期限です。第2引数と第3引数は省略でき、省略した場合の abilities は全権限を表す ['*'] になります。device_name は利用者が見分けるためのラベルで、「仕事用ノートPC」のように分かりやすい値にしておくと、あとで失効させるときに役立ちます。

認証情報が誤っている場合は、ValidationException によって 422 の JSON が返ります。

plainTextToken は発行直後にしか取り出せない

createToken は Laravel\Sanctum\NewAccessToken を返し、その plainTextToken プロパティが利用者に渡すトークン文字列です。DB の personal_access_tokens テーブルには、トークンを SHA-256 でハッシュ化した値だけが保存されます。平文はあとから DB で確認できないため、発行直後に利用者へ表示し、クライアント側で保存してもらいます。

返されるトークンは 1|xxxxxxxx... のように、| の前にトークンの ID が付いた形式です。


ルートを auth:sanctum で保護する

保護したいルートには auth:sanctum ミドルウェアを付けます。install:api が作った /user ルートも、このミドルウェア付きで定義されています。

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

routes/api.php のルートには /api 接頭辞が付くため、URL は /api/user です。$request->user() で、トークンの持ち主のユーザーを取得できます。

curl で確かめる

開発サーバー(php artisan serve)を起動し、まずトークンを発行します。ユーザーは事前に作成しておきます(例: php artisan tinker で User::factory()->create(['email' => 'test@example.com', 'password' => 'password']))。

curl -X POST http://127.0.0.1:8000/api/tokens \
  -H "Accept: application/json" \
  -d email=test@example.com \
  -d password=password \
  -d device_name=cli

返ってきたトークンを Authorization: Bearer ヘッダーに付けて、保護ルートを呼びます。

curl http://127.0.0.1:8000/api/user \
  -H "Accept: application/json" \
  -H "Authorization: Bearer 1|xxxxxxxx..."

正しいトークンならユーザー情報の JSON が 200 で返ります。トークンが無い、または無効な場合は、次の JSON が 401 で返ります。

{"message":"Unauthenticated."}

Accept: application/json を付け忘れたとき

Accept: application/json を付けずに未認証で保護ルートを呼ぶと、Laravel は未認証のユーザーをログイン画面へリダイレクトしようとします。API 専用のアプリで login という名前のルートが無い場合、401 ではなく Route [login] not defined. というエラー(500)になります。

API クライアントからは、常に Accept: application/json を送るようにしておくと、401 の JSON が返り、クライアント側でも扱いやすくなります。


abilities でトークンの権限を絞る

abilities は、トークンごとに「何をしてよいか」を表す文字列です。OAuth のスコープに近い役割を持ちます。先ほどの発行例では、読み取り用の orders:read だけを付けました。

tokenCan / tokenCant で確認する

認証済みのリクエストでは、tokenCan と tokenCant でトークンの abilities を確認できます。

if ($request->user()->tokenCan('orders:read')) {
    // 読み取りを許可するときの処理
}

if ($request->user()->tokenCant('orders:write')) {
    abort(403);
}

ミドルウェアでルート単位に制限する

ルート単位で制限したい場合は、Sanctum の2つのミドルウェアに別名を付けて使います。Laravel 13.x では bootstrap/app.php で登録します。

use Illuminate\Foundation\Configuration\Middleware;
use Laravel\Sanctum\Http\Middleware\CheckAbilities;
use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->alias([
        'abilities' => CheckAbilities::class,
        'ability' => CheckForAnyAbility::class,
    ]);
})

abilities は列挙したすべての権限を、ability は列挙したうちどれか1つを要求します。どちらも auth:sanctum と組み合わせて使います。

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/orders', function () {
        return ['orders' => []];
    })->middleware('abilities:orders:read');

    Route::post('/orders', function () {
        return response()->json(['created' => true], 201);
    })->middleware('abilities:orders:write');
});

orders:read だけを持つトークンで POST /api/orders を呼ぶと、{"message":"Invalid ability provided."} が 403 で返ります。GET /api/orders は 200 です。

abilities と Policy の役割の違い

abilities が表すのは「このトークンに何を許したか」です。「このユーザーがこのデータを操作してよいか」は別の問題で、Gate や Policy で判断します。公式ドキュメントでも、トークンの権限とデータの持ち主の両方を確かめる例を示しています。

return $request->user()->id === $server->user_id &&
       $request->user()->tokenCan('server:update');

なお、Sanctum の SPA 認証で自社 SPA から来たリクエストでは、tokenCan は常に true を返します。そのため、ユーザー単位の認可は Policy 側で行う設計にしておくと、API トークンと SPA の両方で同じ判定を使えます。


トークンを失効させる

トークンの失効は、DB からトークンを削除することで行います。HasApiTokens の tokens リレーションを使います。

// ユーザーのすべてのトークンを失効させる
$user->tokens()->delete();

// 現在のリクエストに使われたトークンを失効させる
$request->user()->currentAccessToken()->delete();

// 特定のトークンを失効させる
$user->tokens()->where('id', $tokenId)->delete();

たとえば、ログアウト用のエンドポイントは次のように書けます。

Route::middleware('auth:sanctum')->delete('/tokens/current', function (Request $request) {
    $request->user()->currentAccessToken()->delete();

    return response()->noContent();
});

このエンドポイントは 204 を返し、以後同じトークンで保護ルートを呼ぶと 401 になります。


有効期限と期限切れトークンの削除

Sanctum のトークンは、既定では期限切れになりません。config/sanctum.php の expiration は初期値が null で、失効させない限り使い続けられます。

すべてのトークンに有効期限を設ける場合は、expiration に分単位の値を設定します。

'expiration' => 525600, // 365日(分単位)

トークンごとに期限を変えたい場合は、createToken の第3引数に日時を渡します。前述の発行例では now()->plus(days: 30) で30日後を指定しました。期限を過ぎたトークンで保護ルートを呼ぶと 401 になります。

期限切れのトークンは自動では削除されず、DB に残ります。Sanctum には削除用の sanctum:prune-expired コマンドがあるので、スケジューラーで定期実行します。次の例は、期限切れから24時間以上たったトークンを毎日削除します。

use Illuminate\Support\Facades\Schedule;

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

この定義は routes/console.php に書けます。スケジューラーの動かし方は Laravelでタスクスケジューリングを実装する方法 を参照してください。


テストで認証済みユーザーを用意する

テストでは、Sanctum::actingAs でユーザーとトークンの abilities を指定できます。実際にトークンを発行しなくても、保護ルートの動作を確かめられます。

<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;

class OrderApiTest extends TestCase
{
    use RefreshDatabase;

    public function test_orders_can_be_listed_with_read_ability(): void
    {
        Sanctum::actingAs(User::factory()->create(), ['orders:read']);

        $this->getJson('/api/orders')->assertOk();
    }

    public function test_orders_cannot_be_created_without_write_ability(): void
    {
        Sanctum::actingAs(User::factory()->create(), ['orders:read']);

        $this->postJson('/api/orders')->assertForbidden();
    }

    public function test_guest_receives_401(): void
    {
        $this->getJson('/api/user')->assertUnauthorized();
    }
}

すべての権限を持たせたい場合は、abilities に ['*'] を渡します。getJson / postJson は Accept: application/json 付きでリクエストするため、未認証時も 401 の JSON で判定できます。テストの基本は Laravelでテストを実装する方法 で扱っています。


API トークンと SPA 認証の選び方

状況 選ぶ方式
外部サービスや他社のクライアントに API を提供する API トークン
モバイルアプリや CLI から呼ぶ API トークン
同じトップレベルドメインで動く自社の SPA から呼ぶ SPA 認証

SPA 認証ではトークンを使わず、Laravel のセッション Cookie で認証します。CSRF 保護が効き、XSS による認証情報の漏えいも防ぎやすくなります。Laravel 13.x では bootstrap/app.php で $middleware->statefulApi(); を呼び、config/sanctum.php の stateful に SPA のドメインを設定します。SPA と API は同じトップレベルドメインに置く必要があり(サブドメインは可)、CORS と Cookie の設定、/sanctum/csrf-cookie による CSRF 保護の初期化も必要です。手順は公式の SPA Authentication を確認してください。


よくある失敗

  • Accept: application/json を送っていない: 未認証時にログイン画面へのリダイレクトが試みられ、login ルートが無いと 500 になります。
  • HasApiTokens を User モデルに追加していない: createToken を呼べません。install:api の最後の案内を見落としがちです。
  • 平文のトークンをあとから DB で探す: DB にはハッシュしか保存されません。発行時に返した plainTextToken を使います。
  • 期限切れトークンが DB にたまる: 有効期限を設けたら、sanctum:prune-expired を定期実行します。
  • 自社 SPA に API トークンを使う: 公式は SPA 認証を推奨しています。

まとめ

  • php artisan install:api で Sanctum と routes/api.php を用意し、User モデルに HasApiTokens を追加します。
  • createToken でトークンを発行し、plainTextToken は発行時に利用者へ渡します。
  • 保護したいルートには auth:sanctum を付け、クライアントは Authorization: Bearer と Accept: application/json を送ります。
  • abilities でトークンの権限を絞り、ユーザー単位の認可は Policy で判断します。
  • 失効は tokens() や currentAccessToken() の delete、有効期限は expiration か createToken の第3引数で設定し、sanctum:prune-expired で掃除します。
  • テストでは Sanctum::actingAs で認証済みの状態を作れます。

参考リンク

コメントする