Laravel tymon/jwt-auth による JWT 認証

## 環境

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

## インストール手順

参考) [jwt-auth > Docs > Laravel Installation](https://jwt-auth.readthedocs.io/en/develop/laravel-installation/)

### インストール

```shell
$ composer require tymon/jwt-auth
```

### 設定ファイルを publish

設定を変更しない場合は不要。

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

### シークレット作成

```shell
php artisan jwt:secret
```

.env に秘密鍵 JWT_SECRET が追記される。

### User モデルに JWTSubject を実装

参考) [jwt-auth > Docs > Quick Start](https://jwt-auth.readthedocs.io/en/develop/quick-start/)

Tymon\JWTAuth\Contracts\JWTSubject を実装する。

app/User.php

```php
<?php
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject
{
    ...

    public function getJWTIdentifier()
    {
        // JWT トークンに保存する ID を返す
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        // JWT トークンに埋め込む追加の情報を返す
        return [];
    }
}
```

### JWTGuard を使うように設定

参考) [Laravel > Authentication > Adding Custom Guards](https://laravel.com/docs/5.8/authentication#adding-custom-guards)

config/auth.php

```php
<?php
'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
],
...
'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],
```

## 認証エンドポイントの実装

まずは公式ドキュメントの [Quick start](https://jwt-auth.readthedocs.io/en/develop/quick-start/) にある AuthController をそのまま実装して使ってみる。

### 認証API

- POST /api/auth/login : ログイン
- POST /api/auth/logout : ログアウト
- POST /api/auth/refresh : トークンをリフレッシュ
- POST /api/auth/me : ユーザー情報取得

### 認証

ログインAPIにユーザー名/パスワードを渡し、アクセストークンを取得する。

```shell
$ curl -X POST -H "Accept: application/json" \
    -F "email=test@example.jp" \
    -F "password=password" \
    http://localhost/api/auth/login | jq
{
  "access_token": "(JWTトークン)",
  "token_type": "bearer",
  "expires_in": 3600
}
```

認証エラー時

```shell
$ curl -X POST -H "Accept: application/json" \
    -F "email=test@example.jp" \
    -F "password=incorrect" \
    http://localhost/api/auth/login | jq
{
  "message": "Unauthenticated."
}
```

### ユーザー情報取得

認証つきAPI要求時は Authorization ヘッダにアクセストークンを付与する。

```shell
$ curl -X POST -H "Accept: application/json" \
    -H "Authorization: Bearer (JWTトークン)" \
    http://localhost/api/auth/me | jq
{
  "id": 1,
  "name": "Test User",
  "email": "test@example.jp",
  "email_verified_at": null,
  "created_at": "2019-05-10 06:51:16",
  "updated_at": "2019-05-10 06:51:16"
}
```

### リフレッシュ

アクセストークンには有効期限があるので、それを超えて使う場合は新しいトークンを取得する必要がある。再度認証をおこなわずに新しいトークンに更新するには、リフレッシュAPIを使用する。

```shell
$ curl -X POST -H "Accept: application/json" \
    -H "Authorization: Bearer (古いJWTトークン)" \
    http://localhost/api/auth/refresh | jq
{
  "access_token": "(新しいJWTトークン)",
  "token_type": "bearer",
  "expires_in": 3600
}
$ curl -X POST -H "Accept: application/json" \
    -H "Authorization: Bearer (古いJWTトークン)" \
    http://localhost/api/auth/refresh | jq
{
  "message": "Unauthenticated."
}
```

デフォルトの有効期限は発行から1時間、リフレッシュ有効期限は発行から2週間(この期限内であれば無効になったトークンを使って新しいトークンを取得することができる)。有効期限を変更する場合は以下の設定を .env に追記する。

|設定|デフォルト値|説明|
|:--|:--|:--|
|JWT_TTL|60 (1時間)|トークンの有効期限 (分)|
|JWT_REFRESH_TTL|20160 (2週間)|トークンのリフレッシュ有効期限 (分)|

リフレッシュをおこなうとすぐに古いトークンは使えなくなる。リフレッシュと並列に古いトークンでアクセスをおこなうと更新直後に認証が通らなくなってしまう。古いトークンをすぐに失効させたくない場合は、猶予期間を .env に設定する。

|設定|デフォルト値|説明|
|:--|:--|:--|
|JWT_BLACKLIST_GRACE_PERIOD|0 (無効)|ブラックリスト猶予期間 (秒)|

### ログアウト

ログアウトするとそのアクセストークンは使えなくなる。jwt-auth では発行したトークンを管理しているわけではなく、逆に無効化したトークンをキャッシュに保持して拒否する実装となっているため、キャッシュをクリアすると再度使用できるようになるので注意。リフレッシュで無効になったトークンも同様。

```shell
$ curl -X POST -H "Accept: application/json" \
    -H "Authorization: Bearer (JWTトークン)" \
    http://localhost/api/auth/logout
{"message":"Successfully logged out"}
$ curl -X POST -H "Accept: application/json" \
    -H "Authorization: Bearer (JWTトークン)" \
    http://localhost/api/auth/me
{"message":"Unauthenticated."}
```

## セキュリティの確認

### JWTトークン

JWTトークンはユーザーIDを含む JSON をエンコードして署名をつけただけのもの(`(ヘッダー).(ペイロード).(署名)`という構成)。サーバー側に設定された秘密鍵を持たないと改竄はできないが、中身を知ることはできる。

受信したJWTトークンをデコードしてみる。

```shell
$ echo "(JWTトークンのヘッダー)" | base64 --decode - | jq
{
  "typ": "JWT",
  "alg": "HS256"
}
$ echo -n "(JWTトークンのペイロード)" | base64 --decode - | jq
{
  "iss": "http://localhost/api/auth/login",
  "iat": 1557471077,
  "exp": 1557474677,
  "nbf": 1557471077,
  "jti": "qyDgabCpUk6atauK",
  "sub": 9,
  "prv": "87e0af1ef9fd15812fdec97153a14e0b047546aa"
}
```

`"sub"` がユーザーIDをあらわしている。

### 署名なしのJWTトークン

JWTトークンの署名アルゴリズムは選択することができ、"none" (署名なし) もJWTトークンとしては Valid である。

署名アルゴリズムに "none" を指定して、JWT トークンをねつ造してみる。

```shell
$ echo -n '{"typ":"JWT","alg":"none"}' | base64
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0=
$ echo -n '{"iss":"http:\/\/localhost:8000\/api\/auth\/login","iat":1557471077,"exp":1557474677,"nbf":1557471077,"jti":"qyDgabCpUk6atauK","sub":1,"prv":"87e0af1ef9fd15812fdec97153a14e0b047546aa"}' | base64
eyJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6ODAwMFwvYXBpXC9hdXRoXC9sb2dpbiIsImlhdCI6MTU1NzQ3MTA3NywiZXhwIjoxNTU3NDc0Njc3LCJuYmYiOjE1NTc0NzEwNzcsImp0aSI6InF5RGdhYkNwVWs2YXRhdUsiLCJzdWIiOjEsInBydiI6Ijg3ZTBhZjFlZjlmZDE1ODEyZmRlYzk3MTUzYTE0ZTBiMDQ3NTQ2YWEi
```

このJWTトークンで認証してみる。

```shell
$ curl -X POST -H "Accept: application/json" -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6ODAwMFwvYXBpXC9hdXRoXC9sb2dpbiIsImlhdCI6MTU1NzQ3MTA3NywiZXhwIjoxNTU3NDc0Njc3LCJuYmYiOjE1NTc0NzEwNzcsImp0aSI6InF5RGdhYkNwVWs2YXRhdUsiLCJzdWIiOjEsInBydiI6Ijg3ZTBhZjFlZjlmZDE1ODEyZmRlYzk3MTUzYTE0ZTBiMDQ3NTQ2YWEifQ." http://localhost/api/auth/me
{"message":"Unauthenticated."}
```

大丈夫っぽい。

## 参考

- [jwt-auth > Docs](https://jwt-auth.readthedocs.io/en/develop/)
- [LaravelでJWT(jwt-auth利用）- Qiita](https://qiita.com/zaburo/items/176f907ea46767283785)