Laravel tymon/jwt-auth のブラックリストをデータベースに保存する

Laravel/Lumen 用の JWT 認証パッケージである [tymon/jwt-auth](https://github.com/tymondesigns/jwt-auth) では、発行した有効なトークンを保持するのではなく、ログアウトしたりリフレッシュされた古いトークンをブラックリストという形で保存しておく。デフォルトではキャッシュに保存されるので、Redis などで共有していないとスケールアウトできない。ということで、データベースに保存してみる。

## 環境

- Laravel 6.0
- [tymon/jwt-auth](https://github.com/tymondesigns/jwt-auth) 1.0.0-rc.5

## 手順

### テーブル jwt_auth_storage を作成

database/migrations 配下に定義してマイグレーションする。

```php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateJwtAuthStorageTable extends Migration
{
    public function up(): void
    {
        Schema::create('jwt_auth_storage', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->timestamps();
            $table->timestamp('expires_at')->nullable();
            $table->string('key')->index();
            $table->string('value');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('jwt_auth_storage');
    }
}
```

### jwt_auth_storage テーブルの Eloquent Model を作成

```php
<?php
namespace App\Model;

use Illuminate\Database\Eloquent\Model;

class JwtAuthStorage extends Model
{
    protected $table = 'jwt_auth_storage';

    protected $fillable = [
        'key', 'value', 'expires_at',
    ];

    protected $dates = ['expires_at'];
}
```

### Storage を継承したアクセスクラスを作成

Tymon\JWTAuth\Contracts\Providers\Storage を継承し、各メソッドを実装したクラスを作成する。デフォルトでは Tymon\JWTAuth\Providers\Storage\Illuminate::class が使用されるので、これを参考にする。

```php
<?php
namespace App\Repository;

use App\Model\JwtAuthStorage;
use DateTime;
use Tymon\JWTAuth\Contracts\Providers\Storage;

class JwtAuthStorageRepository implements Storage
{
    protected $jwtAuthStorageModel;

    public function __construct(JwtAuthStorage $jwtAuthStorageModel)
    {
        $this->jwtAuthStorageModel = $jwtAuthStorageModel;
    }

    public function add($key, $value, $minutes): void
    {
        $expiresAt = (new DateTime('now'))->modify('+ ' . $minutes . ' minutes');
        $this->jwtAuthStorageModel->newQuery()
            ->create([
                'key' => $key,
                'value' => serialize($value),
                'expires_at' => $expiresAt,
            ]);
    }

    public function forever($key, $value): void
    {
        $this->jwtAuthStorageModel->newQuery()
            ->create([
                'key' => $key,
                'value' => serialize($value),
            ]);
    }

    public function get($key)
    {
        $now = new DateTime('now');
        $data = $this->jwtAuthStorageModel->newQuery()
            ->where('key', $key)
            ->where('expires_at', '>', $now)
            ->orderBy('expires_at', 'desc')
            ->first();
        if ($data) {
            return unserialize($data->value);
        } else {
            return null;
        }
    }

    public function destroy($key): bool
    {
        return !!$this->jwtAuthStorageModel->newQuery()
            ->where('key', $key)
            ->delete();
    }

    public function flush()
    {
    }
}
```

### 設定ファイルを publish

設定を変更するため、config/jwt.php をプロジェクトに展開する。

```bash
$ php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
```

### 設定

設定ファイル(config/jwt.php)の provider.storage に、カスタマイズしたアクセスクラスを指定する。ブラックリストの読み書きはこのクラスを経由しておこなわれる。

```php
<?php
return [
    ...
    'providers' => [
        ...
        'storage' => App\Repository\JwtAuthStorageRepository::class,
    ],
];
```