Laravel6.xで認証機能をカスタマイズする

こんにちは、小林です。
今回はLaravelの認証機能をカスタマイズ(Eloquentドライバ)の方法です。
メールアドレスとパスワード以外に参照する項目を追加するのは比較的簡単に出来ると思うのですが、少し複雑な条件(例えば複数テーブルを参照するなど)でユーザを取得したい場合はひと手間かかります。 そのあたりを書いていきたいと思います。

目次

参照する項目を追加するだけの場合

簡単な方から。
参照する項目を追加するだけであれば、以下のようにcredentialsをオーバーライドして 参照したい項目を追加してあげます。
以下であれば、active項目が1である条件が追加されます。

laravel/app/Http/Controllers/Auth/LoginController

<?php
protected function credentials(Request $request)
{
    $request->merge(['active' => 1]);
    
    return $request->only($this->username(), 'password', 'active');
}

もう少し複雑な認証をしたい場合

EloquentUserProviderをカスタマイズして使っていきます。

認証用サービスプロバイダ作成

まず、独自認証するためのサービスプロバイダを作成します。

php artisan make:provider CustomAuthServiceProvider

\Illuminate\Auth\EloquentUserProviderを継承して、retrieveByCredentialsとvalidateCredentialsをオーバーライドします。
retrieveByCredentialsでは、Eloquentを操作します。リレーションや条件を追加しましょう。
validateCredentialsでは、独自の認証を追加します。判定結果(Boolean)を戻り値として返してあげればOKです。
基本となるユーザテーブルからの取得条件等を変えるならretrieveByCredentials、それにプラスして独自認証を追加したい場合は validateCredentialsを修正します。

app/Providers/CustomAuthServiceProvider

<?php
namespace App\Providers;

use Illuminate\Support\Str;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;

class CustomAuthServiceProvider extends \Illuminate\Auth\EloquentUserProvider
{
    public function retrieveByCredentials(array $credentials)
    {
        if (empty($credentials) ||
           (count($credentials) === 1 &&
            array_key_exists('password', $credentials))) {
            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);
            } else {
                $query->where($key, $value);
            }
        }
  
        return $query->first();
    }

    public function validateCredentials(UserContract $user, array $credentials)
    {
        $plain = $credentials['password'];
  
        // こんな感じで判定を追加します
        return $this->hasher->check($plain, $user->getAuthPassword()) && $this->check($user, $credentials);
    }
  
  
     /**
     * 独自追加認証
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  array  $credentials
     * @return bool
     */
    public function check(UserContract $user, array $credentials)
    {
        $check = true;
  
        // 何らかの認証処理
          
        return $check 
    }
}

プロバイダへ登録

次にこのプロバイダを登録します。
ここでは「custom_auth」という名前で登録しています。

app/Providers/AuthServiceProvider.php

<?php

    public function boot()
    {
        $this->registerPolicies();

        // 以下を追加  
        Auth::provider('custom_auth', function($app, array $config) {
            return new CustomAuthServiceProvider($this->app['hash'], $config['model']);
        });
    }

Auth configの修正

登録したら、ドライバとして使用するようにauthのconfigを修正します。

config/auth.php

<?php

'providers' => [
        'users' => [
            'driver' => 'custom_auth', // 修正
            'model' => App\User::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

以上で、CustomAuthServiceProvider を利用して認証処理が行われるようになります。

これで終わりと行きたいところですが、このままだとパスワード再発行時の処理でCustomAuthServiceProviderが使われません。 ということで、パスワード発行のところを修正していきます。

PasswordBrokerの作成

まずBrokerを作成します。

app/Auth/Passwords/CustomPasswordBroker.php

<?php
namespace App\Auth\Passwords;

use Illuminate\Auth\Passwords\PasswordBroker;
use App\Providers\CustomAuthServiceProvider;
use Illuminate\Auth\Passwords\TokenRepositoryInterface;
  
  
class CustomPasswordBroker extends PasswordBroker
{
    /**
     * Create a new password broker instance.
     *
     * @param  \Illuminate\Auth\Passwords\TokenRepositoryInterface  $tokens
     * @param  \Illuminate\Contracts\Auth\UserProvider  $users
     * @return void
     */
    public function __construct(TokenRepositoryInterface $tokens, CustomAuthServiceProvider $users) // ここを書き換える
    {
        $this->users = $users;
        $this->tokens = $tokens;
    }
}

__constructをオーバーライドして、$usersの型をCustomAuthServiceProviderにします。

BrokerManagerの作成

次に、BrokerManagerを作成します。

app/Auth/Passwords/CustomPasswordBrokerManager.php

<?php
namespace App\Auth\Passwords;

use Illuminate\Auth\Passwords\PasswordBrokerManager;
use InvalidArgumentException;
  
  
class CustomPasswordBrokerManager extends PasswordBrokerManager
{
    protected function resolve($name)
    {
        $config = $this->getConfig($name);

        if (is_null($config)) {
            throw new InvalidArgumentException("Password resetter [{$name}] is not defined.");
        }

        return new CustomPasswordBroker( // ここを書き換えてCustomAuthServiceProviderを使うように
            $this->createTokenRepository($config),
            $this->app['auth']->createUserProvider($config['provider'] ?? null)
        );
    }
}

ここでは、__resolveをオーバーライドして、CustomPasswordBrokerを使うようにします。

ServiceProviderの作成

そして、作成したBrokerManagerを使用するためのサービスを作成します。

app/Providers/CustomPasswordResetServiceProvider.php

<?php
  
namespace App\Providers;
  
use Illuminate\Auth\Passwords\PasswordResetServiceProvider;
use \App\Auth\Passwords\CustomPasswordBrokerManager;
  
class CustomPasswordResetServiceProvider extends PasswordResetServiceProvider
{
    protected function registerPasswordBroker()
    {
        $this->app->singleton('auth.password', function ($app) {
            return new CustomPasswordBrokerManager($app);
        });

        $this->app->bind('auth.password.broker', function ($app) {
            return $app->make('auth.password')->broker();
        });
    }
}

app configの修正

最後に、app configの「providers」を修正して、PasswordResetServiceProviderの代わりにCustomPasswordResetServiceProviderを使用するようにします。

<?php

    'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
        Illuminate\Auth\AuthServiceProvider::class,
        Illuminate\Broadcasting\BroadcastServiceProvider::class,
        Illuminate\Bus\BusServiceProvider::class,
        Illuminate\Cache\CacheServiceProvider::class,
        Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
        Illuminate\Cookie\CookieServiceProvider::class,
        Illuminate\Database\DatabaseServiceProvider::class,
        Illuminate\Encryption\EncryptionServiceProvider::class,
        Illuminate\Filesystem\FilesystemServiceProvider::class,
        Illuminate\Foundation\Providers\FoundationServiceProvider::class,
        Illuminate\Hashing\HashServiceProvider::class,
        Illuminate\Mail\MailServiceProvider::class,
        Illuminate\Notifications\NotificationServiceProvider::class,
        Illuminate\Pagination\PaginationServiceProvider::class,
        Illuminate\Pipeline\PipelineServiceProvider::class,
        Illuminate\Queue\QueueServiceProvider::class,
        Illuminate\Redis\RedisServiceProvider::class,
        //Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, // コメントアウト
        App\Providers\CustomPasswordResetServiceProvider::class, // 追加
        Illuminate\Session\SessionServiceProvider::class,
        Illuminate\Translation\TranslationServiceProvider::class,
        Illuminate\Validation\ValidationServiceProvider::class,
        Illuminate\View\ViewServiceProvider::class,
 

これで、パスワード再発行も「CustomAuthServiceProvider」を使って行われるようになりました。

まとめ

このあたりの認証カスタマイズの情報があまり多くないように感じたので書いてみました。
複数テーブルを参照する程度であればEloquentUserProviderをカスタマイズして十分対応できるのかなと思います。
機会があればドライバ自体を作成する方法も試してみたいですね。