LaravelでのJWT認証

# Laravel + tymon/jwt-auth にて認証つきAPIサーバーを構築する

いろいろな情報を参照させて頂き試したが、AccessTokenが切れた状態 & RefreshTokenの期限が切れていない状態でのトークンの更新ができなかったので、少し工夫したところも記載しています。

**※Laravel 8.6.0にて、routes/api.phpの書き方が変わっていたので追記しました。**

なお、ここで構築した環境は以下にてアクセスできますのでご参照ください。
GitHub: https://github.com/sankosc/apitest-server
サーバー: https://develop.sankosc.co.jp/apitest/


## 環境
- PHP 7.4.5
- Laravel Framework 7.10.3
- 10.4.11-MariaDB
- composerとnpmはインストール済み

## 準備
### Laravelのインストール
`$ composer create-project laravel/laravel [プロジェクト名] --prefer-dist`

### データベース作成
- PhpMyAdminで作成 ( utf8mb4_bin )
- .env編集
 `DB_DATABASE=[データベース名]`

### Laravelの認証機能を有効にする
`$ composer require laravel/ui`
`$ php artisan ui vue --auth`
`$ npm install`
`$ npm run dev`

### マイグレーション
`$ php artisan migrate`

# 認証API構築
### JWT認証ライブラリインポート
```
$ composer require tymon/jwt-auth
$ php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
$ php artisan jwt:secret
```
### ユーザーモデル編集
```php:app/User.php
<?php

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject
{
//
// ～～ 省略 ～～ 
//

    /**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}
```
### config/auth.phpを編集
```php:config/auth.php
'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
],

'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],
```
### route追加
```php:routes/api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

Route::post('login', 'Api\AuthController@login');
Route::get('refresh', 'Api\AuthController@refresh');
Route::group(['middleware' => ['jwt.auth']], function () {
    Route::post('logout', 'Api\AuthController@logout');
    Route::get('me', 'Api\AuthController@me');
});
```

**※Larave 8.6.0**

```php:routes/api.php
Route::post('login', [App\Http\Controllers\Api\AuthController::class, 'login']);
Route::get('refresh', [App\Http\Controllers\Api\AuthController::class, 'refresh']);
Route::group(['middleware' => ['jwt.auth']], function () {
    Route::post('logout', [App\Http\Controllers\Api\AuthController::class, 'logout']);
    Route::get('me', [App\Http\Controllers\Api\AuthController::class, 'me']);
});
```


### AuthController作成
```php:app/controllers/api/AuthController.php
<?php

namespace App\Http\Controllers\Api;

use Illuminate\Http\Request;
use Illuminate\Auth\Events\Registered;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;

class AuthController extends Controller
{
    /**
     * Create a new AuthController instance.
     *
     * @return void
     */
    public function __construct()
    {
//        $this->middleware('auth:api', ['except' => ['login']]);
    }

    /**
     * Get a JWT via given credentials.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function login()
    {
        $credentials = request(['email', 'password']);

        if (! $token = auth()->attempt($credentials)) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        return $this->respondWithToken($token);
    }

    /**
     * Get the authenticated User.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function me()
    {
        return response()->json(auth()->user());
    }

    /**
     * Log the user out (Invalidate the token).
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
        auth()->logout();

        return response()->json(['message' => 'Successfully logged out']);
    }

    /**
     * Refresh a token.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function refresh()
    {
        try {
            return $this->respondWithToken(auth()->refresh());
        } catch (\Tymon\JWTAuth\Exceptions\JWTException $e) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }
    }

    /**
     * Get the token array structure.
     *
     * @param  string $token
     *
     * @return \Illuminate\Http\JsonResponse
     */
    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth()->factory()->getTTL() * 60
        ]);
    }
}
```
### 確認
```
Invoke-RestMethod -Uri "http://localhost/api/login" -Method POST -Body "email=sample@sankosc.co.jp&password=sample123"
```
## テスト用のAPI作成
認証なしのget API helloとpost API echoを作成します。
### コントローラ作成
```php:app/controllers/api/TestController.php
<?php

namespace App\Http\Controllers\APi;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class TestController extends Controller
{
    public function hello()
    {
        return response()->json([
            'message' => 'hello'
        ]);
    }

    public function echo(Request $request)
    {
        return response()->json([
            'message' => $request->input('message')
        ]);
    }
}
```
### route追加
```php:routes/api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

Route::post('login', 'Api\AuthController@login');
Route::get('hello', 'Api\TestController@hello');
Route::post('echo', 'Api\TestController@echo');
Route::get('refresh', 'Api\AuthController@refresh');

Route::group(['middleware' => ['jwt.auth']], function () {
    Route::post('logout', 'Api\AuthController@logout');
    Route::get('me', 'Api\AuthController@me');
});
```
## AccessTokenのRefreshについて
AccessTokenが切れた状態かつ、RefreshTokenの期限が切れていない状態でのトークンの更新する方法がどうしてもわかりませんでした。
`jwt.refresh` をroutes/api.phpにて設定するように思えるのですがうまく動作しません。
なので、公式の[Quick start](https://jwt-auth.readthedocs.io/en/develop/quick-start/)で紹介されている方法から以下の２点を変更しています。

1. route/api.phpにてrefreshを認証の外におく
2. AuthControllerにてrefreshの処理を少し修正


```php:routes/api.php(変更前：refreshが認証の中にいる)
Route::post('login', 'Api\AuthController@login');
Route::get('hello', 'Api\TestController@hello');
Route::post('echo', 'Api\TestController@echo');

Route::group(['middleware' => ['jwt.auth']], function () {
    Route::post('logout', 'Api\AuthController@logout');
    Route::get('refresh', 'Api\AuthController@refresh');
    Route::get('me', 'Api\AuthController@me');
});
```
```php:routes/api.php(変更後：refreshが認証の外にいる)
Route::post('login', 'Api\AuthController@login');
Route::get('hello', 'Api\TestController@hello');
Route::post('echo', 'Api\TestController@echo');
Route::get('refresh', 'Api\AuthController@refresh');

Route::group(['middleware' => ['jwt.auth']], function () {
    Route::post('logout', 'Api\AuthController@logout');
    Route::get('me', 'Api\AuthController@me');
});
```
### AuthController.refresh修正
```php:app/controllers/api/AuthController.php（変更前）
    public function refresh()
    {
        return $this->respondWithToken(auth()->refresh());
    }
```
### AuthController.refresh修正
```php:app/controllers/api/AuthController.php（変更後）
    public function refresh()
    {
        try {
            return $this->respondWithToken(auth()->refresh());
        } catch (\Tymon\JWTAuth\Exceptions\JWTException $e) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }
    }
```
こうすることで、完全に認証がない状態だと401を返し、RefreshTokenの期限が切れる前であれば、更新したトークン返すようになります。