tymon/jwt-auth を拡張するパッケージを公開して不満が解消した

## はじめに
`Laravel`でAPIサーバーを開発する際、認証に`JWT`を使うことが増えてきました。
ライブラリには決まって、[tymon/jwt-auth](https://packagist.org/packages/tymon/jwt-auth)を使うのですが、いくつか不満があって、毎回カスタマイズしていたので、拡張ライブラリを作って公開しました。

- https://packagist.org/packages/imunew/tymon-jwt-auth
- https://github.com/imunew/tymon-jwt-auth

## `tymon/jwt-auth`に対する不満
### クエリパラメータ、リクエストボディにある`token`もJWTトークンだと認識してしまう
「[Laravelで作ったらすべて解決ではないよ？](https://qiita.com/imunew/items/ff2b61eb62b5ac4ffac7)」という記事でも書いたのですが、`tymon/jwt-auth`は、クエリパラメータ、リクエストボディにある`token`もJWTトークンだと認識してしまいます。
正直、`QueryString`、`InputSource`、`RouteParams`は使うことないのでは？と考えてます。

```vendor/tymon/jwt-auth/src/Providers/AbstractServiceProvider.php
    protected function registerTokenParser()
    {
        $this->app->singleton('tymon.jwt.parser', function ($app) {
            $parser = new Parser(
                $app['request'],
                [
                    new AuthHeaders,
                    new QueryString,
                    new InputSource,
                    new RouteParams,
                    new Cookies($this->config('decrypt_cookies')),
                ]
            );

            $app->refresh('request', $parser, 'setRequest');

            return $parser;
        });
    }
```

### `'token'`という名前を設定ファイルから変更できない
各`Parser`の`setKey`関数で名前を変更することはできますが、できれば、設定ファイルで環境ごとに変えられるようにしたいですよね？
[Add ability to provide custom cookie name from config by SiebeVE · Pull Request #1933](https://github.com/tymondesigns/jwt-auth/pull/1933)でも、`setKey`する方法が紹介されていますが、設定ファイルに切り出す気はなさそうです。

```vendor/tymon/jwt-auth/src/Http/Parser/KeyTrait.php
namespace Tymon\JWTAuth\Http\Parser;

trait KeyTrait
{
    /**
     * The key.
     *
     * @var string
     */
    protected $key = 'token';

    /**
     * Set the key.
     *
     * @param  string  $key
     *
     * @return $this
     */
    public function setKey($key)
    {
        $this->key = $key;

        return $this;
    }
```

### `'tymon.jwt.parser'`を拡張
以上を踏まえて、[imunew/tymon-jwt-auth](https://packagist.org/packages/imunew/tymon-jwt-auth)では、下記のように、拡張しました。
`QueryString`、`InputSource`、`RouteParams`は削除。
`Cookies`だけにして、`AuthHeaders`は任意で追加可能にしました。

```src/Providers/ServiceProvider.php

    private function resetParserChain()
    {
        $this->app->extend('tymon.jwt.parser', function (Parser $parser) {
            $chain = [
                (new Cookies(config('jwt.decrypt_cookies')))
                    ->setKey(config('jwt-auth.cookie.key'))
            ];
            if (config('jwt-auth.auth-headers.enabled')) {
                $chain[] = new AuthHeaders();
            }
            return $parser->setChain($chain);
        });
    }
```

```config/jwt-auth.php
return [
    "cookie" => [
        'key' => env('JWT_AUTH_COOKIE_KEY', 'auth-token')
    ],
    "auth-header" => [
        'enabled' => env('JWT_AUTH_AUTH_HEADER_ENABLED', false)
    ]
];
```

## 追加機能
今回、`'tymon.jwt.parser'`を拡張しただけではなく、新たに２つの機能を追加しました。

### `AuthResource`
ログインAPIが返すリソースとして使えそうな、[API Resource](https://laravel.com/docs/6.x/eloquent-resources)を実装しました。
`JWT`トークンを`Cookie`にセットします。

```src/Resources/AuthResource.php
class AuthResource extends JsonResource
{
    /** @var string|null */
    public static $wrap = null;

    /**
     * {@inheritDoc}
     */
    public function toArray($request)
    {
        return [];
    }

    /**
     * @param Request $request
     * @return JsonResponse
     */
    public function toResponse($request)
    {
        $response = parent::toResponse($request);
        return HttpHelper::respondWithCookie($request, $response, $this->resource->accessToken);
    }
}
```

### `RefreshJwtToken`
今回の目玉機能だと思っているんですが、JWTトークンの有効期限が切れたら自動でリフレッシュしてくれるミドルウェアを実装しました。
尚、このミドルウェアのアイデアは、[Laravel での tymon/jwt-auth による JWT トークンの自動更新](https://qiita.com/yh1224/items/bd00e85d5c53350e93ca)から得ました。
@yh1224 さん、ありがとうございました。

```src/Middleware/RefreshJwtToken.php
class RefreshJwtToken extends BaseMiddleware
{
    /**
     * @param Request $request
     * @param Closure $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        $token = null;
        try {
            $token = $this->auth->parseToken();
            $token->authenticate();
        } catch (TokenExpiredException $e) {
            // Token expired: try refresh
            try {
                return $this->refreshJwtToken($token, $request, $next);
            } catch (JWTException $e) {
                // Refresh failed (refresh expired)
            }
        } catch (JWTException $e) {
            // Invalid token
        }
        return $next($request);
    }

    /**
     * @param JWT $jwt
     * @param Request $request
     * @param Closure $next
     * @return JsonResponse|Response
     */
    private function refreshJwtToken(JWT $jwt, Request $request, Closure $next)
    {
        $newToken = $jwt->refresh();
        $request->cookies->set(config('jwt-auth.cookie.key'), $newToken);
        $response = $next($request);
        return HttpHelper::respondWithCookie($request, $response, $newToken);
    }
}
```

`RefreshJwtToken`を有効にするには、以下のように、`Kernel.php`に設定を追加します。

```app/Http/Kernel.php

    protected $middlewareGroups = [
        'web' => [
            // ...
        ],

        'api' => [
            'throttle:60,1',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            \Imunew\JWTAuth\Middleware\RefreshJwtToken::class, // ここを追加
        ]
    ];

    protected $middlewarePriority = [
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \Imunew\JWTAuth\Middleware\RefreshJwtToken::class, // ここを追加
        \App\Http\Middleware\Authenticate::class, // この行よりも上に追加する
        \Illuminate\Routing\Middleware\ThrottleRequests::class,
        \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \Illuminate\Auth\Middleware\Authorize::class,
    ];
```

`$middlewarePriority`の`\App\Http\Middleware\Authenticate::class`よりも前に、`RefreshJwtToken`を追加しないと、先に`Authenticate`が実行されてしまい、`401`になってしまうので注意してください。

### 追記（2021-01-14）
`Laravel 7.x`以降、`App\Http\Kernel`クラスから`$middlewarePriority`が削除されました。
（詳しくは、以下の`pull-request`を参照）

- https://github.com/laravel/laravel/pull/5181

対応としては、以下の公式ドキュメントにあるように、基底クラス（`Illuminate\Foundation\Http\Kernel\Kernel`）からコピーした後、`\Imunew\JWTAuth\Middleware\RefreshJwtToken::class`を挿入します。

- https://laravel.com/docs/8.x/middleware#sorting-middleware

> Rarely, you may need your middleware to execute in a specific order but not have control over their order when they are assigned to the route. In this case, you may specify your middleware priority using the $middlewarePriority property of your app/Http/Kernel.php file. This property may not exist in your HTTP kernel by default. If it does not exist, you may copy its default definition below:

```php
    protected $middlewarePriority = [
        \Illuminate\Cookie\Middleware\EncryptCookies::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \Imunew\JWTAuth\Middleware\RefreshJwtToken::class, // ここを追加
        \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,
        \Illuminate\Routing\Middleware\ThrottleRequests::class,
        \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \Illuminate\Auth\Middleware\Authorize::class,
    ];
```

## おわりに
`2020-03-05`に、ついに、[1.0.0](https://github.com/tymondesigns/jwt-auth/releases/tag/1.0.0)がリリースされた`tymon/jwt-auth`ですが、`github`の`issues`など見ていても、`Parser`周りは特に不満があるかと思います。
そんなときは、[imunew/tymon-jwt-auth](https://packagist.org/packages/imunew/tymon-jwt-auth)も合わせて、`composer require`していただければ幸いです。
`pull-requests`または`issues`お待ちしております。
ではでは。