Laravel5.7で標準？のAPI認証を試す


LaravelのAPIに認証を付ける方法としてはPassportやjwt-auth等の外部ライブラリを利用する方法がありますが、「そもそも標準の認証機能って何よ」ということで試してみます。

### 標準の認証機能

あまり記述が見当たりませんが、認証用のテーブルにapi_tokenってカラムを足して、その値をHTTPヘッダのAuthorization: Bearer の値として送ってやるのが標準？みたいですね（違ってたらご指摘を）。

Guardで下記のように定義されているやつ。

```php
'api' => [
        'driver' => 'token',
        'provider' => 'users',
    ],
```

## 準備と実装

### migrationファイルの編集

では、テーブルを作成します。標準で用意されているmigrationファイルにapi_tokenを追加します。
60文字にしていますが、それより短くても長くてもOKなようです。

```diff
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{

    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
+           $table->string('api_token',60)->unique()->nullable();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}
```

### migrate

migrateします。

```bash
php artisan migrate
```

### ユーザーの追加

認証用のユーザー（token）を追加します。
めんどいのでtinkerで追加します。api_tokenには60文字の乱数（乱文字）をセット。

```bash
$ php artisan tinker
Psy Shell v0.9.8 (PHP 7.2.7 — cli) by Justin Hileman
>>>
>>>
>>> $user = new App\User;
=> App\User {#2898}
>>> $user->name = 'user1';
=> "user1"
>>> $user->email = 'user1@test.com';
=> "user1@test.com"
>>> $user->password = Hash::make('testtest');
=> "$2y$10$o3Grx73i7PaNV1ptGyukDetQM0FJjxYbXmHdWyNSH0TYK9m2ssf2e"
>>> $user->api_token = str_random(60);
=> "p1zQhxuL2EaNUVUZiMYYPi1iXLWW9R8RLwbMFHjBJmxLgUkkOn1DZApuuobo"
>>> $user->save();
=> true
```

準備は以上です。サーバを立ち上げておきます。

```bash
php artisan serve
```

## 動作確認

### リクエストしてみる

リクエストもめんどいのでcurlで。
api_tokenに登録した値をAuthorization: Bearer {token}という形で添付して送ります。
APIとしては標準で記述されている認証付きAPIルートである/api/userを利用してみます（リクエストしたユーザの情報を返す）。

>API処理としてLaravelに認識させるためにAccept: application/jsonヘッダも追加しています。

```bash
curl -H 'Accept: application/json' -H 'Authorization: Bearer p1zQhxuL2EaNUVUZiMYYPi1iXLWW9R8RLwbMFHjBJmxLgUkkOn1DZApuuobo' http://localhost:8000/api/user
```

### レスポンス

あっさりOK。
でも、tokenの内容が見えてるのでいちおう隠します。

```bash
{"id":1,"name":"user1","email":"user1@test.com","email_verified_at":null,"created_at":"2018-10-05 22:02:33","updated_at":"2018-10-05 22:02:33","api_token":"p1zQhxuL2EaNUVUZiMYYPi1iXLWW9R8RLwbMFHjBJmxLgUkkOn1DZApuuobo"}
```

### hiddenにapi_tokenを追加

User.phpのhiddenにapi_tokenも追記してやります。

```bash
protected $hidden = [
    'password', 'remember_token', 'api_token',
];
```

見えなくなりました。

```bash
{"id":1,"name":"user1","email":"user1@test.com","email_verified_at":null,"created_at":"2018-10-05 22:02:33","updated_at":"2018-10-05 22:02:33"}
```

### 認証エラーを発生させてみる

tokenの値を少し変えてリクエストしてみます。
ハンドリングされていないエラーが出ます。標準では認証がNGをだとloginに飛ばされるようになっていますが、「loginなんて無いぞ」と言われています。make:authで必要な設定をしてもエラーは消えますが、APIリクエスト認証時のエラーを追加することにします。

```php
{
    "message": "Route [login] not defined.",
    "exception": "InvalidArgumentException",
    .
    .
    .
```

### とりあえずエラーをでなくする

loginって（名前の）ルートがないぞ！と怒られるので、とりあえずloginって名前のルートを作ります。
めんどいので既にある/にname()で名前つけます。

```php:routes/web.php
Route::get('/', function () {
    return view('welcome');
})->name('login');
```

それかエラーの原因となっているリダイレクトを消してもいいです。

```php:app/Http/Middleware/Authenticate.php
protected function redirectTo($request)
{
    // return route('login');
}
```

### 再度リクエスト

再度（エラーになるよう）リクエストしてみます。API認証エラー時の標準メッセージが出ています。

```bash
{"message":"Unauthenticated."}
```

### エラーメッセージを編集する

標準メッセージを変更したい場合はHandle.phpをオーバーライドします。

```diff:app/Exceptons/Handle.php
<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

+use Request;
+use Response;
+use Illuminate\Auth\AuthenticationException;

class Handler extends ExceptionHandler
{

    protected $dontReport = [
        //
    ];


    protected $dontFlash = [
        'password',
        'password_confirmation',
    ];

    public function report(Exception $exception)
    {
        parent::report($exception);
    }

    public function render($request, Exception $exception)
    {
        return parent::render($request, $exception);
    }

	//追加
+   public function unauthenticated($request, AuthenticationException $exception)
+   {
+       if($request->expectsJson()){
+           return response()->json(['error' => 'NG'], 401);
+       }

+       return route('login');
+   }
}
```

一応確認。

```bash
{"error":"NG"}
```

認証の実装より、エラー処理の方が手間。が、とりあえずは以上です。

Passport等の「おまかせ」系に比べ100%自分で制御できてる感があり、これはこれで安心な感じ。