Laravel での tymon/jwt-auth による JWT トークンの自動更新

## 環境

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

## 概要

JWT トークンはトークン自体に有効期限が埋め込まれており、期限を更新するにはトークン自体を置き換えなければならない。
フロントで有効期限を管理してトークンのリフレッシュをせずに、スライディングセッションを実現したい。

トークンは Cookie でフロントに保存するものとする。サーバー側で期限切れを検出したら新しいトークンを Cookie に設定してあげる。

[トークンの有効期限(JWT_TTL:デフォルト1時間)](https://github.com/tymondesigns/jwt-auth/blob/9265f94a404d3d60ca19326d1fa9f366746937a2/config/config.php#L90)が切れても、[リフレッシュ有効期限(JWT_REFRESH_TTL:デフォルト2週間)](https://github.com/tymondesigns/jwt-auth/blob/9265f94a404d3d60ca19326d1fa9f366746937a2/config/config.php#L108)内であればそのトークンを用いて新しいトークンを発行することができる。

### ログインシーケンス

![](http://www.plantuml.com/plantuml/svg/NOqz2i9048NxFSMGIekWM0Qr4IoqY920ZSqInsI9cP7TDVQciRImzmWMjjuceDTGWyQVxl6-Z_TCgcWbjSmWO-8eqPmXMBr6OMfY4ZddZQ8O5d9fK8X7RLMaLf8iE4FDP1sG1XRFz0i3r3dgYfeg2b5PuFPV1NWmyOCGMakE5GaPMPLBYs-T-97vwTLwMUpAub9kJsLnlXsErzt-jTbrtO_LQRV17yyeGEiEc5E57bXEaNgDqJJytcYAEVxTtW40)

### トークン更新シーケンス

![](http://www.plantuml.com/plantuml/svg/pL4zImCn6DxpAruwwN1uYbDH5y51XmgjkBY4kxGNhfUK9AtWPkDGEtMmkWX5i0WYg3Y8uBy9T_qRtiLh_QYsZWO2xyVpFi_x8KFA6bZW22BEk1S87X5GeacGd6HaW7r-I5aDghWk2K9fM56FDZ1JKDWIdAa2O0dLpFgQB1FH8i9cfJKHiYXmDl80566tLAx0KftNA4FOKxI55Hd36Kzjdk4cSACxHh-QzhtHJwEpGTBfvPnhZlE1Md5TAEqSi39HpYRd8IL5K3maR7rXUx_ocMCH-UIxhFm7QvdNzHzMbLoUnjqNeuzD-yPedLOYN5Tv647wVjqS8kbCC3gv6YFdNCBeMwFxHjzPdMUZXxRAodLwSNTWtPcIFnuhEN-OcZnZiUt5ruz9_s8Mywgx37iCDrN01JqY_dijyr7sf-x-1W00)

### 実装

サーバー側でトークンの有効期限切れを検出し、リフレッシュをおこなって返すミドルウェアを作成する。不正なトークンや更新に失敗したりした場合はこのあとの通常処理で 401 になるので、何もしない。

```php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;

class RefreshToken extends BaseMiddleware
{
    public function handle($request, Closure $next)
    {
        $token = $newToken = null;
        try {
            $token = $this->auth->parseToken();
            $token->authenticate();
        } catch (TokenExpiredException $e) {
            // Token expired: try refresh
            try {
                $newToken = $token->refresh();
            } catch (JWTException $e) {
                // Refresh failed (refresh expired)
            }
        } catch (JWTException $e) {
            // Invalid token
        }

        $response = $next($request);
        if ($newToken) {
            // Send the refreshed token back to the client.
            $response->withCookie(cookie(
                'token',
                $newToken,
                config('jwt.refresh_ttl'), // minutes
                null, // path
                null, // domain
                $request->getScheme() === 'https', // secure
                true // httpOnly
            ));
        }
        return $response;
    }
}
```

このミドルウェアを有効にする。

```php
namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    ...
    protected $middlewareGroups = [
        'api' => [
            ...
            \App\Http\Middleware\RefreshToken::class, // JWTトークン更新 ※bindings の前に置く
            'bindings',
            ...
        ],
    ];
}
```

### ブラックリスト猶予期間を設定

リフレッシュがおこなわれると、即時古いトークンは使えなくなるため、フロントから同時に複数のリクエストをおこなった場合、一部のリクエストが失敗してしまう可能性がある。これを防止するため、一定時間は古いトークンも使用できるようにする。.env に[ブラックリスト猶予期間(JWT_BLACKLIST_GRACE_PERIOD)](https://github.com/tymondesigns/jwt-auth/blob/9265f94a404d3d60ca19326d1fa9f366746937a2/config/config.php#L190)を設定しておく。(例: 15秒)

```file
JWT_BLACKLIST_GRACE_PERIOD=15
```