LaravelPasportでアクセストークンに有効期限を設定したお話

LaravelPassportを使ってSPAを開発するにあたって、LaravelPassportのアクセストークンの制限時間に少し癖があったので共有したいと思います!!

laravelPassportの使い方は以前記事を書いたのでこちらをご参考ください

[vue.js (Nuxt.js)とlaravelPassportを使ってSPAでのログイン機能を実装してみた](https://qiita.com/frostnday/items/2268c825923310571d93)


#LaravelPassportに制限時間を設定する
では早速アクセストークンに制限時間を設定してみたいと思います。

といっても、アクセストークン自体に制限時間を設定するのは簡単です。
公式の手順にも記載のある通り

``` php

namespace App\Providers;

class AuthServiceProvider extends ServiceProvider
{
    public function boot()
    {
       $this->registerPolicies();
       Passport::routes();

       // アクセストークンの制限時間を15日に設定している
       Passport::tokensExpireIn(Carbon::now()->addDays(15));

       // リフレッシュトークンの制限時間を30日に設定している
       Passport::refreshTokensExpireIn(Carbon::now()->addDays(30));
    }
}

```
のようにbootの箇所で

```php
tokensExpireIn(Carbon::now()->addDays(15))
```
と指定してやれば、アクセストークン生成時に
`oauth_access_tokens`テーブルの`expires_at`カラムに、ここで設定した制限時間がセットされます。


ここでセットした有効期限を過ぎてしまった、つまり無効になったアクセストークンを使用したアクセスを試みた場合。。。普通に考えれば有効期限が切れているので無効になるはずです

**しかし！！！！**


無効にはならず、普通にアクセスできてしまいました。。。

どうやら調べてみると、アクセストークンが有効かどうかは`oauth_access_tokens`テーブルの`revoked`カラムにフラグが立っているかどうかでのみ判断しているようです


有効期限を過ぎてしまったトークンに対して`revoked`のフラグを立てる設定は存在するみたいですが
即反映はされない模様。。。それじゃ有効期限の意味ないんじゃ。。。

というわけで、自分で作ることにしました

#アクセストークンの有効期限のチェックする

##ルーティング
まずチェック用のルーティングを定義します

```php
Route::get('/checkToken', 'AuthController@checkToken')->middleware('auth:api');
```

ここで定義しているmiddlewareは、ログイン認証を通っていないと通れないようにするもので、ヘッダーにLaravelPassportで生成したアクセストークンがセットされていないと無効になってしまいます。

今回は有効期限が切れていても`revoked`にフラグが立っていないため、実質アクセストークン自体は有効である　と言う状況を利用します。


##Controller

次にControllerにcheckTokenメソッドを実装します

```php


use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use League\OAuth2\Server\ResourceServer;
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use App\Models\OauthAccessToken;
use Carbon\Carbon;

class AuthController extends Controller
{

    public function checkToken(ResourceServer $server, Request $request)
    {
        // ヘッダーのアクセストークンを取得
        $psr = (new DiactorosFactory)->createRequest($request);
        $psr = $server->validateAuthenticatedRequest($psr);
        $reqToken = $psr->getAttribute('oauth_access_token_id');

        // 有効期限が切れていないかチェック
        $accessToken = OauthAccessToken::where('id', $reqToken)
                                       ->where('expires_at', '>', Carbon::now())
                                       ->first();
        //デバッグ用
        if ($accessToken) {
            logger('ok');
        } else {
            logger('no');
        }
    }
}
```

ヘッダーに含まれているアクセストークンを取得し、それをDBに保存されている値と比較できる形に変換します。

```php
// ヘッダーのアクセストークンを取得
$psr = (new DiactorosFactory)->createRequest($request);
$psr = $server->validateAuthenticatedRequest($psr);
$reqToken = $psr->getAttribute('oauth_access_token_id');
```


次にアクセストークンが保存されている`oauth_access_tokens`テーブルに対して
取得したアクセストークンと有効期限範囲内で対象データが存在するかどうか確認します

```php
// 有効期限が切れていないかチェック
$accessToken = OauthAccessToken::where('id', $reqToken)
                               ->where('expires_at', '>', Carbon::now())
                               ->first();
```



これで有効期限をチェックするメソッドを実装できました！
有効期限が切れていた場合、そのアクセストークンの有効期限が切れているよ〜
という状態を知ることができるようになりました

では切れていた場合、どうするのでしょうか？
今回はリフレッシュトークンという仕組みを利用して、アクセストークンを再発行しちゃいます！

#リフレッシュトークンを使う

##リフレッシュトークンってなんぞ？

リフレッシュトークンとは、新しいアクセストークンを発行する際に使用されるトークンです

リフレッシュトークンはアクセストークンと紐付いています。

アクセストークンの有効期限が切れたとしても、それに紐付いたリフレッシュトークンを使うことで、ユーザーにもう一度ログイン処理をさせることなく、内部的に新しいアクセストークンを発行できます。

新しいトークンが発行されることにより、アクセストークンが漏洩した場合の不正操作のリスクを減らすことができます。

##LaravelPassportで使ってみる

laravelPassportではアクセストークンを取得した際に、一緒にリフレッシュトークンも発行され、レスポンスとして返ってきます

取得したリフレッシュトークンを以下のようにアクセストークンを取得する時と同じエンドポイントに対してPOSTすることで、アクセストークンを再発行することができます

```javascript

const postData = {
  grant_type: 'refresh_token',
  client_id: ２, //２を指定する
  client_secret: '12345678sample', //laravelPassoprtで発行したシークレットキー
  refresh_token: ''azsxdcfvgbhn, // 取得したリフレッシュトークン
  scope: '*',
};

axios.post('http://localhost:1010/api/oauth/token', postData);
```

戻り値として、新しいアクセストークンとリフレッシュトークンが返ってきます


#再発行までのフロー
再発行までのフローをまとめると

①　ID/PASSでアクセストークンとリフレッシュトークンをLaravelPassportから取得

②　チェック用のエンドポイントに対して期限切れかどうかを問い合わせしに行く

③　期限切れだったら①で取得したリフレッシュトークンを使用して再度アクセストークンを発行する


といった流れになります。②の処理を認証が必要なページのレンダリング前に挟むことで、
アクセストークンの有効期限切れなのにアクセスできしまったと言った状況を防ぐことができます


全くログインしておらず、リフレッシュトークンの有効期限の方も切れてしまっていたら、その時はユーザーに再度ログインしてもらう

といったフローにすることで、ユーザービリティを損なうことなく、不正アクセスのリスクを減らすことができると思います。


間違っている点、もっと良いやり方がありましたらご指摘頂けると嬉しいです