Laravel7 + JWT-Authでログイン認証機能を実装

# まえおき
認証って色々種類があって何を使うべきなのか迷いますよね...
・Laravel Passport
・Sanctum 
・laravel/ui --auth
・Firebase Authentication
・AWS Cognito
・JWT-Auth
etc...

その中でも今回はJWT-Authを導入してみます。
Laravel+VueSPAのリポジトリ分離パターンや
Laravel+ReactNative等のモバイルに使えるかと思います
Laravel+Bladeの場合は使う意味はないです

# JWT-Auth設定

#### JWT-Authインストール

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



#### 設定ファイルをpublish


```
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
```
JWT-Authの設定ファイル(config/jwt.php)を生成
TTLを設定したい場合はこのファイルを見てみると良いです

#### シークレットキー発行

```
php artisan jwt:secret
```

.envに暗号化する際に使用するシークレットキーが定義される

#### Usersモデル編集


```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 // 追加
{
    use Notifiable;

    protected $fillable = [
        'name', 'email', 'password',
    ];

    protected $hidden = [
        'password', 'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function getJWTIdentifier() // 追加
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims() // 追加
    {
        return [];
    }
}
```

追加した2つのメソッドは定義しないとエラーになってしまう
(JWTSubjectがインターフェースなので実装する必要がある)

#### 認証ガード変更

``` config/auth.php
<?php

return [

    'defaults' => [
        'guard' => 'api', // 変更
        'passwords' => 'users',
    ],


    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'jwt', // 変更
            'provider' => 'users',
            'hash' => false,
        ],
    ],

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],


    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

    'password_timeout' => 10800,
];
```

# 認証API作成

#### Route定義

``` routes/api.php
<?php

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

Route::group(['middleware' => 'api'], function () {
    Route::post('/login', 'AuthController@login');
    Route::post('/register', 'AuthController@register');
    Route::post('/refresh', 'AuthController@refresh');
});

Route::group(['middleware' => 'auth:api'], function () {
    Route::post('/logout', 'AuthController@logout');
    Route::post('/mypage', 'MypageController@index');
});
```

login, registerは認証する必要ないのでmiddlewareはapiのみ
mypageはログインしていないと閲覧ができないdashboardてきなものです

#### AuthController作成

```
php artisan make:controller AuthController
```

```php:AuthController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use App\User;

class AuthController extends Controller
{
    public function login(Request $request)
    {
        $validator = Validator::make($request->only(["email", "password"]), [
            'email' => 'required|email',
            'password' => 'required|string',
        ]);

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

        return response()->json(['token' => $token]);
    }

    public function register(Request $request)
    {
        $validator = Validator::make($request->only(["name", "email", "password", "password_confirmation"]), [
            'name' => 'required|max:10|unique:users',
            'email' => 'required|email|unique:users|max:50',
            'password' => 'required|confirmed|string|min:6|max:30',
        ]);

        if ($validator->fails()) {
            return response()->json($validator->errors(), 422);
        }

        $user = User::create(array_merge(
            $validator->validated(),
            ['password' => bcrypt($request->password)]
        ));

        return response()->json([
            'message' => 'success'
        ], 201);
    }

    public function logout()
    {
        auth()->logout();
        return response()->json(['message' => 'success']);
    }

    public function refresh()
    {
        $token = auth()->refresh();
        return response()->json(['token' => $token]);
    }
}
```

ログイン成功時にtokenだけでなくユーザー情報も返してフロントで保持しておく場合、

```php
$name = auth()->user()->name
```

上記の様にユーザー情報(上の例はname)を取得することができるのでresponse()->json内で記述してあげればよいです

#### MypageController作成

```
php artisan make:controller MypageController
```

```php:MypageController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class MypageController extends Controller
{
    public function index()
    {
        return response()->json(auth()->user());
    }
}
```
認証に成功したユーザー(ログイン中)の情報をただ返しているだけです...

# POSTMANでAPI検証

#### 新規登録
![スクリーンショット 2021-03-12 2.04.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535228/121177c5-33e1-efa0-490f-8c3107d550d1.png)


#### ログイン
![スクリーンショット 2021-03-12 2.08.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535228/2dc4e25d-69ab-783d-a09d-f22bcd357d7f.png)

#### マイページ(未認証時)
![スクリーンショット 2021-03-12 2.10.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535228/1e8d2eec-9059-3fa6-9a87-0b84c0379cf6.png)
ちゃんと401が返ってきています
**※HeadersにAccept application/jsonを追加しないとloginルートにリダイレクトしようとします**

#### マイページ(認証済 トークン付与)
![スクリーンショット 2021-03-12 2.13.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535228/7d799a29-0e31-8e51-768f-986abde13d04.png)

POSTMANでトークンをヘッダーに含めるにはAuthorizationタブから設定します
POSTMANのバージョンによってやり方が異なる場合があるので注意

#### これで基本的な認証システムが完成しました

# おまけ

#### JWT_TTL,  JWT_REFRESH_TTLとは
**JWT_TTL=トークンの有効期限**
**デフォルト: 60分**

**JWT_REFRESH_TTL=トークンを更新できる有効期限**
**デフォルト: 2週間**

上記のデフォルト設定値の場合
アクセストークンの有効期限(60分)が切れてしまうと middleware api:authを指定しているルートは401を返す
有効期限切れのトークンを使用して新しいトークンの発行(refresh)が2週間-60分の間であれば可能

60分経過する度にログインを毎回させるのはユーザーのストレスでしかないので
うまくトークンを更新するロジックをフロントで実装する必要があります
(例えば認証が必要なAPIから401が返ってきた場合、自動でトークンの更新を試行するようにします)


#### APIのURIプレフィックス変更 (api→v1)

```php:app/providers/RouteServiceProvider.php
    protected function mapApiRoutes()
    {
        Route::prefix('v1') // 変更
            ->middleware('api')
            ->namespace($this->namespace)
            ->group(base_path('routes/api.php'));
    }
```
デフォルトだと「test.com/api/login」の様に"api"が自動的にURIの先頭に付与されます
これだとAPIサーバーをサブドメインで切っている(api.test.com/loginの様な)と困るので上記で解決
v1,v2とかのAPIバージョニングを表すプレフィックスはrouteファイルで設定しても勿論良い

#### ローカル開発環境時のChromeブラウザによるCORSエラー
axiosでBASE_URLを設定する際にlocalhost:8000/apiだとCORSエラーが発生してしまうので
下記の様に設定すると発生しなくなる

```.env.development
NODE_ENV='development'
VUE_APP_API_BASE_URL='http://127.0.0.1:8000/api'
```