Laravel 8.x 時点での認証の実装

Laravel は多彩な認証ライブラリをリリースしています。

- Tailwind CSS && Blade Templates で出来た [Laravel Breeze](https://laravel.com/docs/8.x/starter-kits#laravel-breeze)
- Tailwind CSS && (Livewire || Inertia) で出来た [Laravel Jetstream](https://jetstream.laravel.com/2.x/introduction.html)
    - 認証部分に [Fortify](https://laravel.com/docs/8.x/fortify)
    - API/SPA認証部分に [Sanctum](https://laravel.com/docs/8.x/sanctum)
- OAuth 2 プロバイダ実装の [Passport](https://laravel.com/docs/8.x/passport)
- OAuth 2 ログイン実装の [Socialite](https://laravel.com/docs/8.x/socialite)
- (レガシーパッケージとなっていますが) Bootstrap && (React || Vue) で出来た [laravel UI](https://github.com/laravel/ui)

どれが要件に合うのかという所でもかなり悩みどころですが、とにかく選択肢は豊富です。

今回はこれらのライブラリについて深掘りしていくより、 Laravel 自体の認証の実装方法について調べてみます。

## イントロダクション

[Laravel Authentication](https://laravel.com/docs/8.x/authentication)ページでは、最初にこのように書かれています。

> 多くのウェブアプリケーションは、ユーザがアプリケーションで認証して「ログイン」するための方法を提供しています。この機能をウェブアプリケーションに実装することは、複雑でリスクを伴う可能性があります。このため、Laravelは、認証を素早く、安全に、簡単に実装するために必要なツールを提供するように努めています。
> Laravelの認証機能は「ガード」と「プロバイダ」で構成されています。ガードは、各リクエストに対してユーザーがどのように認証されるかを定義します。例えば、Laravelにはセッションガードが搭載されており、セッションストレージとクッキーを使用して状態を維持します。
> プロバイダーは、永続的なストレージからユーザーを取得する方法を定義します。LaravelはEloquentとデータベースクエリビルダを使ったユーザーの取得をサポートしています。しかし、アプリケーションに必要に応じて追加のプロバイダを自由に定義することができます。
> アプリケーションの認証設定ファイルはconfig/auth.phpにあります。このファイルには、Laravelの認証サービスの動作を微調整するためのいくつかのオプションが含まれています。
> www.DeepL.com/Translator （無料版）で翻訳しました。


## Config

認証情報の設定は `config/auth.php` にまとまっています。

```php
<?php

return [
    // デフォルト(Guard未指定)での利用 Guard(`Auth::user()` などした時に使われるもの)
    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

    // Guard の実装リスト。 Middleware の `auth:api` や `Auth::guard('api')` のように明示指定出来る
    'guards' => [
        'web' => [
            // SessionGuard を利用し、プロバイダは `users` (後述のキー値)
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            // TokenGuard を利用し、プロバイダは `users`, ハッシュ化はしない
            'driver' => 'token',
            'provider' => 'users',
            'hash' => false,
        ],
    ],

    'providers' => [
        // 'users' プロバイダは Eloquent を利用する
        // 'users' というキー名は上記 Guard の 'provider' に指定するもの
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],

        // 代わりに DatabaseUserProvider を指定することも可能
        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],
// ... 以下省略
];
```

## Guard

どのリクエスト情報(session だとか Authorization ヘッダーだとか)からユーザ情報を取得出来るかの実装です。

```php
<?php

namespace Illuminate\Contracts\Auth;

interface Guard
{
    /**
     * 現在のユーザが認証されているかどうか
     *
     * @return bool
     */
    public function check();

    /**
     * 現在のユーザがゲスト(未ログイン)かどうか
     *
     * @return bool
     */
    public function guest();

    /**
     * 現在の認証されたユーザーを取得する
     *
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function user();

    /**
     * 現在の認証されたユーザーの ID を取得する
     *
     * @return int|string|null
     */
    public function id();

    /**
     * ユーザーの秘匿情報をバリデートする
     *
     * @param  array  $credentials
     * @return bool
     */
    public function validate(array $credentials = []);

    /**
     * 現在のユーザーを設定する
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @return void
     */
    public function setUser(Authenticatable $user);
}
```

このインターフェースの実装となります。

これに対する Laravel 上での実装は `RequestGuard`, `TokenGuard`, `SessionGuard` の三つになります。

### RequestGuard

これは HTTP Request 内の任意の要素を使ってログイン判定をする最も簡単な Guard です。

```php
Auth::viaRequest('custom-token', function (Request $request) {
  return User::where('token', $request->token)->first();
});
```

このようにして新しいドライバーをクロージャで作ることが出来ます。

```php
return [
  'guards' => [
    'api' => [
      'driver' => 'custom-token',
    ],
  ],
];
```

config/auth.php でこのようにドライバーを指定することで機能します。簡単。

これはステートレスなのでセッションを張らずに認証が可能です(代わりに毎回同じトークンを渡す必要があります)。

### TokenGuard

これはリクエストからトークンを取得し、それをもって認証する Guard です。

- クエリストリング `/foo?api_token=aaa`
- リクエストデータ `curl --data '{"api_token":"aaa"}'`
- Bearer トークン `Authorization: Bearer aaa`
- パスワード `https://foo:aaa@example.com`

これらを探索し、トークンが見つかればそれを利用してログインを試みます。 `api_token` というキー名は config で上書きが可能です。

これもステートレスです。

### SessionGuard

これはクッキーとストレージにセッション情報を残し、ステートフルに認証する Guard です。

普通にブラウザでアクセスする要件ではまずこれを使うのが基本になると思います。

---

## UserProvider

Guard は「クライアントから送られてきた認証情報をどう取得するか」を定義したものに対し、 UserProvider は「取得した認証情報からデータベース内のユーザー情報をどう取得するか」を定義したものです。

```php
<?php

namespace Illuminate\Contracts\Auth;

interface UserProvider
{
    /**
     * ユニークIDでユーザーを取得する
     *
     * @param  mixed  $identifier
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveById($identifier);

    /**
     * ユニークIDと "remember me" トークンでユーザーを取得する
     *
     * @param  mixed  $identifier
     * @param  string  $token
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByToken($identifier, $token);

    /**
     * 渡されたユーザーの "remember me" トークンを更新する
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  string  $token
     * @return void
     */
    public function updateRememberToken(Authenticatable $user, $token);

    /**
     * 渡された秘匿情報からユーザーを取得する
     *
     * @param  array  $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByCredentials(array $credentials);

    /**
     * 渡された秘匿情報を使ってユーザーをバリデートする
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  array  $credentials
     * @return bool
     */
    public function validateCredentials(Authenticatable $user, array $credentials);
}
```

インターフェースはこのようになっています。

remember token が必須になっているのは若干モヤっとする設計ですが、基本的に渡されたパラメータでデータベースからユーザーを取得するメソッドを実装することになります。

Laravel では `EloquentUserProvider` と `DatabaseUserProvider` が実装されています。

### EloquentUserProvider

最初からある `App\Models\User` クラスのような ORM を使って取得する手法です。最も一般的です。

### DatabaseUserProvider

コンフィグからテーブル名を指定して `password` カラムを使って取得する手法です。カラム名が `password` 固定なのがモヤっとするというか、使いづらい所ですね。

---

ちなみに `RequestGuard` を利用する場合はこの UserProvider を通さずにログインすることが出来ます(指定したクロージャで DB にアクセスしているので)。

---

この UserProvider で利用されるユーザー情報は `Illuminate\Contracts\Auth\Authenticatable` インターフェースを実装したクラスである必要があります(Eloquent である必要はありません)。

```php
<?php

namespace Illuminate\Contracts\Auth;

interface Authenticatable
{
    /**
     * ユーザーのユニークIDのカラム名を取得する
     *
     * @return string
     */
    public function getAuthIdentifierName();

    /**
     * ユーザーのユニークIDを取得する
     *
     * @return mixed
     */
    public function getAuthIdentifier();

    /**
     * ユーザーのパスワードを取得する
     *
     * @return string
     */
    public function getAuthPassword();

    /**
     * "remember me" セッション用のトークンを取得する
     *
     * @return string
     */
    public function getRememberToken();

    /**
     * "remember me" セッションのトークンを設定する
     *
     * @param  string  $value
     * @return void
     */
    public function setRememberToken($value);

    /**
     * "remember me" トークンのカラム名を取得する
     *
     * @return string
     */
    public function getRememberTokenName();
}
```

またこの Authenticatable のデフォルト実装 trait として `Illuminate\Auth\Authenticatable` を利用することが出来ます。