Laravelの認証カスタマイズに関するメモ

# 概要
EloquentUserProviderを継承し、カスタムユーザープロバイダを定義して認証をカスタマイズした時に書いためも

## ざっくり結論
### カスタマイズ内容と、どこに実装するか、の関係
カスタマイズする内容と実装するべきコンポーネントの関係は以下の通り。

- ログインの認証状態をどうやって管理するかをカスタマイズしたい場合は「ガード」

- 認証時に、どのようにユーザーを取得するかをカスタマイズしたい場合は「プロバイダ」（本稿のテーマ）

- プレゼンテーションに関する内容のカスタマイズはController

### 「プロバイダ」のカスタマイズ手順
本稿の主なテーマ。
EloquentUserProvider を継承した独自のカスタムユーザープロバイダを作成し、retrieveByCredentials() 、validateCredentials() メソッドあたりをオーバーライドすればだいたいのことができる。

## 具体例
・認証後のリダイレクト先を変更したい => Controllerでやる

・認証条件にemailやpassword以外の条件を加えたい => 「プロバイダ」でやる
・メールアドレスでの認証を、ユーザIDに変更する => 「プロバイダ」でやる

・認証状態をRedisやデータベースで管理したい => 「ガード」でやる

・ゲストユーザ、一般ユーザと管理者でサイトを制限するために、ロールを追加する。
=> 「認証」が取り扱う範囲ではない！（「権限」が扱うべき範囲）

## 備考
Laravelの認証をカスタマイズする方法は色々と紹介されているが、クラスの責務の範囲について考慮していない記事も散見されるので、注意した方がよい。（認証ロジックをコントローラーに書くのは良くないですよ）

Laravelはお手軽で便利だが、よく理解しないと不適切な場所に実装してしまえるという問題がある。何をどこに書くべきなのかをよく検討しておくと、メンテしやすいコードになりやすい。

「ガード」に手を入れる必要がある場合はわりと頑張る必要がある。
https://re-engines.com/2020/10/05/laraval-custom-auth/
https://notes.yh1224.com/tech/a210332ea70fb99bcd607e3a21f51aa4/


# 前提
このあたりを読んでおくと理解がはやいです。  

- [図解] Laravel の認証周りのややこしいあれこれ。
https://zenn.dev/ad5/articles/48671b32c89897  



## ガードとプロバイダ

Laravelのドキュメントに記載されている通り、「ガード」と「プロバイダ」という2つの概念によって構成されていることを理解する必要がある。
> Laravelの認証機能は「ガード」と「プロバイダ」を中心概念として構成されています。ガードは各リクエストごとに、どのようにユーザーを認証するかを定義します。たとえば、Laravelにはセッションストレージとクッキーを使いながら状態を維持するsessionガードが用意されています。
>
> プロバイダは永続ストレージから、どのようにユーザーを取得するかを定義します。LaravelはEloquentとデータベースクリエビルダを使用しユーザーを取得する機能を用意しています。しかし、アプリケーションの必要性に応じて、自由にプロバイダを追加できます。
>
> [https://readouble.com/laravel/8.x/ja/authentication.html](https://readouble.com/laravel/8.x/ja/authentication.html)


## 補足

### ガードの種類

ガードにはSessionGuardの他に、RequestGuard、TokenGuardなどが用意されている。

（SessionGuardはステートフル、RequestGuard、TokenGuardはステートレス）

### プロバイダ

上記ドキュメントでは、単に「プロバイダ」と記載されているので、サービスプロバイダと混同しないよう注意が必要。

### ガードとプロバイダの関係

設定ファイル config/auth.php  で、どのガードがどのプロバイダを利用するかが設定されている。（詳細は後述）

### 責務の範囲

カスタマイズの際には、

・ログインの認証状態をどうやって管理するかをカスタマイズしたい場合は「ガード」

・認証時に、どのようにユーザーを取得するかをカスタマイズしたい場合は「プロバイダ」

を独自に定義すればよい。

※「ガード」をカスタマイズしなければならない状況はあまりなく、基本的には独自の「プロバイダ」を定義すれば事足りる。

※独自の「ガード」を作成する場合は、本当に「ガード」で実装を行うべきかどうかの検討を行ったほうがよい。


以下、本稿では、大部分の機能についてLaravelのSessionGuard、EloquentUserProviderを利用しつつ、
EloquentUserProviderを継承した独自の「プロバイダ」を定義することで、認証処理をカスタマイズする具体的手順について説明する。


# 作業手順

- EloquentUserProvider を継承した MyEloquentUserProviderクラスを作成
- サービスプロバイダへの追記（MyEloquentUserProviderクラスのインスタンス作成処理の追加）
- config/auth.php の修正

## MyEloquentUserProviderクラスの作成

以下の要領で、EloquentUserProviderを継承した独自の「プロバイダ」を定義する。   

主にretrieveByCredentials() 、validateCredentials() の2つのメソッドをオーバーライドすることが多いだろう。


```php:MyEloquentUserProvider.php
<?php

namespace App\Auth;

use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Auth\Authenticatable;

class MyEloquentUserProvider extends EloquentUserProvider implements UserProvider
{
    /**
     * retrieveByCredentials をオーバーライドし、$credentialsから認証対象のEloquentモデルを取得する処理をカスタマイズする。
     * Retrieve a user by the given credentials.
     * @see \Illuminate\Auth\EloquentUserProvider::retrieveByCredentials()
     * @see \Illuminate\Auth\SessionGuard::attempt()
     * @see \Illuminate\Auth\SessionGuard::validate()
     * @param  array  $credentials
     * @return Authenticatable|null
     */
    public function retrieveByCredentials(array $credentials)
    {
        //Do anything
        //DBへの問い合わせ条件を追加したりするのはココ
        //一意なレコードが定まるような条件でクエリするよう留意する。
    }

    /**
     * validateCredentials をオーバーライドして、$credentials に対する検査内容をカスタマイズする。
     *
     * @see \Illuminate\Auth\EloquentUserProvider::validateCredentials()
     * @see \Illuminate\Auth\SessionGuard::attempt()
     * @see \Illuminate\Auth\SessionGuard::validate()
     * @param UserContract $user
     * @param array $credentials
     * @return bool
     */
    public function validateCredentials(UserContract $user, array $credentials)
    {
        //Do anything
        //retrieveByCredentials() により得られたModelに対するパスワードの突合などをカスタマイズする場合はココ
    }
}

```


※これらのメソッドをオーバーライドする際には、SessionGuard、EloquentUserProviderの処理の流れもあわせて確認しておくこと。認証時に実行される処理の順序や、それぞれのメソッドの役割について理解しておかないと、不適切な（本来そのメソッドが期待されている役割以上の）内容を実装することもできてしまうため、注意が必要である。



## サービスプロバイダ

独自に定義した「プロバイダ」を適用するために、サービスプロバイダにインスタンスの生成処理を追記する。

デフォルトで用意されている \App\Providers\AppServiceProvider のboot() メソッド内で行ってもよいし、新たにAuthServiceProviderなどの名称でサービスプロバイダを追加してもよい。基本的には後者をオススメ。

```php:

    public function boot()
    {
        //なんやかんやあって
        
        
         
        //カスタムプロバイダの名前を定義
        \Auth::provider(
            //この部分の名前は何でもよい。config/auth.php には、この名称で設定を行う。
            'my_eloquent_user_provider',
            function ($app, array $config) {
                //MyEloquentUserProviderクラスのインスタンスを生成する
                return new MyEloquentUserProvider($app['hash'], $config['model']);
            }
        );
        

        //なんやかんやが続く
        
    }

```

※サービスプロバイダを新規に追加した場合は、config/app.php にそのサービスプロバイダを追記するのを忘れないように。



## 設定ファイル config/auth.php の修正

設定ファイルで、「プロバイダ」の設定を変更する。この例では、ログイン情報が customers テーブルに格納されていて、App\Models\Customer経由でログイン者情報を取得する際のロジックとして、"my_eloquent_user_provider" を利用することを設定している。

```php:config/auth.php
    /*
    |--------------------------------------------------------------------------
    | Authentication Guards
    |--------------------------------------------------------------------------
    |
    | Next, you may define every authentication guard for your application.
    | Of course, a great default configuration has been defined for you
    | here which uses session storage and the Eloquent user provider.
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | Supported: "session", "token"
    |
    */
    'guards' => [
        'web' => [
            //「ガード」の種類。SessionGuard。
            'driver' => 'session',
            //「プロバイダ」の種類。（この下の'providers'中の'customers' 設定を参照する）
            'provider' => 'customers',
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | If you have multiple user tables or models you may configure multiple
    | sources which represent each model / table. These sources may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */
    'providers' => [
        // "customers"「プロバイダ」
        'customers' => [
            // 前項のサービスプロバイダと同じ文字列を指定する
            'driver' => 'my_eloquent_user_provider',
            // ログイン者に対応するEloquentモデルのクラス名
            'model' => App\Models\Customer::class,
        ],
    ],

```

# 余談
Guardのカスタマイズは比較的ハードルが高いが、必要な場合はSessionGuardの処理を参考に。

SessionGuardを継承して独自のGuardを定義する場合、サービスプロバイダでインスタンスを生成する際に、\Illuminate\Auth\AuthManager::createSessionDriver() の処理を参考に実装すること。

適切にプロパティをセットしないとLaravelのLoginイベントが発火しなかったり、RemeberMeの機能が不具合を起こしたりといろいろと問題が起こるっぽいので、親クラスの処理をよく見て実装すすめるとよい。
（基本的には「プロバイダ」で済む範囲ではないかの検討をしておくべき）

```php:AuthServiceProvider.php
    public function boot()
    {
        $this->registerPolicies();

        $this->app['auth']->extend('my_custom_guard', function ($app, $name, array $config) {
            $provider = Auth::createUserProvider($config['provider']);
            $guard = new MyCustomGuard($name, $provider, $app['session.store']);

            /**
             * 以下、Laravelのセッションドライバインスタンス生成ロジックと同等の処理を行う必要がある。
             * @see \Illuminate\Auth\AuthManager::createSessionDriver()
             */
            //これを忘れるとたぶんRemeberMe周りの機能がおかしくなる（未検証）
            if (method_exists($guard, 'setCookieJar')) {
                $guard->setCookieJar($this->app['cookie']);
            }
            //これを忘れるとLaravelの認証関連イベントが発火しなくなる（実際に試した）
            if (method_exists($guard, 'setDispatcher')) {
                $guard->setDispatcher($this->app['events']);
            }
            //未検証
            if (method_exists($guard, 'setRequest')) {
                $guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
            }
            return $guard;
        });
    }
```