【有料級】Laravel Sanctumをbreezeで組み立ててPostmanでテストする (laravel11)
まあこういうニッチな情報を探してる人にとってはという事でね
大前提
breeze install前まで迅速にセットアップする
curl -s "https://laravel.build/example_app?with=mysql" | bash
APP_PORTは8000を指定している
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:4dKFWJfI7w9yFnuNNOBQVSdTiwFCAuih+pT6b9G5yCQ=
APP_DEBUG=true
APP_TIMEZONE=UTC
APP_URL=http://localhost:8000
FRONTEND_URL=http://localhost:3000
APP_PORT=8000
sailを起動して
% ./vendor/bin/sail up
keyをセットする
% ./vendor/bin/sail artisan key:gen
INFO Application key set successfully.
DBをseedする
% ./vendor/bin/sail artisan migrate:fresh --seed
INFO Preparing database.
Creating migration table ........................................................................... 37.05ms DONE
INFO Running migrations.
0001_01_01_000000_create_users_table .............................................................. 212.59ms DONE
0001_01_01_000001_create_cache_table ............................................................... 74.82ms DONE
0001_01_01_000002_create_jobs_table ............................................................... 163.78ms DONE
INFO Seeding database.
この段階でtest@example.com / passwordなアカウントが作成される。ここまでは前回と何ら変わらないので、必要に応じて過去の記事を参照する事。
なお、この段階でgitに登録しておくとファイルの変化がわかりやすいかも。
% git init
% git add .
% git commit -m init
laravel breezeのダウンロードとインストール
./vendor/bin/sail composer require laravel/breeze --dev
./vendor/bin/sail artisan breeze:install
とすると
% ./vendor/bin/sail artisan breeze:install
┌ Which Breeze stack would you like to install? ───────────────┐
│ › ● Blade with Alpine │
│ ○ Livewire (Volt Class API) with Alpine │
│ ○ Livewire (Volt Functional API) with Alpine │
│ ○ React with Inertia │
│ ○ Vue with Inertia │
│ ○ API only │
└──────────────────────────────────────────────────────────────┘
このようになるので、API only を選択
┌ Which Breeze stack would you like to install? ───────────────┐
│ ○ Blade with Alpine │
│ ○ Livewire (Volt Class API) with Alpine │
│ ○ Livewire (Volt Functional API) with Alpine │
│ ○ React with Inertia │
│ ○ Vue with Inertia │
│ › ● API only │
└──────────────────────────────────────────────────────────────┘
テストはまあ適当に
┌ Which testing framework do you prefer? ──────────────────────┐
│ Pest │
└──────────────────────────────────────────────────────────────┘
すると
./composer.json has been updated
Running composer update laravel/sanctum
Loading composer repositories with package information
Updating dependencies
Lock file operations: 1 install, 0 updates, 0 removals
- Locking laravel/sanctum (v4.0.5)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
- Downloading laravel/sanctum (v4.0.5)
- Installing laravel/sanctum (v4.0.5): Extracting archive
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi
INFO Discovering packages.
laravel/breeze ........................................................ DONE
laravel/pail .......................................................... DONE
laravel/sail .......................................................... DONE
laravel/sanctum ....................................................... DONE
laravel/tinker ........................................................ DONE
nesbot/carbon ......................................................... DONE
nunomaduro/collision .................................................. DONE
nunomaduro/termwind ................................................... DONE
78 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
> @php artisan vendor:publish --tag=laravel-assets --ansi --force
INFO No publishable resources for tag [laravel-assets].
No security vulnerability advisories found.
INFO Published API routes file.
One new database migration has been published. Would you like to run all pending database migrations? (yes/no) [yes]:
>
これはyesでもなんでもいいけどyes
INFO Discovering packages.
laravel/breeze ........................................................ DONE
laravel/pail .......................................................... DONE
laravel/sail .......................................................... DONE
laravel/sanctum ....................................................... DONE
laravel/tinker ........................................................ DONE
nesbot/carbon ......................................................... DONE
nunomaduro/collision .................................................. DONE
nunomaduro/termwind ................................................... DONE
pestphp/pest-plugin-laravel ........................................... DONE
85 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
> @php artisan vendor:publish --tag=laravel-assets --ansi --force
INFO No publishable resources for tag [laravel-assets].
No security vulnerability advisories found.
Using version ^3.5 for pestphp/pest
Using version ^3.0 for pestphp/pest-plugin-laravel
INFO Breeze scaffolding installed successfully.
変更点
% git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: app/Http/Controllers/Auth/AuthenticatedSessionController.php
new file: app/Http/Controllers/Auth/EmailVerificationNotificationController.php
new file: app/Http/Controllers/Auth/NewPasswordController.php
new file: app/Http/Controllers/Auth/PasswordResetLinkController.php
new file: app/Http/Controllers/Auth/RegisteredUserController.php
new file: app/Http/Controllers/Auth/VerifyEmailController.php
new file: app/Http/Middleware/EnsureEmailIsVerified.php
new file: app/Http/Requests/Auth/LoginRequest.php
modified: app/Providers/AppServiceProvider.php
modified: bootstrap/app.php
modified: composer.json
modified: composer.lock
new file: config/cors.php
new file: config/sanctum.php
new file: database/migrations/2024_11_29_135103_create_personal_access_tokens_table.php
deleted: package.json
deleted: resources/css/app.css
deleted: resources/js/app.js
deleted: resources/js/bootstrap.js
new file: resources/views/.gitkeep
deleted: resources/views/welcome.blade.php
new file: routes/api.php
new file: routes/auth.php
modified: routes/web.php
new file: tests/Feature/Auth/AuthenticationTest.php
new file: tests/Feature/Auth/EmailVerificationTest.php
new file: tests/Feature/Auth/PasswordResetTest.php
new file: tests/Feature/Auth/RegistrationTest.php
modified: tests/Feature/ExampleTest.php
new file: tests/Pest.php
modified: tests/Unit/ExampleTest.php
deleted: vite.config.js
このようにdeleteされているものも多数ある。実はここで行われている作業は
artisan api:install
などが裏で走ってたりするわけだがそれはまあいいや。
特に注目するファイル
追加されたもの
config/cors.php
config/sanctum.php
変更されたもの
bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
+ api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
+ $middleware->api(prepend: [
+ \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
+ ]);
+
+ $middleware->alias([
+ 'verified' => \App\Http\Middleware\EnsureEmailIsVerified::class,
+ ]);
+
//
})
->withExceptions(function (Exceptions $exceptions) {
変更されたり追加されたりしているroute
routes/web.php
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return ['Laravel' => app()->version()];
});
require __DIR__.'/auth.php';
routes/api.php
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::middleware(['auth:sanctum'])->get('/user', function (Request $request) {
return $request->user();
});
routes/auth.php
<?php
use App\Http\Controllers\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Auth\EmailVerificationNotificationController;
use App\Http\Controllers\Auth\NewPasswordController;
use App\Http\Controllers\Auth\PasswordResetLinkController;
use App\Http\Controllers\Auth\RegisteredUserController;
use App\Http\Controllers\Auth\VerifyEmailController;
use Illuminate\Support\Facades\Route;
Route::post('/register', [RegisteredUserController::class, 'store'])
->middleware('guest')
->name('register');
Route::post('/login', [AuthenticatedSessionController::class, 'store'])
->middleware('guest')
->name('login');
Route::post('/forgot-password', [PasswordResetLinkController::class, 'store'])
->middleware('guest')
->name('password.email');
Route::post('/reset-password', [NewPasswordController::class, 'store'])
->middleware('guest')
->name('password.store');
Route::get('/verify-email/{id}/{hash}', VerifyEmailController::class)
->middleware(['auth', 'signed', 'throttle:6,1'])
->name('verification.verify');
Route::post('/email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
->middleware(['auth', 'throttle:6,1'])
->name('verification.send');
Route::post('/logout', [AuthenticatedSessionController::class, 'destroy'])
->middleware('auth')
->name('logout');
変更後のroute:list
% ./vendor/bin/sail artisan route:list
GET|HEAD / ....................................................................................................
GET|HEAD api/user .............................................................................................
POST email/verification-notification verification.send › Auth\EmailVerificationNotificationController@store
POST forgot-password .............................. password.email › Auth\PasswordResetLinkController@store
POST login .............................................. login › Auth\AuthenticatedSessionController@store
POST logout .......................................... logout › Auth\AuthenticatedSessionController@destroy
POST register .............................................. register › Auth\RegisteredUserController@store
POST reset-password ..................................... password.store › Auth\NewPasswordController@store
GET|HEAD sanctum/csrf-cookie ................ sanctum.csrf-cookie › Laravel\Sanctum › CsrfCookieController@show
GET|HEAD storage/{path} ......................................................................... storage.local
GET|HEAD up ...................................................................................................
GET|HEAD verify-email/{id}/{hash} ............................ verification.verify › Auth\VerifyEmailController Showing [12] routes
注目すべきは sanctum/csrf-cookie とか login とか api/user とか
CSRF保護
とりあえずCSRF保護とapiの認証に関しては全く別のレイヤーとしてあるんだけど、CSRFに関して説明が非常に面倒なので今回は割愛。つまり、CSRFを無効にする。
これはlaravel11ではbootstrap/app.php に定義する
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->api(prepend: [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
]);
$middleware->alias([
'verified' => \App\Http\Middleware\EnsureEmailIsVerified::class,
]);
$middleware->validateCsrfTokens(except: [
'api/*',
'login',
// 'http://example.com/foo/*', // ...
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
これは
$middleware->validateCsrfTokens(except: [
'api/*',
'login',
// 'http://example.com/foo/*', // ...
]);
ここに
api/*
login
のURLをCSRF保護を無効化している。
Postmanを起動
ここではbreeze api testというコレクションを作成した。さらにリクエストを作成
これはブラウザーでトップにアクセスしたのと何も変わらない結果となっている
まあ単純にこれが実行されているだけ
Route::get('/', function () {
return ['Laravel' => app()->version()];
});
Postmanで認証がしたい
冒頭にも書いたようにデフォルトのseedでmigrateが終わっているのであれば
test@example.com
password
でログインできるユーザーが1つ作られているはずだ。routes:listで調査すると
POST login .............................................. login › Auth\AuthenticatedSessionController@store
これが重要なはずである。まあroot accessは必要ないからとりあえずそれを改変して/loginにpostしてみよう
すると非常にわかり辛い事に、またトップページの内容が表示されてしまった。これは実は内部的にredirectが発生している。これを行わないよう設定の変更をする
この状態でリクエストを送信すると
このようになる。しかし何故こうなるかの理由は明らかにされていない
【重要】Acceptヘッダを変更する
このように
Accept: application/json
を追加している。Acceptは2つあっても構わない。これで実行すると
このように、どういった理由でうまくいってないのかようやくわかるようになるのだ。つまり、emailとpasswordがセットされていないということ。当然だよね。
emailとpasswordをセットする
ボディタブからform-data を選択しキーに
email: test@example.com
password: password
を入力する
これを入力するとNo Contentとなり処理が終了する。これは
app/Http/Controllers/Auth/AuthenticatedSessionController.php
public function store(LoginRequest $request): Response
{
$request->authenticate();
$request->session()->regenerate();
return response()->noContent();
}
これに由来している。
noContentのままでいいのか?
【重要】Laravel Sanctumの認証方式は2種類ある
ここでSanctumの認証方式について
1つめはcookieを利用した認証、これをSPA認証と呼んでいる
2つ目はapi tokenを利用した認証、これをAPIトークン認証と呼んでいる
今回は1つ目のSPA認証を利用するため、重要なのはcookieの値という事になる。つまり xxxx_create_personal_access_tokens_table.php みたいなマイグレーションファイルは全く利用しない。捨ててもいい。
で、Postmanの場合基本的にcookieは自動的に引き継がれていくため、いちいちセットする必要は無い。
(しかしAPI認証を行う場合はnoContentだと辛いからちょっと加工しないといけないとは思う)
/api/userにアクセスしてみる
これはroutes/api.php の中の非常にシンプルな行で定義されている
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::middleware(['auth:sanctum'])->get('/user', function (Request $request) {
return $request->user();
});
が、しかしここで注目すべきは
Route::middleware(['auth:sanctum'])
これであり、この部分は基本的にauth:sanctumでmiddlewareの段階で保護されている。って話はさておいて、リクエストを投げてみよう
このように、./api/userにリクエストを放りこんでいるにもかかわらず/loginっぽいところに移動し、GETはダメですよとか言われているのはもちろんこれはリダイレクトを追いかけてしまっているからだ。
設定から外してみて
リクエストを送信すると
リダイレクトされている事はわかるんだけど、でもやっぱりこれだと理由がよくわからんので、前と同じようにAcceptを変更してapplication/jsonを指定する、すると
しかしここからが茨の路である
ということなので、ここではOriginを付けてみる
とまあこんな感じでport番号を含む値をセットした。そうしてもまだ
{"message":"Unauthenticated."}
が出力されていると思う。これはさらに設定を必要としており
SANCTUM_STATEFUL_DOMAINS
これにポート番号を含むサーバーの値を記入しなくてはならない
ここでは
SANCTUM_STATEFUL_DOMAINS=server:8000
みたいな値を書いている。これでリクエストしてみると
このように認証される
参考までに: これを行っている場所
vendor/laravel/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php
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);
}
この辺で行っている
以上がとりあえず簡単なLaravel Sanctumの認証だ。apiを生やす場合はroutes/api.php に書いていくという事になるんじゃないでしょーか。
次回は
reactとかと連携するかもしれないし、しないかもしれない。
#laravel #laravel_breeze #laravel初心者 #laravel学習 #laravel_sanctum #postman