Laravelのカスタムガードでステートフルな認証を実現する

## 前置き

Laravelの認証では<code>Illuminate\Contracts\Auth\Guard</code>を実装することで、自前の認証ロジックを組み込むことができます。

<p><a href="https://readouble.com/laravel/8.x/ja/authentication.html#adding-custom-guards">https://readouble.com/laravel/8.x/ja/authentication.html#adding-custom-guards</a></p>


例えば、このようなGuardを作成すると、パラメータに<code>id</code>というキーの値が含まれている時、そのIDのUserとして認証をさせることができます。
この認証ロジックを工夫することで、JWTを使った認証や、その他外部サービスと連携した認証が行えるという訳です。。

```php:app/Auth/MyGuard.php
MyGuard implements Guard
{
    use GuardHelpers;

    private Request $request;

    private UserProvider $userProvider;

    /**
     * MyGuard constructor.
     * @param Request $request
     * @param UserProvider $userProvider
     */
    public function __construct(Request $request, UserProvider $userProvider)
    {
        $this->request = $request;
        $this->userProvider = $userProvider;
    }

    public function user()
    {
        $id = $this->request->get('id');
        if ($id) {
            $this->user = $this->userProvider->retrieveByCredentials(['id' => $id]);
        }
        return $this->user;
    }

    public function validate(array $credentials = []): bool
    {
        return false;
    }
```

<!-- wp:paragraph -->
<p>この自前のGuardをサービスプロバイダに登録します。</p>
<!-- /wp:paragraph -->

```php:app/Providers/AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
...
    public function boot()
    {
        Auth::extend('custom', function ($app, $name, array $config) {
            return new MyGuard(request(), Auth::createUserProvider($config['provider']));
        });
    }
}
```

<!-- wp:paragraph -->
<p>そして、<code>auth.php</code>を編集し登録したサービスをアプリケーションで使用するように設定します。</p>
<!-- /wp:paragraph -->

```php:config/auth.php
    'guards' => [
        'web' => [
            'driver' => 'custom',
            'provider' => 'users',
        ],
```

<!-- wp:paragraph -->
<p>最後に、<code>web.php</code>に以下のようなルートを追加し、http://localhost/users?id=1にアクセスすると、DBにID1のユーザーが登録されている時、そのユーザーのnameが表示されます。</p>
<!-- /wp:paragraph -->

```php:routes/web.php
Route::get('/users', function () {
    /** @var User $user */
    $user = Auth::user();
    return $user->name ?? 'not logged in';
});
```

<!-- wp:heading -->
<h2>本題</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Laravelの認証に自前のロジックを組み込む方法はわかりました。しかし、上で作成したGuardはステートレスな物になっており、認証をするためには毎回<code>id=1</code>のパラメータを付与する必要があります。</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>一度<code>id=1</code>で認証したら、二度目以降はパラメータなしでも認証できるようにするにはどうしたら良いのでしょうか？</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Laravelのドキュメントには<code>Auth::login($user, $rememberMe)</code>というメソッドを実行し、<code>$rememberMe</code>を<code>true</code>にすることで次回から自動ログインできる、と書いてあります。</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><a href="https://readouble.com/laravel/8.x/ja/authentication.html#authenticate-a-user-instance">https://readouble.com/laravel/8.x/ja/authentication.html#authenticate-a-user-instance</a></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>なので、以下のようにユーザーを取得したタイミングで、このメソッドを呼び出せば良さそうです。</p>
<!-- /wp:paragraph -->

```php
    public function user()
    {
        $id = $this->request->get('id');
        if ($id) {
            $this->user = $this->userProvider->retrieveByCredentials(['id' => $id]);
            Auth::login($this->user, true)
        }
        return $this->user;
    }
```

<!-- wp:paragraph -->
<p>が、実はこれでは動きません。この<code>Auth</code>ファサードの<code>login()</code>メソッドは実はアプリケーションで使用されているGuard（今回は<code>CustomGuard</code>）の<code>login()</code>メソッドのエイリアスになっているので、<code>CustomGuard</code>自体に<code>login()</code>メソッドを実装する必要があるのです。</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>もう少し正確な話をすると「ステートフルな認証を行う場合、アプリケーションで使用するGuardは<code>Illuminate\Contracts\Auth\StatefulGuard</code>を実装する必要があり、<code>login()</code>メソッドはその中で実装しなければいけないメソッドの一つ」ということです。</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><code>StatefulGuard</code>は<code>login()</code>メソッドの他にこれらのメソッドを実装する必要があります。</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li><code>attempt()</code></li><li><code>once()</code></li><li><code>login()</code></li><li><code>loginUsingId()</code></li><li><code>onceUsingId()</code></li><li><code>viaRemember()</code></li><li><code>logout()</code></li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>これらのメソッドを生真面目に実装しても良いのですが、認証状態を継続したい、という要件だけなら<code>SessionGuard</code>を継承してしまうのが早いです。</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>まず親クラスのSessionGuardの<code>user()</code>を呼び出し、ステートフルな認証情報からユーザーが取得できたら、それを返します。見つからなかった場合は、自前の認証ロジックを呼び出し、<code>SessionGuard</code>の<code>login()</code>メソッド（<code>Auth::login()</code>と同じ意味）を呼び出し、ログイン状態を記憶します。</p>
<!-- /wp:paragraph -->

```php
class MyGuard extends SessionGuard
{
    public function __construct($name, UserProvider $provider, Session $session, Request $request = null)
    {
        parent::__construct($name, $provider, $session, $request);
    }

    public function user()
    {
        $this->user = parent::user();

        if ($this->user) {
            return $this->user;
        }

        $id = $this->request->get('id');
        if ($id) {
            $this->user = $this->provider->retrieveByCredentials(['id' => $id]);
            $this->login($this->user);
        }
        
        return $this->user;
    }
}
```

<!-- wp:paragraph -->
<p>プロバイダーへの登録はこのようになります。</p>
<!-- /wp:paragraph -->

```php
class AppServiceProvider extends ServiceProvider
{
...
    public function boot()
    {
        Auth::extend('custom', function ($app, $name, array $config) {
            $session = $this->app->make(Session::class);
            return new MyGuard($name, Auth::createUserProvider($config['provider']), $session, request());
        });
    }
}
```

<!-- wp:paragraph -->
<p>以上！</p>
<!-- /wp:paragraph -->