LaravelでPassport認証とPolicy認可の併用

Laravelでは、OAuthによるAPI認証を実現するために[Passport](https://readouble.com/laravel/5.6/ja/passport.html)というパッケージが提供されています。
また、認可の仕組みとして[Policy](https://readouble.com/laravel/5.6/ja/authorization.html)があります。
しかし、それぞれを単純に実装しただけだと単体テストはうまくいくのに通常のリクエストではうまくいかずはまったので今回はそれの解決法を書きます。
Laravel 5.6です。

# Passportの実装
[公式サイト](https://readouble.com/laravel/5.6/ja/passport.html)の手順に従ってPassportを導入します(詳しい手順については今回は省略)。

Passportの設定が完了したら、コントローラなどを作成します。
今回は適当にItemというモデルを定義してそれに関するもろもろ作りました。
簡易化のためにコントローラはGET用のメソッドのみ作成しています。

```php:create_item_table.php(マイグレーション)
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateItemsTable extends Migration
{
    public function up()
    {
        Schema::create('items', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('user_id');
            $table->text('content');
            $table->timestamps();
            $table->foreign('user_id')
                ->references('id')
                ->on('users');
        });
    }

    public function down()
    {
        Schema::dropIfExists('items');
    }
}
```

```php:DatabaseSeeder.php
use Illuminate\Database\Seeder;

class UsersTableSeeder extends Seeder
{
    public function run()
    {
        // User
        $alice = new \App\User();
        $alice->name = 'Alice';
        $alice->email = 'alice@example.com';
        $alice->password = bcrypt('AlicePass');
        $alice->remember_token = str_random(10);
        $alice->save();

        $bob = new \App\User();
        $bob->name = 'Bob';
        $bob->email = 'bob@example.com';
        $bob->password = bcrypt('BobPass');
        $bob->remember_token = str_random(10);
        $bob->save();

        // Item
        $itemA = new \App\Item();
        $itemA->user_id = $alice->id;
        $itemA->content = "by Alice";
        $itemA->save();

        $itemB = new \App\Item();
        $itemB->user_id = $bob->id;
        $itemB->content = "by Bob";
        $itemB->save();
    }
}
```

```php:App\Item
namespace App;

use Illuminate\Database\Eloquent\Model;

class Item extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}
```

```php:App\Http\Controllers\ItemController
namespace App\Http\Controllers;

use App\Item;
class ItemController extends Controller
{
    public function show(Item $item)
    {
        return response($item);
    }
}
```

```php:api.php
use Illuminate\Http\Request;

Route::Get('/items/{item}', 'ItemController@show');
```

# Policyの実装
続いてPolicyを実装し、コントローラに適用します。
今回はItemにuser_idというカラムを用意してUserと紐づけているため、user_idがログインユーザと等しい場合のみItemの情報が取得できるようにします。

```php:App\Policies\ItemPolicy.php
namespace App\Policies;

use App\User;
use App\Item;
use Illuminate\Auth\Access\HandlesAuthorization;

class ItemPolicy
{
    use HandlesAuthorization;

    public function view(User $user, Item $item)
    {
        return $item->user_id == $user->id;
    }
}
```

```php:App\Http\Controllers\ItemController(コントローラ)
namespace App\Http\Controllers;

use App\Item;
class ItemController extends Controller
{
    public function show(Item $item)
    {
        $this->authorize('view', $item);  // 追記
        return response($item);
    }
}
```

# とりあえず実行してみる
## PHPUnitのテスト

```php:ItemTest.php
namespace Tests\Feature;

use App\User;
use App\Item;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Passport\Passport;

class ItemTest extends TestCase
{
    use DatabaseMigrations;

    protected function setUp()
    {
        parent::setUp();
        (new \DatabaseSeeder())->run(); // テストデータ登録
    }

    public function testAliceGetItem1()
    {
        Passport::actingAs(
            User::where(['name' => 'Alice'])
        );
        $response = $this->get('/api/items/1');
        $response->assertStatus(200);
    }

    public function testAliceGetItem2()
    {
        Passport::actingAs(
            User::where(['name' => 'Alice'])
        );
        $response = $this->get('/api/items/2');
        $response->assertStatus(403);
    }

    public function testBobGetItem1()
    {
        Passport::actingAs(
            User::where(['name' => 'Bob'])
        );
        $response = $this->get('/api/items/1');
        $response->assertStatus(403);
    }

    public function testBobGetItem2()
    {
        Passport::actingAs(
            User::where(['name' => 'Bob'])
        );
        $response = $this->get('/api/items/2');
        $response->assertStatus(200);
    }
}
```
このテストは成功します。

## APIの確認

[ARC](https://chrome.google.com/webstore/detail/advanced-rest-client/hgmloofddffdnphfgcellkdfbfbjeloo?hl=ja)などのリクエストクライアントを使ってAPIの確認を行います。
まずは認証用のトークンを取得します。
`oauth/token`に以下のデータでPOSTリクエストを実行します。
ヘッダには`application/content` : `json`を指定してください。
`client_id`と`client_secret`では`php artisan passport:install`を実行したときの`Client ID`と`Client Secret`を指定します。

```json
{
  "grant_type": "password",
  "client_id": {passport:install時のClient ID},
  "client_secret": "{passport:install時のClient Secret}",
  "username": "alice@example.com",
  "password": "AlicePass"
}
```

レスポンスとして`access_token`を含むデータが取得できるのでその値を使ってAPIリクエストを実行します。
ヘッダに`application/content` : `json`と`Authorization`: `Bearer {さっき取得したaccess_token}`を設定し、`api/items/1`と`api/items/2`にGETリクエストを実行します。

認証したのはAliceなので、`api/items/1`のときは200、`api/items/2`のときは403が返ってきてほしいのですが両方403になります。

# APIでPolicyを有効にする
APIでPolicyを有効にするためのミドルウェアを作り、設定します。

```php:App\Http\Middleware\ApiAuthorization.php
namespace App\Http\Middleware;

use Illuminate\Support\Facades\Auth;
use Closure;

class ApiAuthorization
{
    public function handle($request, Closure $next)
    {
        Auth::shouldUse('api'); // APIでPolicyを有効にするための処理
        return $next($request);
    }
}
```

```php:App\Http\Karnel.php(抜粋)
    protected $middleware = [
        \App\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        \App\Http\Middleware\TrustProxies::class,
        \App\Http\Middleware\ApiAuthorization::class // 追記
    ];
```

# APIの確認
再度、トークンの取得とAPIへのリクエストを実行すると今度は正しいステータスが取得できます。