LaravelでAPIを組んだ後に決めるのは、そのAPIを叩く相手をどう認証するかです。Laravel Sanctumは、この問いに対して性質の異なる2つの答えを1つのパッケージへ同居させています。1つは利用者が自分で発行する個人用アクセストークン、もう1つはトークンを一切使わずCookieセッションで自社SPAを通す経路。仕組みも制約も別物で、混ぜたまま設計するとドメイン構成の段階で行き詰まります。この記事では、install:apiが作るもの、personal_access_tokensの列、SPA認証のドメイン条件、Passportへ切り替える条件までを、2026年8月29日時点の4.3.3のソースと13.x公式ドキュメントで確認しました。
まとめ:Sanctumが解く2つの問題とPassportへ切り替える条件
先に結論を置きます。版に依存する記述は4.3.3のソースと13.x公式ドキュメントで確認しました。
- Sanctumが解くのは別々の2問題です。個人用アクセストークンの発行と、自社SPAをCookieセッションで通す経路を、それぞれ単独でも使えます
- 導入はphp artisan install:apiの1コマンド。テーブルが1枚増え、ルートはauth:sanctumガードで守ります
- トークンはSHA-256でハッシュ化して保存されます。平文を受け取れるのはcreateTokenの戻り値plainTextTokenの一度きりです
- 既定では期限がありません。設定ファイルのexpirationはnullで、期限を切るなら設定値かcreateTokenの第3引数を明示し、sanctum:prune-expiredで掃除します
- SPA認証はトークンを使いません。SPAとAPIが同じトップレベルドメインにある前提で、別ドメイン運用では成立しない点が構成の制約になります
- 第三者のアプリへ認可を出すOAuth2が要件なら、Passportへ切り替えてください。そこが2つの分岐点です
Laravel Sanctumとは:1つのパッケージに同居する2種類の認証方式
公式ドキュメントは冒頭で「Sanctumは2つの別問題を解くために存在する」と明言しています。この前置きを飛ばすと、後の設定が噛み合いません。
APIトークンはDBの1テーブルとAuthorizationヘッダで完結する
1つ目は、OAuthの複雑さを持ち込まずAPIトークンを発行する仕組みです。公式はGitHubの個人用アクセストークンを発想の源に挙げています。アカウント設定の画面に「トークンを発行する」ボタンを置き、利用者が自分の分を何本でも作る、あの形。
保存先はデータベースの1テーブルだけで、受信リクエストはAuthorizationヘッダのトークンで認証されます。この種のトークンは年単位の長い期限を持つのが一般的で、失効は利用者がいつでも手動で行う前提だと公式は述べています。
SPA認証はトークンを使わずCookieセッションで通す仕組み
2つ目は、Laravel製APIと通信する単一ページアプリケーションの認証です。ここでSanctumはトークンをまったく使いません。使うのはLaravelに元からあるCookieベースのセッション認証、つまりwebガードです。
この方式にはCSRF保護とセッション認証の利点が付き、資格情報がXSS経由で漏れる経路も塞げます。SPAがLaravelと同じリポジトリにあっても、Next.jsやNuxtで別リポジトリに作ってあっても構いません。判断の軸はリポジトリではなくドメインの側にあります。SPAを別に立てず、同一アプリ内でセッションのまま画面を組む方式はLaravel Inertiaとは|APIを作らずSPAを組む仕組みと3系の変更点を参照してください。
受信リクエストはCookieを先に見てからBearerトークンを見る
Sanctumが受信リクエストを調べるとき、まず認証Cookieの有無を見ます。Cookieが無ければAuthorizationヘッダの有効なAPIトークンを探しに行く、という順序。Cookie認証を試みるのは、リクエストが自社のSPAフロントエンドから来た場合に限られます。
この2段構えがあるため、同じルートを自社SPAからも第三者クライアントからも叩けます。もちろん片方だけを使う構成も成り立ちます。APIトークンだけ、あるいはSPA認証だけという採り方でも問題ありません。
導入手順:install:apiが作るテーブルとauth:sanctumガードの位置
導入そのものは1コマンドで終わります。むしろ後から効いてくるのは、生成されたテーブルの列と設定ファイルの既定値です。
install:apiコマンドで入るものとガードを付ける場所の関係
実行するのはphp artisan install:apiです。Sanctumが入り、必要なマイグレーションが走り、APIのルートファイルが用意されます。4.3.3が要求するのはPHP 8.2以上で、Laravelは11・12・13系に対応します。ルーティングとJSON整形の組み立て方はLaravel APIの作り方|install:apiとAPIリソース・エラー応答の統一にまとめました。
保護するルートにはsanctumガードを付けます。
use Illuminate\Http\Request;
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
公式が少し変わった助言をしているのはこの先です。api.phpだけでなくweb.php側のルートにもsanctumガードを勧めています。理由は2つ。セッションCookieが無ければトークンへ自動的に切り替わるため実害が無いこと、そして認証済みユーザーに対してtokenCanをいつでも呼べる状態を保てることです。
personal_access_tokensテーブルの列がトークン設計を縛る
マイグレーションが作るテーブルの定義は次のとおりです。
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->text('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
読み取れる制約が2つあります。tokenableがmorphsなので、発行元はUserモデルに限りません。管理者モデルや組織モデルへ直接トークンを持たせる設計も取れます。token列は64文字のuniqueで、SHA-256のhex表現がちょうど収まる幅です。
設定ファイルsanctum.phpで先に決める4つの値と既定の危うさ
設定ファイルで最初に見るべき値は4つです。
| 設定キー | 既定値 | 先に決める観点 |
|---|---|---|
| stateful | localhost等と自APP_URL | SPAを載せるホスト名 |
| guard | web | セッション側のガード名 |
| expiration | null(期限なし) | 期限を切るか否か |
| token_prefix | 空文字 | 漏洩検知の仕組みに乗せるか |
注意が要るのはexpirationです。既定のnullは、失効操作をしない限りトークンが生き続けることを意味します。もう1つのtoken_prefixは、発行するトークンへ固定の接頭辞を付ける設定です。GitHubのシークレットスキャンのような仕組みは接頭辞を手がかりに漏洩を検知するため、公開リポジトリを扱う案件では入れておく価値があります。
APIトークン認証の実装:発行から失効と有効期限までの設計手順
ここからはトークン方式の実装です。公式は先に釘を刺しています。自社の一次パーティSPAをAPIトークンで認証してはいけません。そこはSPA認証機能の担当です。
HasApiTokensとcreateTokenで発行し平文は一度だけ渡す
UserモデルにトレイトLaravel\Sanctum\HasApiTokensを付け、createTokenを呼びます。
Route::post('/tokens/create', function (Request $request) {
$token = $request->user()->createToken($request->token_name);
return ['token' => $token->plainTextToken];
});
createTokenが返すのはNewAccessTokenのインスタンスです。データベースへ入るのはSHA-256でハッシュ化した値で、平文はplainTextTokenプロパティからしか取れません。つまり画面へ表示する機会は発行直後の一度きりで、後から「あのトークンをもう一度表示する」機能は原理的に作れません。
アビリティはミドルウェア2種とtokenCanの両方で検査する
createTokenの第2引数に文字列の配列を渡すと、そのトークンにアビリティが付きます。OAuthのスコープに相当する考え方です。
return $user->createToken('token-name', ['server:update'])->plainTextToken;
検査の入口は2つあります。1つはコード内でのtokenCanとtokenCant。もう1つはルートに付けるミドルウェアで、bootstrap/app.phpへエイリアスを登録して使います。abilitiesは列挙した全てを持つことを求め、abilityは1つでも持てば通します。
Route::get('/orders', function () {
// check-status と place-orders の両方を持つ場合のみ通る
})->middleware(['auth:sanctum', 'abilities:check-status,place-orders']);
自社SPAからの要求ではtokenCanが常にtrueを返す注意点
ここは事故になりやすい仕様です。SPA認証で通った一次パーティのリクエストに対し、tokenCanは常にtrueを返します。トークンが存在しない経路で、検査する対象が無いためです。
したがって、アビリティの検査だけで認可を組むと自社SPA経由の要求は素通りします。公式が示す書き方は、所有者の確認とアビリティの確認を並べる形です。
return $request->user()->id === $server->user_id &&
$request->user()->tokenCan('server:update');
有効期限はexpirationと第3引数、掃除はprune-expiredで
期限の与え方は2段階です。アプリ全体の既定は設定ファイルのexpirationに分単位で書きます。525600と書けば1年。個別に変えたいトークンは、createTokenの第3引数へ期限を渡します。
return $user->createToken(
'token-name', ['*'], now()->plus(weeks: 1)
)->plainTextToken;
期限を設定したら、期限切れレコードの掃除もセットで組んでください。sanctum:prune-expiredコマンドがあり、スケジューラへ登録すれば日次で消せます。–hours=24を付けると「期限切れから24時間以上経過したもの」に限定されるので、切り戻しの余地を残しつつテーブルの肥大を抑えられます。
Schedule::command('sanctum:prune-expired --hours=24')->daily();
失効はレコードの削除で表現します。全件は$user->tokens()->delete()、現在のリクエストで使われた1本は$request->user()->currentAccessToken()->delete()、特定の1本はidを指定して消します。ログアウト処理で現在のトークンだけを消す実装は、モバイルアプリで頻出のパターンです。
SPA認証の実装:ドメイン条件とCSRF Cookieを取る順序の決まり
ここからはトークンを使わない側の話です。設定箇所が複数のファイルへ散るため、順番に潰していきます。
SPA認証は同じトップレベルドメインで動かす前提が構成を強く縛る
最初に構成の制約を確認します。公式は「認証するには、SPAとAPIが同じトップレベルドメインを共有していなければならない」と書いています。サブドメインが違うのは構いません。app.example.comとapi.example.comは成立します。
成立しないのは、SPAをexample-app.comに、APIをexample-api.comに置くような別ドメイン構成です。Cookieの届く範囲がドメインで決まる以上、ここは設定で回避できません。加えて、リクエストにはAccept: application/jsonヘッダと、RefererかOriginのいずれかが必要です。フロント側の環境構築から通しで追う場合はLaravelとReactの連携・使い方ガイドも併せて参照してください。
statefulApiの登録とstateful設定に書くホストの並べ方
次にミドルウェアの登録です。bootstrap/app.phpでstatefulApiを呼ぶと、自社SPAからの受信リクエストがセッションCookieで認証できるようになります。第三者やモバイルからのトークン認証は、そのまま並行して動きます。
->withMiddleware(function (Middleware $middleware): void {
$middleware->statefulApi();
})
どのホストをstatefulとして扱うかは、設定ファイルのstatefulに並べます。既定値は環境変数SANCTUM_STATEFUL_DOMAINSから読み、localhostや127.0.0.1系に加えてSanctum::currentApplicationUrlWithPort()が返すAPP_URL由来のホストが入ります。もう1つ、コメントアウトされた状態でSanctum::currentRequestHost()も置かれており、有効にすると同じホストからの受信を全てstateful扱いにする仕組みです。
127.0.0.1:8000のようにポート付きのURLでアプリへアクセスしている場合は、ホスト名だけでなくポート番号まで含めて書いてください。
ログインの手順は決まっています。SPAのログイン画面はまず/sanctum/csrf-cookieへリクエストを投げ、CSRF保護を初期化します。このときLaravelが返すのが、XSRF-TOKENという名前のCookieです。
axios.get('/sanctum/csrf-cookie').then(response => {
// ここでログイン処理へ進む
});
以降のリクエストでは、このCookieの値をURLデコードしてX-XSRF-TOKENヘッダへ載せます。AxiosやAngularのHttpClientは自動で処理しますが、fetchを直接使う実装では自前でヘッダを組む必要があります。
CSRFの初期化が済んだら、/loginへPOSTします。このルートは自前で書いても構いませんし、画面を持たない認証バックエンドに任せる手もあります。後者を選ぶならLaravel Fortifyとは|画面を持たない認証バックエンドの責務と2FA・パスキーの実装で挙げたルート群がそのまま使えるはずです。自前で書く場合も、必ずwebガードによるセッション認証を通してください。
CORSとセッションCookieのドメイン指定で詰まりやすい箇所
別サブドメインのSPAから認証できない場合、原因はCORSかセッションCookieの設定に絞られます。確認は3か所です。
セッションの保存先と寿命そのものの設計はLaravelのセッション管理|ドライバ選定とCSRF・複数台構成の共有設計にまとめています。
1つ目はCORS設定です。cors.phpは既定でpublishされていないため、php artisan config:publish corsで出してから、supports_credentialsをtrueにします。このヘッダが返らなければ、ブラウザはCookieを送りません。
2つ目はHTTPクライアント側で、axiosならwithCredentialsとwithXSRFTokenの両方を有効にします。3つ目はsession.phpのdomainで、ルートドメインの先頭にドットを付けた形('.domain.com')にすると配下のサブドメインへCookieが届きます。
セッションが失効した状態でリクエストを送ると401か419が返ります。SPA側はこれを捕まえてログイン画面へ戻す実装にしてください。419そのものの切り分け手順はInvalid CSRF tokenの意味と直し方|Laravel 419・Django対応に分けて書いています。
モバイルアプリ認証:端末名を添えて資格情報をトークンへ交換する
モバイルアプリの認証は第三者クライアントとほぼ同じ流れで、違いはトークンの発行方法にあります。
ログイン画面で受け取った資格情報をトークンへ交換するルート設計
メールアドレスとパスワード、そして端末名を受け取り、その場でトークンへ交換するルートを1本用意します。
Route::post('/sanctum/token', 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.'],
]);
}
return $user->createToken($request->device_name)->plainTextToken;
});
device_nameは情報提供の目的で、公式が推奨するのは「利用者が見て分かる名前」です。返ってきた平文トークンは端末側に保存し、以降はBearerトークンとして送ります。ここでもアビリティの指定はできるため、モバイル用トークンの権限をWebより狭める設計も取れます。
端末ごとの失効を利用者自身が実行できる画面を先に用意する理由
端末を紛失したとき、利用者が自分で該当端末のトークンだけを無効化できるかどうかで、事故対応の重さが変わります。トークンには発行時のdevice_nameが残っているので、名前の一覧と「失効」ボタンを並べた画面を用意しておけば足ります。
// 特定の1本だけを失効させる
$user->tokens()->where('id', $tokenId)->delete();
受託開発でSanctumを採る条件と、Passportへ切り替える場面
ここからは案件でどう判断するかです。公式の選択基準は明快なので、それを実務の条件へ翻訳します。
採ってよいのは認証する相手が自社の画面と自社利用者で閉じる案件
Sanctumで足りるのは、認証する相手が自社の管理下に収まっている案件です。自社のWeb画面、自社が配布するモバイルアプリ、そして自社の利用者が個人的にAPIを叩くための個人用トークン。この3つに収まるなら、OAuth2サーバを立てる理由はありません。
逆に言えば、要件定義で確認すべきは「認証させたい相手のリストに、自社が管理していない第三者のアプリが含まれるか」の1点だけです。含まれなければSanctum。スターターキットから始めた案件では認証の入口が既に整っているため、追加で決めるのはAPI側の方式だけになります。キットの選び分けはLaravel Breeze非推奨の理由とスターターキット移行ガイドにまとめました。
Passportへ切り替えるのは第三者アプリへ認可を出す要件のとき
公式は「アプリケーションがOAuth2を絶対に必要とするならPassportを使うべきだ」と書き、続けて「SPA・モバイルアプリの認証やAPIトークンの発行が目的ならSanctumを使うべきで、SanctumはOAuth2に対応しない」と述べています。判断はこの一線で決まります。
実務で境界に立つのは、外部のSaaSやパートナー企業のシステムから自社APIを叩かせる案件です。相手のアプリが自社の利用者へ「このアプリにあなたのデータへのアクセスを許可しますか」と同意を求める画面を出すなら、それはOAuth2の認可コードフローそのもので、Passportの領域。導入はphp artisan install:api –passportで、OAuth2クライアントとアクセストークンのテーブルに加え、トークン署名用の暗号化キーが生成されます。
一方、パートナーのサーバが自社の資格情報だけでAPIを叩く、いわゆるサーバ間連携であれば、Sanctumの個人用トークンにアビリティを付ける形で十分こと足ります。同意画面が要るかどうかを聞けば、その場で切り分けられます。外部システムとの接続方式そのものを設計から相談したい場合は、API開発・システム連携で扱っている領域です。
SPA認証を見送ってトークン方式へ寄せる判断が要る2つの構成
Sanctumを採ると決めた後にもう1つ分岐があります。SPA認証を使うか、自社SPAに対してもトークン方式で通すか。公式は前者を推していますが、構成上そちらを選べない場合が2つあります。
1つ目は、フロントとAPIのドメインが別で、統一する権限が案件に無いケース。ブランドごとに独立ドメインを持つ運用や、フロントだけを外部のホスティングへ置く構成が該当します。2つ目は、同じフロントコードをWebとモバイルの両方へ配る構成で、ここは経路をトークンに寄せたほうが分岐が減ります。
逆に、ドメインを揃えられる新規案件でSPA認証を見送る理由はほとんどありません。この判断は初期の構成設計でしか安く済まないので、ドメイン設計の打ち合わせに必ず持ち込んでください。
よくある質問
SanctumとPassportはどちらを選べばよいですか?
第三者のアプリへ認可を出すOAuth2が要件に入るならPassportで、それ以外はSanctumです。公式もこの基準を明示しています。SPA・モバイル・個人用APIトークンの3用途に収まるなら、OAuth2サーバの運用コストを負う理由はありません。
SPA認証とAPIトークン認証は同時に使えますか?
同時に使えます。Sanctumは受信リクエストのCookieを先に確認し、無ければAuthorizationヘッダのトークンを見る順序で動くためです。同じルートを自社SPAと外部クライアントの双方へ開く構成も、auth:sanctumガード1つで賄えます。
Sanctumのトークンに有効期限は既定でありますか?
既定ではありません。設定ファイルのexpirationはnullで、明示的に失効させるまでトークンは生き続けます。期限を設けるなら分単位でexpirationへ書くか、createTokenの第3引数で個別に指定してください。期限切れレコードの掃除にはsanctum:prune-expiredを使います。
SPAのログインで419が返るのはなぜですか?
多くはCSRFトークンの受け渡しが成立していない状態です。/sanctum/csrf-cookieを先に呼んでいるか、XSRF-TOKEN CookieをURLデコードしてX-XSRF-TOKENヘッダへ載せているか、session.phpのdomain指定でCookieがフロント側へ届いているかを順に確認します。セッション失効でも419は返るため、その場合はログイン画面へ戻す実装が要ります。
SanctumとFortifyの役割はどう違いますか?
Sanctumは「認証された状態をどう保持し、どう検証するか」を担い、Fortifyは「ログイン・登録・パスワードリセットといった入口の処理」を担います。役割が重ならないため併用が前提で、SPA認証で叩く/loginルートをFortifyに用意させる組み合わせは公式ドキュメントにも登場します。
関連記事
- Laravel APIの作り方|install:apiとAPIリソース・エラー応答の統一:認証を載せる土台の作り方です
- Laravel Fortifyとは|画面を持たない認証バックエンドの責務と2FA・パスキーの実装:ログイン処理側の担当です
- LaravelとReactの連携・使い方ガイド:SPA側の構成手順です
- Invalid CSRF tokenの意味と直し方|Laravel 419・Django対応:419の切り分け手順です