Posted at

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