実務でNext.js(SSG)+Laravel8(API)のWebアプリケーションを開発しており、Laravel Sanctum(ララベル サンクタム)を使ったSPA認証を実装することができたのでまとめておきます。
始めに書いておきますが、めちゃくちゃエラーにハマりました…ドキュメント通りに進めても上手くいかないところがあったのでそれも込みで書きます。
Laravel Sanctumについてはこちらで詳しくまとめていますのでよかったらこちらも読んでみてください。

Laravelの学習にオススメの教材はこちらにまとめています。

環境準備
Next.jsとLaravel(API)の開発環境はDockerで構築しているという前提でこの記事を書きます。
ですのでステージング環境や本番環境ではそれぞれ設定する内容は変更する必要があります。
それぞれのアプリケーションの各コンテナのポート番号は以下のように設定しています。(一応URLごと記載)
- Next.js:http://localhost:3001
- Laravel(API):http://localhost:8080
Next.jsからLaravel(API)にはAxiosで通信します。
CORS設定は以下。(完成版ではありません)
<?php
return [
// 略
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['http://localhost:3001'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => false,
];
Laravel Sanctumを導入する
以下2つのコマンドを実行。
# インストール
composer require laravel/sanctum
# 設定ファイル自動作成
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
# 上記コマンドで作成されるファイル
database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php
config/sanctum.php
SPA認証の場合は生成されたマイグレーションファイルは使用しないので削除してOKです。
Laravel SanctumのSPA認証を実装する
ここから本題の実装方法をまとめます。
基本的には以下のドキュメント通りに進めていけば実装ができますが、一部ドキュメント通りに行かなかった(そこでめちゃくちゃハマりましたw)のでその辺りも含めての実装方法です。
- 公式ドキュメント

- 日本語訳
ファーストパーティドメインの設定
APIにリクエストを行うときにLaravelセッションクッキーを使用して「ステートフル」な認証を維持するドメインを指定します。
<?php
return [
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''
))),
// 略
];
.env
に認証状態を保持したいNext.sjアプリのドメインとポート番号を指定します。
SANCTUM_STATEFUL_DOMAINS=localhost:3001
config/sanctum.php
でデフォルトで指定されているドメイン(+ポート番号)(例えばlocalhostとかlocalhost:3000とか)の中に認証状態を維持したいアプリのドメイン+ポート番号が含めれている場合は上記設定は不要ですが、設定しておくのが確実です。
Sanctumミドルウェアの設定
api
ミドルウェアグループにSanctumミドルウェアを追加します。
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
// 略
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
// 略
'api' => [
// この1行を追加
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
// 略
}
これで/api/〜
でルーティングされたAPIに対するリクエストでセッション・Cookieによる自動認証が可能となります。
Sanctumミドルウェアの詳しい仕様はこちらの記事がわかりやすいです。
CORSとCookieの設定
config/cors.php
を以下の通り修正します。
<?php
return [
// 略
// trueに変更
'supports_credentials' => true,
];
レスポンスヘッダの Access-Control-Allow-Credentials
が true
を返すようになります。
false
のままだと、認証処理時にCORSエラーになります。(以下のエラー文がブラウザコンソールに表示されます)
Access to XMLHttpRequest at ‘http://localhost:8080/sanctum/csrf-cookie’ from origin ‘http://localhost:3001’ has been blocked by CORS policy: The value of the ‘Access-Control-Allow-Credentials’ header in the response is ” which must be ‘true’ when the request’s credentials mode is ‘include’. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
次にアプリケーションのグローバルなaxios
インスタンスでwithCredentials
オプションを有効にする必要があります。
LaravelにVue.js、Reactを組み込んだ構成の場合はresources/js/bootstrap.js
に以下を追記します。
(今回はフロントエンドはNext.jsアプリで独立しているので、ログイン画面作成時にNext.js側で設定します)
axios.defaults.withCredentials = true;
アプリケーションのセッションクッキードメインを設定します。
SESSION_DOMAIN=localhost
定義した環境変数はconfig/session.php
に使用されます。
<?php
use Illuminate\Support\Str;
return [
// 略
'domain' => env('SESSION_DOMAIN', null),
// 略
];
これは任意ですが、セッションドライバーをfile
からcookie
に変更します。
SESSION_DRIVER=cookie
僕はデフォルトのfile
のままにしています。(Redis使った方がいいのかな?)
ログイン処理の実装
ログイン処理を実装します。
SPA認証をするには、Next.jsアプリのログインページでログイン処理の前に必ず/sanctum/csrf-cookie
エンドポイントにリクエストを送信して、アプリケーションのCSRF保護を初期化する必要があります。
axios.get('/sanctum/csrf-cookie').then(response => {
// ログイン処理を実装する
});
/sanctum/csrf-cookie
へのルーティングはLaravel Sanctumをインストールした時点で自動的に追加されます。
上記ルーティングは/vendor/laravel/sanctum/src/SanctumServiceProvider.php
によって追加されているため、routes/web.php
、routes/api.php
を探しても見つかりません。
// 略
protected function defineRoutes()
{
// 略
Route::group(['prefix' => config('sanctum.prefix', 'sanctum')], function () {
Route::get(
'/csrf-cookie',
CsrfCookieController::class.'@show'
)->middleware('web');
});
}
確認のため、php artisan route:list
で確認しておきましょう。
+--------+----------+---------------------+------+------------------------------------------------------------+------------------------------------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+----------+---------------------+------+------------------------------------------------------------+------------------------------------------+
| | GET|HEAD | / | | Closure | web |
| | GET|HEAD | api/user | | Closure | api |
| | | | | | App\Http\Middleware\Authenticate:sanctum |
| | GET|HEAD | sanctum/csrf-cookie | | Laravel\Sanctum\Http\Controllers\CsrfCookieController@show | web |
+--------+----------+---------------------+------+------------------------------------------------------------+------------------------------------------+
ログイン処理は/login
ルートにPOSTリクエストする必要があります。
独自の
https://readouble.com/laravel/8.x/ja/sanctum.html/login
エンドポイントを自由に作成できます。ただし、標準のLaravelが提供するセッションベースの認証サービスを使用してユーザーを認証していることを確認する必要があります。通常、これはweb
認証ガードを使用することを意味します。
この記事ではドキュメント通りweb
認証ガードを使用して手動実装していきます。(Laravel Fortifyというパッケージを使用する方法もあるようです)
ということでルーティングの追加、CORS設定の変更を行います。
Route::post('/login', [LoginController::class, 'login']);
Laravel8はこれまでのルーティングの指定方法から変更になっているので少し注意です。
return [
// 略
// 'login'を追加
'paths' => ['api/*', 'login', 'sanctum/csrf-cookie'],
// 略
];
Next.js側で実装するログイン画面は記事用なので超簡易的ですがこのような感じです。
// ログインページ
import axios from 'axios'
import { ChangeEvent, useState } from 'react'
type LoginParams = {
email: string
password: string
}
export default function Login() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const changeEmail = (e: ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value)
}
const changePassword = (e: ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value)
}
const handleClick = () => {
const loginParams: LoginParams = { email, password }
axios
// CSRF保護の初期化
.get('http://localhost:8080/sanctum/csrf-cookie', { withCredentials: true })
.then((response) => {
// ログイン処理
axios
.post(
'http://localhost:8080/login',
loginParams,
{ withCredentials: true }
)
.then((response) => {
console.log(response.data)
})
})
}
// SPA認証済みではないとアクセスできないAPI
const handleUserClick = () => {
axios.get('http://localhost:8080/api/user', { withCredentials: true }).then((response) => {
console.log(response.data)
})
}
return (
<>
<div>
メールアドレス
<input onChange={changeEmail} />
</div>
<div>
パスワード
<input onChange={changePassword} />
</div>
<div>
<button onClick={handleClick}>ログイン</button>
</div>
<div>
<button onClick={handleUserClick}>ユーザー情報を取得</button>
</div>
</>
)
}
ログイン処理を担当するコントローラーはこちらも簡易的に以下の(最低限の)内容にしています。
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
public function login(Request $request): JsonResponse
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => 'required',
]);
if (Auth::attempt($credentials)) {
$request->session()->regenerate();
return response()->json(['name' => Auth::user()->email], 200);
}
throw new Exception('ログインに失敗しました。再度お試しください');
}
}
ここまでの状態でログインを行えば、ログインが成功してブラウザコンソールにログインユーザーのメールアドレスが表示されると思います。
ルートの保護
Laravel Sanctumでの認証が必要なルーティングを設定する場合はsanctum
認証ガードを指定します。
<?php
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\QuestionnaireController;
use App\Http\Controllers\SampleController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
// auth:apiをauth:sanctumに変更
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
受信リクエストがNext.jsアプリからのステートフル認証済みリクエストとして認証されるか、リクエストがサードパーティからのものである場合は有効なAPIトークンヘッダを含むことを保証します。
この修正によりログイン画面(pages/login.tsx)の以下の部分にはSPA認証後でないとリクエストできないようになります。
(認証前だと401エラーになります)
// SPA認証済みではないとアクセスできないAPI
const handleUserClick = () => {
axios.get('http://localhost:8080/api/user', { withCredentials: true }).then((response) => {
console.log(response.data)
})
}
ログイン後であれば、ブラウザコンソールにログインユーザーの情報が表示されます。
ブラウザをリロードしても認証状態が維持されます。
補足
ドキュメント通りに進めてうまくいなかったことがあるのでいくつか書いておきます。
CSRF保護初期化時にSRF-TOKENのCookieが保存されない
/sanctum/csrf-cookie
ルートへのリクエスト後にブラウザにXSRF-TOKENのCookieが保存されなかったことです。
対応方法としてはAxiosの設定に{ withCredentials: true }
を追加することで解決しました。
// CSRF保護の初期化
axios.get('http://localhost:8080/sanctum/csrf-cookie', { withCredentials: true })
上記ルーティングからのレスポンスヘッダーにSet-Cookieヘッダーが含まれていたのですが、{ withCredentials: true }
を設定しないとブラウザにCookieが保存されないようです。以下の記事参考。

ログイン後にsanctum認証ガードを設定したAPIにリクエストできない
ログイン後に/api/userにGETすると、401エラーになりました。
この原因は.envに設定していたSANCTUM_STATEFUL_DOMAINSの値をLaravel側のポート番号にしていたことでした。
SANCTUM_STATEFUL_DOMAINS=localhost:8080
認証状態を維持するアプリケーションはNext.jsなのでLaravelにしたらダメですよね。。初めは上記環境変数の意味を正確に理解できていなかったのでここも結構詰まりました。(なお、localhostのみでも同様にエラーになります)
参考記事


最後に
社内に聞ける人がおらず、悪戦苦闘しまくり時間も溶かしてしまいましたが、なんとか実装できてよかったです…
Laravel7では、SPA認証のログイン処理にweb
認証ガードを使うような記載はないので大抵の記事はログイン処理はroutes/api.php
に定義していますが、Laravel8からはweb
認証ガードを使うように書かれているのでここも意外と手こずりました。
ちなみに、ログイン処理をルーティングをroutes/api.php
に定義して/api/login
にしても問題なく動作しました。
コメント