Laravel Breezeでマルチ認証に対応する方法

Laravel Breezeでマルチ認証(複数ユーザーの認証)に対応する方法です。認証システムをLaravel UIからBreezeに移行するための備忘録です。

Laravel UIでは認証系コントローラーでTraitを実装していたのでguardメソッドを上書きすることでガードの設定ができましたが、Laravel Breezeでは各コントローラーで処理が完結しているので1つずつ設定する必要があります。

なお、Breezeのインストールまで完了しているところからの解説となります。

環境

  • PHP 8.1
  • Laravel 10.15
  • Laravel Breeze 1.21

認証用モデルを追加

CustomUserモデルとパスワードリセット用のテーブルを作ります。内容はデフォルトのUserモデルを一旦コピペします。

なお、モデルとガードの名前はお好みで変更してください。

php artisan make:model CustomUser -m
php artisan make:migration create_custom_user_password_reset_tokens_table
<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class CustomUser extends Authenticatable
{
// 以下は app/Models/User.php の内容をそのままコピペ
...

    public function up(): void
    {
        Schema::create('custom_users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

...
...
    public function up(): void
    {
        Schema::create('custom_user_password_reset_tokens', function (Blueprint $table) {
            $table->string('email')->primary();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });
    }
...

認証ガードを登録

...

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'custom_user' => [
            'driver' => 'session',
            'provider' => 'custom_users',
        ],
    ],

...

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
        'custom_users' => [
            'driver' => 'eloquent',
            'model' => App\Models\CustomUser::class,
        ],
    ],

...

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_reset_tokens',
            'expire' => 60,
            'throttle' => 60,
        ],
        'custom_users' => [
            'provider' => 'custom_users',
            'table' => 'custom_user_password_reset_tokens',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

...

認証系コントローラーにガードを適用する

続いて認証系のコントローラーにガードを適用してきます。

まずは、app/Http/Controllers/CustomUser/Auth というディレクトリを作成し、 app/Http/Controllers/Auth にあるファイルを全てコピペします。

次に、コピペしたファイルのnamespaceを App\Http\Controllers\CustomUser\Auth に変更します。

ようやくコントローラーを修正していきます。コードをすべて記載すると記事が長くなってしまうので変更点に絞って記載します。少し面倒ですが1つずつ変更していきましょう。

    public function destroy(Request $request): RedirectResponse
    {
        Auth::guard('custom_user')->logout();

        $request->session()->invalidate();
    public function store(Request $request): RedirectResponse
    {
        if (! Auth::guard('custom_user')->validate([
            'email' => $request->user('custom_user')->email,
            'password' => $request->password,
    public function store(Request $request): RedirectResponse
    {
        if ($request->user('custom_user')->hasVerifiedEmail()) {
            return redirect()->intended('/custom'.RouteServiceProvider::HOME);
        }

        $request->user('custom_user')->sendEmailVerificationNotification();
    public function __invoke(Request $request): RedirectResponse|Response
    {
        return $request->user('custom_user')->hasVerifiedEmail()
                    ? redirect()->intended('/custom'.RouteServiceProvider::HOME)
    public function store(Request $request): RedirectResponse
    {
...
        $status = Password::broker('custom_users')->reset(
            $request->only('email', 'password', 'password_confirmation', 'token'),
            function ($user) use ($request) {
                $user->forceFill([
                    'password' => Hash::make($request->password),
                    'remember_token' => Str::random(60),
                ])->save();

                event(new PasswordReset($user));
            }
        );
    public function update(Request $request): RedirectResponse
    {
...
        $request->user('custom_user')->update([
            'password' => Hash::make($validated['password']),
        ]);
    public function store(Request $request): RedirectResponse
    {
...
        $status = Password::broker('custom_users')->sendResetLink(
            $request->only('email')
        );
    public function store(Request $request): RedirectResponse
    {
        $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:'.CustomUser::class,
            'password' => ['required', 'confirmed', Rules\Password::defaults()],
        ]);

        $user = CustomUser::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);

        event(new Registered($user));

        Auth::guard('custom_user')->login($user);

        return redirect('/custom'.RouteServiceProvider::HOME);
    }
    public function __invoke(EmailVerificationRequest $request): RedirectResponse
    {
        if ($request->user('custom_user')->hasVerifiedEmail()) {
            return redirect()->intended('/custom'.RouteServiceProvider::HOME.'?verified=1');
        }

        if ($request->user('custom_user')->markEmailAsVerified()) {
            event(new Verified($request->user('custom_user')));
        }

        return redirect()->intended('/custom'.RouteServiceProvider::HOME.'?verified=1');
    }

LoginRequestを修正

Laravel Breezeでは実際の認証処理は App\Http\Requests\Auth\LoginRequest で行っています。CustomUser用のリクエストを作成してもいいのですが、LoginRequestを少し修正するだけで流用できます。

    public function authenticate(string $guard = 'web'): void
    {
        $this->ensureIsNotRateLimited();

        if (! Auth::guard($guard)->attempt($this->only('email', 'password'), $this->boolean('remember'))) {
            RateLimiter::hit($this->throttleKey());
    public function store(LoginRequest $request): RedirectResponse
    {
        $request->authenticate('custom_user');

        $request->session()->regenerate();

テストファイルとミドルウェアを修正

最後にテストファイルとミドルウェアを修正しましょう。

テストでは actingAs メソッドの第二引数にガードを渡して認証モデルを指定できます。

$response = $this->actingAs($user, 'custom_user')->get('/');

認証に関するミドルウェアは

  • App\Http\Middleware\Authenticate : 認証前のユーザーをログイン画面にリダイレクト
  • App\Http\Middleware\RedirectIfAuthenticated : 認証済みのユーザーを管理画面などにリダイレクト

の2つです。

    protected function redirectTo(Request $request): ?string
    {
        if ($request->expectsJson()) {
            return null;
        }

        if ($request->routeIs('custom.*')) {
            return route('custom.login');
        }

        return route('login');
    }
    public function handle(Request $request, Closure $next, string ...$guards): Response
    {
        if (Auth::guard('custom_user')->check()) {
            return redirect('/custom'.RouteServiceProvider::HOME);
        }

        guards = empty($guards) ? [null] : $guards;

        foreach ($guards as $guard) {
            if (Auth::guard($guard)->check()) {
                return redirect(RouteServiceProvider::HOME);
            }
        }

        return $next($request);
    }

カスタムガードのリダイレクト先も既存の実装にならって RouteServiceProvider の定数で指定してもいいかもしれません。

以上です。

コメント

タイトルとURLをコピーしました