見出し画像

【有料級】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. 1つめはcookieを利用した認証、これをSPA認証と呼んでいる

  2. 2つ目はapi tokenを利用した認証、これをAPIトークン認証と呼んでいる

SPA認証

Sanctumは、Laravelを利用したAPIと通信する必要があるシングルページアプリケーション(SPA)を認証する簡単な手段を提供するためにも存在しています。これらのSPAは、Laravelアプリケーションと同じリポジトリに存在する場合もあれば、完全に別個のリポジトリである場合もあります。

https://readouble.com/laravel/11.x/ja/sanctum.html

Sanctumを使用すると、アプリケーションへのAPIリクエストの認証に使用できるAPIトークン/パーソナルアクセストークンを発行できます。APIトークンを使用してリクエストを行う場合、トークンは「Bearer」トークンとして「Authorization」ヘッダに含める必要があります。

https://readouble.com/laravel/11.x/ja/sanctum.html


今回は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を指定する、すると


しかしここからが茨の路である


さらに、必ずリクエストへAccept: application/jsonヘッダと、RefererかOriginヘッダのどちらかを付け、送信してください。

https://readouble.com/laravel/11.x/ja/sanctum.html

ということなので、ここでは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


いいなと思ったら応援しよう!