[Laravel] EloquentUserProviderを継承して、独自のGuardを定義する


## 概要

EloquentUserProviderを継承して、独自のGuardを定義をします。

定義することによって、例えば、以下のよくあるユースケースの例のような、認証の微妙なカスタマイズが可能となります。

- 非会員であるユーザは、ログイン不可
- 非アクティブなステータスのユーザは、ログイン不可
- etc...

### 具体的には

やることに関しては、公式の「認証」>「カスタムユーザープロバイダの追加」のあたりが該当します。

https://readouble.com/laravel/9.x/ja/authentication.html

`config/auth.php`で定義している`driver`を、`'eloquent'` (`EloquentUserProvider`)ではなく、拡張したものを使用するのがゴールです。

```php:config/auth.php
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
```


## 前準備

### バージョン
```bash
# sail artisan --version
Laravel Framework 9.7.0
```

### laravel/uiを用いた、auth関連の導入

```bash
composer require laravel/ui
php artisan ui bootstrap --auth
```

（Laravel7以降で導入可能

### Modelを用意

- 認証に使用するModelに、`Illuminate\Foundation\Auth\User`を継承させておく。

```php:app/Models/AdminUser.php
<?php

namespace App\Models;

use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class AdminUser extends Authenticatable
{
// ...
```

## 実装

### 独自のAuthProviderの作成

`AdminUserAuthProvider.php`を作成し、`Illuminate\Auth\EloquentUserProvider`を継承します。

継承元の`EloquentUserProvider`については[こちら](https://github.com/laravel/framework/blob/9.x/src/Illuminate/Auth/EloquentUserProvider.php)

（↓のディレクトリ位置は私の趣味です。ご自由に
```php:app/Providers/Auth/AdminUserAuthProvider.php
<?php

namespace App\Providers\Auth;

use Illuminate\Auth\EloquentUserProvider;

class AdminUserAuthProvider extends EloquentUserProvider
{
    /**
     * {@inheritDoc}
     */
    public function retrieveById($identifier)
    {
        $model = $this->createModel();

        return $this->newModelQuery($model)
                    ->where($model->getAuthIdentifierName(), $identifier)
                    // AdminUser::scopeActive()
                    ->active()
                    ->first();
    }

    /**
     * {@inheritDoc}
     */
    public function retrieveByToken($identifier, $token)
    {
        $model = $this->createModel();

        $retrievedModel = $this->newModelQuery($model)->where(
            $model->getAuthIdentifierName(), $identifier
        )
        // AdminUser::scopeActive()
        ->active()
        ->first();

        if (! $retrievedModel) {
            return;
        }

        $rememberToken = $retrievedModel->getRememberToken();

        return $rememberToken && hash_equals($rememberToken, $token)
                        ? $retrievedModel : null;
    }

    /**
     * {@inheritDoc}
     */
    public function retrieveByCredentials(array $credentials)
    {
        if (empty($credentials) ||
           (count($credentials) === 1 &&
            str_contains($this->firstCredentialKey($credentials), 'password'))) {
            return;
        }

        // First we will add each credential element to the query as a where clause.
        // Then we can execute the query and, if we found a user, return it in a
        // Eloquent User "model" that will be utilized by the Guard instances.
        $query = $this->newModelQuery();

        foreach ($credentials as $key => $value) {
            if (str_contains($key, 'password')) {
                continue;
            }

            if (is_array($value) || $value instanceof Arrayable) {
                $query->whereIn($key, $value);
            } elseif ($value instanceof Closure) {
                $value($query);
            } else {
                $query->where($key, $value);
            }
        }

        // AdminUser::scopeActive()
        $query->active();

        return $query->first();
    }
}

```

オーバライドしている3つのメソッドは、それぞれ下記の認識です。

- `retrieveById()`
    - ログイン認証後の認証を行う。（ログイン後の認証保持
- `retrieveByToken()`
    - `remember_token`を用いた、ユーザ認証の取得
    - おそらく、`remember_token`を使用しないなら不要
- `retrieveByCredentials()`
    - 実際のログイン認証時のパスワード認証

今回はModelのAdminUserに`scopeActive()`を定義し、このスコープを用いて、アクティブなステータスのAdminUserを取得するようにしています。
実際に使用する場合は、各種ロジックにおいて、ユースケースに応じたロジックを追加するとよいです。

### `AuthServiceProvider`に、作成したAuthProviderを登録

```php:app/Providers/AuthServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use App\Providers\Auth\AdminUserAuthProvider;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{

// 省略

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        // ↓を追加
        // @see Illuminate\Auth\CreatesUserProviders::createUserProvider()
        Auth::provider('admin_users', function ($app, array $config) {
            return new AdminUserAuthProvider($this->app['hash'], $config['model']);
        });
    }
}

```

（`Auth::provider()`が、あんまりドキュメントに残ってなくて鬼門でした...

### `config/auth.php`に登録

```php:config/auth.php
    'providers' => [
        // ↓ 登録したdriverで登録
        'admin' => [
            'driver' => 'admin_users',
            'model' => AdminUser::class,
        ],
```

これでguardが作成できました。
あとは、このguardを用いた`LoginController`や、認証まわりのルーティングを作成しましょう。

## 余談

認証まわりのテストメソッドとして、TestCaseにて、`$this->assertCredentials()`などで使用できるメソッドが多々あります。
特に、例として載せたコードのように、ログインさせないケースがある場合は、`assertInvalidCredentials()`が役に立ちました。

`Illuminate\Foundation\Testing\ConcernsInteractsWithAuthentication`に、認証回りのテストメソッドがまとまっているので見てみるといいかもです。

https://github.com/laravel/framework/blob/9.x/src/Illuminate/Foundation/Testing/Concerns/InteractsWithAuthentication.php

## 最後に

探していてあまり同様の記事が見つからなかったので、今後の自分のメモがてら書いてみました。
間違っている点ありましたら、コメントにてご指摘お願いします。

もしよろしければ、LGTMをよろしくおねがいします～～！！