Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

Passportの実装

公式サイトの手順に従ってPassportを導入します(詳しい手順については今回は省略)。

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

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');
    }
}
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();
    }
}
App\Item
namespace App;

use Illuminate\Database\Eloquent\Model;

class Item extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}
App\Http\Controllers\ItemController
namespace App\Http\Controllers;

use App\Item;
class ItemController extends Controller
{
    public function show(Item $item)
    {
        return response($item);
    }
}
api.php
use Illuminate\Http\Request;

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

Policyの実装

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

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;
    }
}
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のテスト

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などのリクエストクライアントを使ってAPIの確認を行います。
まずは認証用のトークンを取得します。
oauth/tokenに以下のデータでPOSTリクエストを実行します。
ヘッダにはapplication/content : jsonを指定してください。
client_idclient_secretではphp artisan passport:installを実行したときのClient IDClient Secretを指定します。

{
  "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 : jsonAuthorization: Bearer {さっき取得したaccess_token}を設定し、api/items/1api/items/2にGETリクエストを実行します。

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

APIでPolicyを有効にする

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

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);
    }
}
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へのリクエストを実行すると今度は正しいステータスが取得できます。

frost_star
まだまだ半人前プログラマー。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした