Laravel Sanctum(サンクタム、聖所)は、SPA(シングルページアプリケーション)、モバイルアプリケーション、およびシンプルなトークンベースのAPIの軽量認証システムです。
環境
- https://github.com/ucan-lab/docker-laravel
- PHP: 8.1.5
- Laravel: 9.9.0
- Laravel Sanctum: 2.15.1
- Laravel8.6以降は標準搭載
※Laravel8以前はLaravel Sanctumが標準インストールされているので、補足を追記しています。
公式系
- https://readouble.com/laravel/9.x/ja/sanctum.html
- https://github.com/laravel/sanctum
- https://laravel.com/docs/9.x/sanctum
- https://laravel-news.com/tag/sanctum
関連記事
Laravel Sanctumの仕組み
SPA認証(クッキー認証)
- SPA(Single Page Application)等のJavaScript APIとの認証に使用する
- HasApiTokensトレイト、personal_access_tokensテーブルは不要
- VueやReact等のシングルページアプリケーション(SPA)を認証する
- Laravel標準のクッキーベースのセッション認証サービスを使用する
- Laravelの
web
認証ガードを利用する - CSRF保護、セッション認証の利点、XSSを介した認証資格情報の漏洩を保護する
- 最初にクッキー認証をチェックし、存在しなければAPIトークン認証を試みます
- SPAとAPIが同じトップレベルドメインを共有している必要がある(サブドメインが異なるのはOK)
- CORS設定により、サーバー間のリクエストが制約を受けることがある
-
HttpOnly
属性を付けることでクッキーへのJavaScriptアクセスを防止(XSS対策)
APIトークン認証(今回は実装しません)
- 主にスマホアプリのネイティブアプリや外部システムとの認証に使用する
- HasApiTokensトレイト、personal_access_tokensテーブルは必要
- ユーザーにAPIトークンを発行する
- GitHubの個人アクセストークンのようなイメージ
- Sanctumを使用してトークンを生成および管理ができる
- トークンは通常、非常に長い有効期限(年)を設定する
- ユーザーはいつでも手動でトークンを取り消すことができる
- ユーザーAPIトークンを単一のデータベーステーブルに保存する
-
Authorization
ヘッダを介してAPIトークンを認証する - VueやReact等のSPA(Single Page Application)ではセキュリティ観点から利用しない
SPA認証においてはセキュリティ観点からAPIトークン認証を使用するべきではありません
- JWTは使うべきではない 〜 SPAにおける本当にセキュアな認証方式 〜
- SPAのログイン認証のベストプラクティスがわからなかったのでわりと網羅的に研究してみた〜JWT or Session どっち?〜
- SPAセキュリティ入門~PHP Conference Japan 2021
モバイルアプリケーション認証(今回は実装しません)
- HasApiTokensトレイト、personal_access_tokensテーブルは必要
- ユーザーのメールアドレスorユーザー名、パスワード、デバイス名を受け入れるエンドポイントを作成する
- モバイルアプリケーション(iOS, Android等)の「ログイン」画面からトークンエンドポイントにリクエストを送信する
- デバイス名は「Nuno'siPhone12」等のユーザーが認識できる名前にする(デバイス名自体は任意の文字列)
- エンドポイントはプレーンテキストのAPIトークンを返却する
- APIトークンはモバイルデバイス内に保存する
- 認証が必要なエンドポイントは
Authorization
ヘッダにBearer
トークンを渡す
今回やること
SPA認証のみ、またはAPIトークン認証のみに利用することはまったく問題ありません。
Sanctumを利用しているからと言って両方の機能を使用する必要はありません。
今回はSPA認証に絞って進めていきたいと思います。
インストール(Laravel8.6以前)
$ composer require laravel/sanctum
※Laravel8.6以降からLaravel Sanctumが標準でインストールされるので不要です。
Composerパッケージマネージャーを介してLaravel Sanctumをインストールします。
$ php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
※Laravel8.6以降からLaravel Sanctumが標準でインストールされるので不要です。
下記のファイルが生成されます。
今回はAPIトークンを利用しないので database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php
は不要なので削除してokです。
今回はAPIトークンは利用しないのでマイグレーションの実行は不要です。
User
モデルにHasApiTokens
トレイトを追加する手順も不要です。
Sanctumのマイグレーションを無効化する
SPA認証を使う場合は、 personal_access_tokens
テーブルは不要です。
合っても困りはしませんが不要なので無効化します。
無効化手順は別記事で紹介しています。
SPA認証(クッキー認証)の実装
ファーストパーティドメインの設定
まず、SPA(Vue, React等)からリクエストを行うドメインを設定します。
SANCTUM_STATEFUL_DOMAINS=www.example.com
-
SANCTUM_STATEFUL_DOMAINS
にはフロント側のFQDN(ホスト名+ドメイン名)を設定します。- 基本は
APP_URL
に設定している値に合わせると良いです
- 基本は
- ローカルのSPA環境が定義済み(config/sanctum.php)のFQDNを使用していればこの設定は不要です。
- ローカル環境でも
config/sanctum.php
に定義されていないFQDNを使用している場合は下記のようにSANCTUM_STATEFUL_DOMAINS
環境変数を設定します。
SANCTUM_STATEFUL_DOMAINS=localhost:8000
ファーストパーティドメインの設定(補足)
SANCTUM_STATEFUL_DOMAINS
の環境変数は config/sanctum.php
の stateful
に設定されます。
デフォルト値は下記になってます。
'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) : ''
))),
ごちゃごちゃしてますが、最終的には下記の配列になってます。
>>> config('sanctum.stateful')
=> [
"localhost",
"localhost:3000",
"127.0.0.1",
"127.0.0.1:8000",
"::1",
"localhost",
]
各環境に合わせて .env
にドメインとポート番号を設定してください。
Sanctumミドルウェアを api
ミドルウェアグループに追加
Sanctumのミドルウェアを app/Http/Kernel.php
の api
ミドルウェアグループに追加します。
SPAからの受信リクエストがLaravelのセッションクッキーを使用して認証できるようになります。
また、サードパーティまたはモバイルアプリケーションからのリクエストがAPIトークンを使用して認証できるようにする役割を果たします。
class Kernel extends HttpKernel
{
protected $middlewareGroups = [
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, // 追記
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
}
※Laravel8.6以降は追記箇所がコメントアウトになっているので、有効化してください。
補足: Sanctumミドルウェア(必須)
EnsureFrontendRequestsAreStateful
ミドルウェアの補足です。
vendor/laravel/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php クリックして表示
<?php
namespace Laravel\Sanctum\Http\Middleware;
use Illuminate\Routing\Pipeline;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class EnsureFrontendRequestsAreStateful
{
/**
* Handle the incoming requests.
*
* @param \Illuminate\Http\Request $request
* @param callable $next
* @return \Illuminate\Http\Response
*/
public function handle($request, $next)
{
$this->configureSecureCookieSessions();
return (new Pipeline(app()))->send($request)->through(static::fromFrontend($request) ? [
function ($request, $next) {
$request->attributes->set('sanctum', true);
return $next($request);
},
config('sanctum.middleware.encrypt_cookies', \Illuminate\Cookie\Middleware\EncryptCookies::class),
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
config('sanctum.middleware.verify_csrf_token', \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class),
] : [])->then(function ($request) use ($next) {
return $next($request);
});
}
/**
* Configure secure cookie sessions.
*
* @return void
*/
protected function configureSecureCookieSessions()
{
config([
'session.http_only' => true,
'session.same_site' => 'lax',
]);
}
/**
* Determine if the given request is from the first-party application frontend.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public static function fromFrontend($request)
{
$domain = $request->headers->get('referer') ?: $request->headers->get('origin');
if (is_null($domain)) {
return false;
}
$domain = Str::replaceFirst('https://', '', $domain);
$domain = Str::replaceFirst('http://', '', $domain);
$domain = Str::endsWith($domain, '/') ? $domain : "{$domain}/";
$stateful = array_filter(config('sanctum.stateful', []));
return Str::is(Collection::make($stateful)->map(function ($uri) {
return trim($uri).'/*';
})->all(), $domain);
}
}
ここでやってる内容は下記の通りです。
- セッションクッキーのセキュア設定を強制
- refererヘッダーまたはoriginヘッダーのチェック
- SPAからこのヘッダーが送られないと認証エラーになる
- Postmanで動作確認するときに注意する
-
web
ミドルウェアグループで使用しているの一部のミドルウェアを導入- クッキー暗号化ミドルウェア(
\App\Http\Middleware\EncryptCookies
)- Cookieを暗号化する
- 攻撃者がCookieにアクセスできたとしても、その内容を変更するとCookieが返送されたときにサーバーによって拒否する
- レスポンスにクッキー付与(
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse
)- Cookieファサードでキューに入れられたCookieを処理する
- セッション有効化(
\Illuminate\Session\Middleware\StartSession
)- Laravelセッションとセッションクッキーを設定し、レスポンスに追加する
- CSRFトークン検証(
\App\Http\Middleware\VerifyCsrfToken
)- CSRFトークンがすべて正常であることを確認する
- クッキー暗号化ミドルウェア(
CORSとクッキー(別のサブドメインで実行する場合)
'supports_credentials' => true,
レスポンスヘッダの Access-Control-Allow-Credentials
が true
を返すようになります。
SPA側で axios
を使う場合は withCredentials
オプションを有効にする必要があります。
Laravelアプリケーションのセッションクッキードメイン設定をします。
'domain' => env('SESSION_DOMAIN', null),
SESSION_DOMAIN=.example.com
- ※ドメインの先頭に
.
を付けます。 -
null
の場合、サブドメイン間でCookieの共有ができません。 -
localhost
で確認する場合は不要です。
CSRF保護(Laravel側)
Laravel Sanctumをインストールした時点で下記のエンドポイントが追加されています。
$ php artisan route:list
GET|HEAD sanctum/csrf-cookie ................................................................................... Laravel\Sanctum › CsrfCookieController@show
CSRF保護(SPA側のログイン画面)
SPAを認証するには、SPAの「ログイン」ページで最初に/sanctum/csrf-cookie
エンドポイントにリクエストを送信して、アプリケーションのCSRF保護を初期化する必要があります。
axios.get('/sanctum/csrf-cookie').then(response => {
// ログイン…
})
このエンドポイントにGETリクエストするとCSRFトークンを含むXSRF-TOKEN
クッキー付きレスポンスが返却されます。
このクッキーに入っているトークンを X-XSRF-TOKEN
ヘッダに入れてSPA側からリクエストする必要があります。
(AxiosやAngular HttpClientなどの一部のHTTPクライアントライブラリでは自動的に行います。)
ログイン
-
/login
ルートにPOST
リクエストを手動実装する -
web
認証ガードを使用する - Laravelが提供するセッションベースの認証サービス
- ユーザーのセッションが期限切れになった場合、後続のリクエストは401か419HTTPエラーを返却する
- 401, 419エラーが出た場合はSPA側でログインページにリダイレクトする必要がある
補足: ルートの保護の例
routes/api.php
のAPIルートに sanctum
認証ガードを指定します。
受信リクエストがSPAからのステートフル認証済みリクエストとして認証されるか、リクエストがサードパーティからのものである場合は有効なAPIトークンヘッダを含むことを保証します。
use Illuminate\Http\Request;
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
補足: セッションの保存先設定(任意)
SESSION_DRIVER
のデフォルト値は file
です。
Google App Engineなどを使ってるとインスタンス間でセッションを共有できなくなります。
セッションドライバーを cookie
に変更してクライアント側でクッキーにセッションを入れて対応します。
SESSION_DRIVER=cookie
セッションドライバーを cookie
にする場合ですが、クッキーの機能として容量は4Kバイトまでで20個までしか保存できない制約があります。
認証情報以外にも何かしらの値をクッキーに保存していくとすぐ制約に引っかかってしまいます。
その場合はRedisをセッションドライバーとして利用すると良いでしょう。
SPA認証(クッキー認証)の実装
コントローラの作成
$ php artisan make:request Auth/LoginRequest
$ php artisan make:controller -i Auth/LoginController
$ php artisan make:controller -i Auth/LogoutController
$ php artisan make:controller -i Api/MeController
-i
はシングルアクションコントローラのひな形を生成します。
app/Http/Requests/Auth/LoginRequest.php クリックして表示
<?php declare(strict_types=1);
namespace App\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
final class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'email' => ['required', 'email'],
'password' => ['required', 'min:6'],
];
}
}
app/Http/Controllers/Auth/LoginController.php クリックして表示
<?php declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Auth\AuthManager;
use Illuminate\Http\JsonResponse;
final class LoginController extends Controller
{
/**
* @param AuthManager $auth
*/
public function __construct(
private readonly AuthManager $auth,
) {
}
/**
* @param LoginRequest $request
* @return JsonResponse
* @throws AuthenticationException
*/
public function __invoke(LoginRequest $request): JsonResponse
{
$credentials = $request->only(['email', 'password']);
if ($this->auth->guard()->attempt($credentials)) {
$request->session()->regenerate();
return new JsonResponse([
'message' => 'Authenticated.',
]);
}
throw new AuthenticationException();
}
}
app/Http/Controllers/Auth/LogoutController.php クリックして表示
<?php declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\AuthManager;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
final class LogoutController extends Controller
{
/**
* @param AuthManager $auth
*/
public function __construct(
private readonly AuthManager $auth,
) {
}
/**
* @param Request $request
* @return JsonResponse
*/
public function __invoke(Request $request): JsonResponse
{
if ($this->auth->guard()->guest()) {
return new JsonResponse([
'message' => 'Already Unauthenticated.',
]);
}
$this->auth->guard()->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return new JsonResponse([
'message' => 'Unauthenticated.',
]);
}
}
app/Http/Controllers/Api/MeController.php クリックして表示
<?php declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
final class MeController extends Controller
{
/**
* @param Request $request
* @return JsonResponse
*/
public function __invoke(Request $request): JsonResponse
{
$user = $request->user();
return new JsonResponse([
'data' => [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
],
]);
}
}
以上でコントローラの実装は完了です。
ルーティングの定義
作成したコントローラに対応するルーティングを実装します。
公式の例に倣ってログインログアウトはweb
ルートに作成します。
<?php declare(strict_types=1);
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\LogoutController;
use Illuminate\Support\Facades\Route;
Route::post('/login', LoginController::class)->name('login');
Route::post('/logout', LogoutController::class)->name('logout');
<?php declare(strict_types=1);
use App\Http\Controllers\Api\MeController;
use Illuminate\Support\Facades\Route;
Route::group(['middleware' => ['auth:sanctum']], function () {
Route::get('/me', MeController::class);
});
config/cors.php
の paths
に login
と logout
を追記します。
'paths' => [
'api/*',
'login', // 追記
'logout', // 追記
'sanctum/csrf-cookie',
],
$ php artisan route:list
GET|HEAD / .................................................................................................................................................
POST _ignition/execute-solution .......................................... ignition.executeSolution › Spatie\LaravelIgnition › ExecuteSolutionController
GET|HEAD _ignition/health-check ...................................................... ignition.healthCheck › Spatie\LaravelIgnition › HealthCheckController
POST _ignition/update-config ................................................... ignition.updateConfig › Spatie\LaravelIgnition › UpdateConfigController
GET|HEAD api/me ........................................................................................................................... Api\MeController
POST login ................................................................................................................ login › Auth\LoginController
POST logout ............................................................................................................. logout › Auth\LogoutController
GET|HEAD sanctum/csrf-cookie ................................................................................... Laravel\Sanctum › CsrfCookieController@show
続けてエンドポイントの動作確認を行なっていきます。
補足: デモユーザーの追加
tinkerを使ってデモユーザーを追加します。
$ php artisan tinker
App\Models\User::factory()->create(['email' => 'demo1@example.com']);
App\Models\User::factory()->create(['email' => 'demo2@example.com']);
App\Models\User::factory()->create(['email' => 'demo3@example.com']);
シーダーに書いてもokです。
デフォルトパスワードは password
です。
Laravelフレームワークのバージョンによっては secret
になってる場合もあります。
補足: Postmanを使うに当たっての注意点
動作確認はPostmanで行ってます。
注意点①
PostmanでPUT
, PATCH
, DELETE
などのメソッドを実行したい場合ですが、 form-data
ではなく raw
でJSON形式でAPIリクエストを行うようにしてください。
form-data
でリクエストを送ると _method
キー設定されず、意図した動作にならない場合があります。
注意点②
ログインの流れとして、 /sanctum/csrf-cookie
で返却されたXSRFクッキーの中にあるXSRFトークンを保存し、/login
へリクエストを送る際に X-XSRF-TOKEN
ヘッダにXSRFトークン入れて送る必要があります。
手動でやるのは面倒なので、Pre-request Script
を書きました。
Postmanの環境変数 APP_URL
に http://localhost
を入れておいてください。
let csrfRequestUrl = pm.environment.get('APP_URL') + '/sanctum/csrf-cookie';
pm.sendRequest(csrfRequestUrl, function(err, res, {cookies}) {
let xsrfCookie = cookies.one('XSRF-TOKEN');
if (xsrfCookie) {
let xsrfToken = decodeURIComponent(xsrfCookie['value']);
pm.request.headers.upsert({
key: 'X-XSRF-TOKEN',
value: xsrfToken,
});
pm.environment.set('XSRF-TOKEN', xsrfToken);
}
});
Postmanの環境変数 XSRF-TOKEN
を設定してます。
/login
エンドポイントの動作確認
新しく作られたエンドポイントをPostmanで実行してみます。
200レスポンスが返ってきているのでokです。
CookieにX-XSRF-TOKEN
が設定していることに注目してください。
Pre-request Script
タブに /sanctum/csrf-cookie
を事前に叩くコードを仕込んでいます。
/api/me
エンドポイントの動作確認
/api/*
のエンドポイントを実行するときは Referer
もしくは Origin
のどちらかのヘッダを必ず指定する必要があります。
GET
メソッドの場合は X-XSRF-TOKEN
は不要です。
/logout
エンドポイントの動作確認
/login
エンドポイントも同様ですが、Laravelのweb
ルートに定義した場合は Referer
と Origin
のヘッダは不要です。
また POST
, PUT
, PATCH
, DELETE
等の GET
メソッド以外を実行する場合は X-XSRF-TOKEN
が必要です。
テストコードの作成
- https://readouble.com/laravel/9.x/ja/sanctum.html#testing
- https://readouble.com/laravel/9.x/ja/http-tests.html
Laravel Sanctum のテストコードを書きます。
今回はAPIトークンを利用しないので、Laravel標準のHTTPテストのactingAs
メソッドを利用します。
$ php artisan make:test Http/Controllers/Auth/LoginControllerTest
$ php artisan make:test Http/Controllers/Auth/LogoutControllerTest
$ php artisan make:test Http/Controllers/Api/MeControllerTest
tests/Feature/Http/Controllers/Auth/LoginControllerTest.php クリックして表示
<?php declare(strict_types=1);
namespace Tests\Feature\Http\Controllers\Auth;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class LoginControllerTest extends TestCase
{
use RefreshDatabase;
/**
* @return void
*/
public function testSuccess(): void
{
User::factory()->create(['email' => 'test@example.com']);
$params = [
'email' => 'test@example.com',
'password' => 'password',
];
$this->postJson('/login', $params)
->assertStatus(200)
->assertJson([
'message' => 'Authenticated.',
]);
}
/**
* @return void
*/
public function testUnauthenticated(): void
{
$params = [
'email' => 'test@example.com',
'password' => 'password',
];
$this->postJson('/login', $params)
->assertStatus(401)
->assertJson([
'message' => 'Unauthenticated.',
]);
}
/**
* @return void
*/
public function testUnprocessableEntity(): void
{
$this->postJson('/login', [])
->assertStatus(422)
->assertJson([
'message' => 'The email field is required. (and 1 more error)',
'errors' => [
'email' => ['The email field is required.'],
'password' => ['The password field is required.'],
],
]);
}
}
tests/Feature/Http/Controllers/Auth/LogoutControllerTest.php クリックして表示
<?php declare(strict_types=1);
namespace Tests\Feature\Http\Controllers\Auth;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class LogoutControllerTest extends TestCase
{
use RefreshDatabase;
/**
* @return void
*/
public function testSuccess(): void
{
$user = User::factory()->create(['email' => 'test@example.com']);
$this->actingAs($user)
->postJson('/logout')
->assertStatus(200)
->assertJson([
'message' => 'Unauthenticated.',
]);
$this->assertGuest();
}
/**
* @return void
*/
public function testAlreadyUnauthenticated(): void
{
$this->postJson('/logout')
->assertStatus(200)
->assertJson([
'message' => 'Already Unauthenticated.',
]);
$this->assertGuest();
}
}
tests/Feature/Http/Controllers/Api/MeControllerTest.php クリックして表示
<?php declare(strict_types=1);
namespace Tests\Feature\Http\Controllers\Api;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class MeControllerTest extends TestCase
{
use RefreshDatabase;
/**
* @return void
*/
public function testSuccess(): void
{
$user = User::factory()->create([
'email' => 'test@example.com'
]);
$this->actingAs($user)
->getJson('/api/me')
->assertStatus(200)
->assertJson([
'data' => [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
],
]);
}
/**
* @return void
*/
public function testUnauthenticated(): void
{
$this->getJson('/api/me')
->assertStatus(401)
->assertJson([
'message' => 'Unauthenticated.',
]);
}
}
テストコードは補足することはないですが、ログイン、ログアウト、認証中ユーザー取得のテストを追加してます。
テストコードの実行
$ php artisan test
PASS Tests\Unit\ExampleTest
✓ that true is true
PASS Tests\Feature\ExampleTest
✓ the application returns a successful response
PASS Tests\Feature\Http\Controllers\Auth\LoginControllerTest
✓ success
✓ unauthenticated
✓ unprocessable entity
PASS Tests\Feature\Http\Controllers\Auth\LogoutControllerTest
✓ success
✓ already unauthenticated
PASS Tests\Feature\Http\Controllers\Api\MeControllerTest
✓ success
✓ unauthenticated
Tests: 9 passed
Time: 1.78s
テストを実行して正常に通ればokです。
テスト用のデータベースを利用していない場合、データベースの中身がテストするたびにクリアされてしまうのでテスト用のデータベースコンテナを追加すると便利です。
補足: $this->postJson(), $this->getJson()
$this->postJson()
, $this->getJson()
を指定しないと例外発生時はHTTPレスポンスが返ってきてしまいます。
$this->post()
, $this->get()
でテスト書きたい場合は下記の記事を参考にミドルウェアを作ると良いです。
補足: sanctum:prune-expired
php artisan sanctum:prune-expired
コマンドはpersonal_access_tokens テーブルで有効期限の切れたトークンのレコードを削除します。
つまり、今回のSPA認証では使用しないコマンドです。