Laravel で認証パラメータや認証条件をもっと追加する方法

# 何がしたい？

ユーザーの作成・ログイン・パスワードリマインダーなど、ユーザー認証周りの機能を「任せとけ！」とワンタッチでサラッと作ってくれる男前でカッコいいLaravelさんですが、チョット待って、認証条件がEmailとパスワードだけじゃなくて、有効無効フラグや「ログイン権限」もチェックしたいのですが…。

という、Google先生ならサラッと教えてくれそうなテーマですが、サラッと教えてくれる答えがあまりぴったり来る感じではなかったので、僭越ながら穴埋め係させていただきます。


# 結論

## １．ユーザーを探す条件を追加する

最も手軽な方法。
Userモデルに is_valid というカラムを追加しているようなケースに適用できます。
やることは、標準で app ディレクトリ内に作ってくれているログインコントローラにメソッドを追加するだけ。

```php:app/Http/Controllers/Auth/LoginController.php
// 冒頭にコレを追加するのを忘れずに
use Illuminate\Http\Request;

class LoginController extends Controller
{
    // ...

    /**
     * ユーザーを探す条件を指定する
     *
     * @param  \Illuminate\Http\Request $request
     * @return Response
     */
    protected function credentials(Request $request)
    {
        return array_merge( 
            $request->only($this->username(), 'password'), // 標準の条件
            [ 'is_valid' => true ] // 追加条件
        );
    }
}
```


## ２．ユーザーを探す方法をカスタマイズする

１の方法だと、App\User のDBカラムしか検索対象になりません。
「ログイン権限」のような権限は別途RoleやPolicy的なモデルやLaravelの機能を使うことが多いと思いますが、そうした「別テーブル」や「別機能」で制限したい場合は、「ユーザーを探す方法」をカスタマイズします。

参考：https://laravel.com/docs/6.x/authentication#adding-custom-user-providers

＃(　ﾟДﾟ) passwordをnullにしとけばいいんじゃね？ 
(∩ﾟдﾟ) ｱｰｱｰきこえなーい


### ユーザープロバイダーを作成

場所はどこでも大丈夫なのですが、今回は標準のログインコントローラの横に置きました。

＃わかりやすいけど、役割的にはHTTPと関係ないので、できれば Provider 配下（直下だとServiceProviderと区別しにくいので /Provider/UserProvider/MyUserProvider.php とディレクトリ切ったほうが良いかも）がオススメ。


```php:app\Http\Controllers\Auth\MyUserProvider.php
namespace App\Http\Controllers\MyUserProvider;

use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Gate;

class MyUserProvider extends EloquentUserProvider implements UserProvider
{
    /**
     * 与えられた credentials からユーザーのインスタンスを探す
     *
     * @param  array  $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByCredentials(array $credentials)
    {
        $user = parent::retrieveByCredentials($credentials);

        // リレーション先の権限テーブルで can_login が true を持つユーザーに制限
        if( $user->privileges()->where('privilege','can_login')->where('status',true)->exists() )
        {
            return $user;
        } else {
            return null;
        }
    }
}
```

Gateで権限管理をしている場合はこのように。
（Gateの使い方は別書に譲ります…）

```php:app\Http\Controllers\Auth\MyUserProvider.php
    public function retrieveByCredentials(array $credentials)
    {
        $user = parent::retrieveByCredentials($credentials);

        if( Gate::forUser($user)->allows('feature.login') )
        {
            return $user;
        } else {
            return null;
        }
    }
```

もちろん、検索をゼロからカスタムすることもできます。

```php:app\Http\Controllers\Auth\MyUserProvider.php
    public function retrieveByCredentials(array $credentials)
    {
        $query = $this->createModel()->newQuery();

        $user = $query
        ->where('login_id', $credentials['login_id'] ?? null )
        // 検索条件を追加する
        ->join( 'privilege', .... )
        ->first();

        return $user;
    }
```

よくある「ユーザー名またはEMAILで…」とするならこんな感じ

```php:app\Http\Controllers\Auth\MyUserProvider.php
    public function retrieveByCredentials(array $credentials)
    {
        $query = $this->createModel()->newQuery();

        $user = $query
        ->where('user_name', $credentials['login_id'] ?? null )
        ->orWhere('email', $credentials['login_id'] ?? null )
        ->first();

        return $user;
    }
```



### AppServiceProviderで登録

ヒトテマですが、作成したユーザープロバイダは登録しないと使えません…。
今回はお手軽に AppServiceProvider で登録します。

```php:app/Providers/AppServiceProvider.php
    public function register()
    {
        \Auth::provider('my_user_provider', function ($app, array $config) {
            return new \App\Http\Controllers\Auth\MyUserProvider( $app['hash'], $config['model'] );
        });
    
        // ...
    }
```

＃ Auth機能にUserProviderとして登録しています。
AppにServiceProviderを登録したり、
サービスコンテナにサービスを登録するのとは別機能なので混同しないように注意…。


### configでユーザープロバイダを指定

最後に、configで認証ドライバを置き換えます。

```php:config/auth.php
    'providers' => [
        'users' => [
            // 'driver' => 'eloquent',    // 消して
            'driver' => 'my_user_provider', // 追加
            'model' => App\User::class,
        ],
```

以上。


## ３．認証方法をカスタマイズする（非推奨）

２の方法など生ぬるいわ……。
そのユーザーがログインできるかどうかのチェックをカスタマイズしたいのだ……👿。
という邪悪な思想をお持ちの方は、同じMyUserProviderにコレを追加sしてください。

```php:app\Http\Controllers\Auth\MyUserProvider.php
    public function validateCredentials(Authenticatable $user, array $credentials)
    {
        // 独自の認証ロジックをコール
        if ( MySuperAuthAlgorithm::validateUser( $user->login_id, $user->password ) {
          return true;
        }
        return false;
    }
```

これは、そもそも「認証ロジック」がLaravel管理外の別モジュールにある場合などに使うもので、それがLaravel標準の認証ロジックよりもセキュリティが強固だ！セキュリティホールも絶対にないしあってもすぐに対応するぜ！という自信と責任感がある場合のみ行ってください😋



# 解説

Laravelの認証周りはカスタマイズ方法がガッツリと用意されています。
さらに、その認証ステップの各段階に、どのクラスのどのメソッドがどんな役割を担っているかも
しっかり決まっていて細かく別れています。

今回の「ログイン」でいうと、下記のような感じ。

1. 入力された情報（Request）から、ユーザーを探す条件（credentials）だけ抽出する
2. ユーザーを探す条件を元に、ユーザーモデルのインスタンス（App\Userなど）を取得する
3. そのユーザーモデルがログインすることができるかをチェックする

それぞれ、

1. LoginController@credentials
2. UserProvicer@retrieveByCredentials
3. UserProvider@validateCredentials

となっています。

このステップをそのまま、3通りのカスタマイズ方法として紹介させていただきました。

以下、その標準コードと役割の解説。

## LoginController@credentials

標準では `Illuminate\Foundation\Auth\AuthenticatesUsers` がトレイトとして機能を提供、 LoginController に組み込まれています。

```php:vendor\laravel\framework\src\Illuminate\Foundation\Auth\AuthenticatesUsers.php
    protected function credentials(Request $request)
    {
        return $request->only($this->username(), 'password');
    }

    public function username()
    {
        return 'email';
    }
```

リクエスト $request から email と password を抜き取って配列にしているだけです。
username()メソッドは、ログインIDが入っているカラムをオーバーライドして変更するためのメソッドですね。

## UserProvicer@retrieveByCredentials

標準だと `Illuminate\Auth\EloquentUserProvider` です。

```php:vendor\laravel\framework\src\Illuminate\Auth\EloquentUserProvider.php
    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->createModel()->newQuery();

        foreach ($credentials as $key => $value) {
            if (! Str::contains($key, 'password')) {
                $query->where($key, $value);
            }
        }

        return $query->first();
    }
```

重要なのは後半で、 credentilals を `->where( $key , $value )` とシンプルにクエリに変換しています。
なので、App\User にカラムがあってそれをANDで連結する限り、
先の credentials メソッドをカスタマイズするだけで条件が増やせます。

## UserProvider@validateCredentials

UserProviderにはいろんな機能を載せることができて、その最も重要なメソッドがコレ。

```php:vendor\laravel\framework\src\Illuminate\Auth\EloquentUserProvider.php
    public function validateCredentials(UserContract $user, array $credentials)
    {
        $plain = $credentials['password'];

        return $this->hasher->check($plain, $user->getAuthPassword());
    }
```

`Hash::check` でパスワードチェックをしています。ここは触らずにおきましょー。


# 感想

全然関係ないけど、最近になってようやく「コントローラ」とそれ以外の役割分担がわかってきました。

コントローラ ... HTTPリクエストを受け取り、必要な機能を呼ぶ
それ以外 ... 提供したい機能に必要な**最小限のパラメータ**を受け取り、結果を最小限で返す

なので、コントローラのコードは、`Request $request` を受け取って、それをバラして、必要な情報だけ選択して、サービスを呼び出すだけ。
**$reqeustはコントローラから外には出さない**。
そうした考え方が「ログインに必要な情報はEMAILとPASSWORDだけだからそれを抜き取ってくるのがログインコントローラ」というのがLaravel標準でもしっかり実装されていて、あー自分のこの考え方は合ってるんだなーと実感した次第。

このあたりはまた機会を改めて記事にできればと思っています。





# こんな記事も書いています

Laravelのちょっとマニアックな視点から、誰も書かない記事を書いています（笑
合わせてご覧いただけると幸いです（＾＾）


#### サービスコンテナ講座

-  [もう怖くない こんなにカンタンだから今日から Laravelのサービスコンテナ を使ってみて！](https://qiita.com/kd9951/items/951ab700f28d1d49c9c9)
- [シングルトンはたった1行 サービスプロバイダの使いどころ](https://qiita.com/kd9951/items/2f803ef9c6ebab1f2dbf)
-  [なんでもツッコんで気軽に取り出すサービスコンテナの核心](https://qiita.com/kd9951/items/f2157607d2f6a9534449)
-  [メソッドインジェクションがしたい！](https://qiita.com/kd9951/items/2328e2f8add9037bd990)