Laravelでは、OAuthによるAPI認証を実現するためにPassportというパッケージが提供されています。
また、認可の仕組みとしてPolicyがあります。
しかし、それぞれを単純に実装しただけだと単体テストはうまくいくのに通常のリクエストではうまくいかずはまったので今回はそれの解決法を書きます。
Laravel 5.6です。
Passportの実装
公式サイトの手順に従ってPassportを導入します(詳しい手順については今回は省略)。
Passportの設定が完了したら、コントローラなどを作成します。
今回は適当にItemというモデルを定義してそれに関するもろもろ作りました。
簡易化のためにコントローラはGET用のメソッドのみ作成しています。
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');
}
}
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();
}
}
namespace App;
use Illuminate\Database\Eloquent\Model;
class Item extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}
namespace App\Http\Controllers;
use App\Item;
class ItemController extends Controller
{
public function show(Item $item)
{
return response($item);
}
}
use Illuminate\Http\Request;
Route::Get('/items/{item}', 'ItemController@show');
Policyの実装
続いてPolicyを実装し、コントローラに適用します。
今回はItemにuser_idというカラムを用意してUserと紐づけているため、user_idがログインユーザと等しい場合のみItemの情報が取得できるようにします。
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;
}
}
namespace App\Http\Controllers;
use App\Item;
class ItemController extends Controller
{
public function show(Item $item)
{
$this->authorize('view', $item); // 追記
return response($item);
}
}
とりあえず実行してみる
PHPUnitのテスト
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_id
とclient_secret
ではphp artisan passport:install
を実行したときのClient ID
とClient 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
: 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を有効にするためのミドルウェアを作り、設定します。
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);
}
}
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へのリクエストを実行すると今度は正しいステータスが取得できます。