web-dev-qa-db-ja.com

Laravel Passportを使用してAPI経由で認証をテストする方法は?

私はLaravelのパスポートで認証をテストしようとしていますが、方法はありません...常にそのクライアントの401が無効であるため、私が試したものを残します:

私のphpunit設定は、laravelをベースにしたものです

tests/TestCase.php

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication, DatabaseTransactions;

    protected $client, $user, $token;

    public function setUp()
    {
        parent::setUp();

        $clientRepository = new ClientRepository();
        $this->client = $clientRepository->createPersonalAccessClient(
            null, 'Test Personal Access Client', '/'
        );
        DB::table('oauth_personal_access_clients')->insert([
            'client_id' => $this->client->id,
            'created_at' => date('Y-m-d'),
            'updated_at' => date('Y-m-d'),
        ]);
        $this->user = User::create([
            'id' => 1,
            'name' => 'test',
            'lastname' => 'er',
            'email' => '[email protected]',
            'password' => bcrypt('secret')
        ]);
        $this->token = $this->user->createToken('TestToken', [])->accessToken;
    }
}

tests/Feature/AuthTest.php

class AuthTest extends TestCase
{
    use DatabaseMigrations;

    public function testShouldSignIn()
    {
        // Arrange
        $body = [
            'client_id' => (string) $this->client->id,
            'client_secret' => $this->client->secret,
            'email' => '[email protected]',
            'password' => 'secret',
        ];
        // Act
        $this->json('POST', '/api/signin', $body, ['Accept' => 'application/json'])
        // Assert
        ->assertStatus(200)
        ->assertJsonStructure([
            'data' => [
                'jwt' => [
                    'access_token',
                    'expires_in',
                    'token_type',
                ]
            ],
            'errors'
        ]);
    }
}

テスト用のパスポートでの便利な認証

routes/api.php

Route::post('/signin', function () {
    $args = request()->only(['email', 'password', 'client_id', 'client_secret']);
    request()->request->add([
        'grant_type' => 'password',
        'client_id' => $args['client_id'] ?? env('PASSPORT_CLIENT_ID', ''),
        'client_secret' => $args['client_secret'] ?? env('PASSPORT_CLIENT_SECRET', ''),
        'username' => $args['email'],
        'password' => $args['password'],
        'scope' => '*',
    ]);
    $res = Route::dispatch(Request::create('oauth/token', 'POST'));
    $data = json_decode($res->getContent());
    $isOk = $res->getStatusCode() === 200;
    return response()->json([
        'data' => $isOk ? [ 'jwt' => $data ] : null,
        'errors' => $isOk ? null : [ $data ]
    ], 200);
});
13
dddenis

これは、実際に機能させるためにこれを実装する方法です。

まず、db:seedsPassport installationを適切に実装する必要があります。

2つ目は、それが機能するかどうかを確認するための独自のルートを作成する必要はありません(基本Passport応答で十分です)。

だから、ここに私のインストール(Laravel 5.5)でどのように機能したかについての説明があります...

私の場合、必要なのは1つのPassportクライアントだけです。そのため、ユーザー名とパスワードのみを提供するために、API認証(api/v1/login)のために別のルートを作成しました。詳細については、こちらをご覧ください こちら

幸いなことに、この例では基本的なPassport authorizationテストもカバーしています。

したがって、テストを正常に実行するための基本的な考え方は次のとおりです。

  1. テストセットアップでパスポートキーを作成します。
  2. 必要なユーザー、ロール、その他のリソースを含むシードデータベース。
  3. .envを使用してPASSPORT_CLIENT_IDエントリを作成します(オプション-Passportは常に、空のデータベースにid = 2でpassword grant tokenを作成します)。
  4. このIDを使用して、dbから適切なclient_secretを取得します。
  5. そして、テストを実行します...

コード例...

ApiLoginTest.php

/**
* @group apilogintests
*/    
public function testApiLogin() {
    $body = [
        'username' => '[email protected]',
        'password' => 'admin'
    ];
    $this->json('POST','/api/v1/login',$body,['Accept' => 'application/json'])
        ->assertStatus(200)
        ->assertJsonStructure(['token_type','expires_in','access_token','refresh_token']);
}
/**
 * @group apilogintests
 */
public function testOauthLogin() {
    $oauth_client_id = env('PASSPORT_CLIENT_ID');
    $oauth_client = OauthClients::findOrFail($oauth_client_id);

    $body = [
        'username' => '[email protected]',
        'password' => 'admin',
        'client_id' => $oauth_client_id,
        'client_secret' => $oauth_client->secret,
        'grant_type' => 'password',
        'scope' => '*'
    ];
    $this->json('POST','/oauth/token',$body,['Accept' => 'application/json'])
        ->assertStatus(200)
        ->assertJsonStructure(['token_type','expires_in','access_token','refresh_token']);
}

注:

もちろん、資格情報を変更する必要があります。

前に説明したように、PASSPORT_CLIENT_IDは2である必要があります。

JsonStructureの検証は、承認が成功した場合にのみ200の応答を受け取るため冗長です。ただし、追加の検証が必要な場合は、これも合格します...

TestCase.php

public function setUp() {
    parent::setUp();
    \Artisan::call('migrate',['-vvv' => true]);
    \Artisan::call('passport:install',['-vvv' => true]);
    \Artisan::call('db:seed',['-vvv' => true]);
}

注:

ここでは、テストに必要なdbに関連するエントリを作成しています。そのため、ここにシードなどの役割を持つユーザーがいることを忘れないでください。

最終ノート...

コードを機能させるにはこれで十分です。私のシステムでは、これはすべて緑色になり、gitlab CIランナーでも機能します。

最後に、ルート上のミドルウェアも確認してください。特に、dingo(またはjwt by thymon)パッケージで実験している場合。

Passport承認ルートに適用する唯一のミドルウェアは、ブルートフォース攻撃から保護するためのthrottleです。

サイドノート...

Passportdingoには、まったく異なるjwt実装があります。

私のテストでは、Passportだけが正しい方法で動作し、これがdingoが維持されない理由であると思います。

それがあなたの問題を解決することを願っています...

19
Bart

パスポートのテストでは、実際のユーザーとパスワードを入力する必要はありません。テスト用のパスポートを作成できます。
_Passport::actingAs_を使用するか、setup()で使用できます。

actingAsの場合、次のようにできます

_public function testServerCreation()
{
    Passport::actingAs(
        factory(User::class)->create(),
        ['create-servers']
    );

    $response = $this->post('/api/create-server');

    $response->assertStatus(200);
}
_

setUp()を使用すると、これを実現できます

_public function setUp()
    {
        parent::setUp();
        $clientRepository = new ClientRepository();
        $client = $clientRepository->createPersonalAccessClient(
            null, 'Test Personal Access Client', $this->baseUrl
        );
        DB::table('oauth_personal_access_clients')->insert([
            'client_id' => $client->id,
            'created_at' => new DateTime,
            'updated_at' => new DateTime,
        ]);
        $this->user = factory(User::class)->create();
        $token = $this->user->createToken('TestToken', $this->scopes)->accessToken;
        $this->headers['Accept'] = 'application/json';
        $this->headers['Authorization'] = 'Bearer '.$token;
    }
_

詳細を取得できます ここ および https://laravel.com/docs/5.6/passport#testing

4

Laravel Passport 実際にはいくつかのテストヘルパーが同梱されています 認証済みAPIエンドポイントのテストに使用できます。

Passport::actingAs(
    factory(User::class)->create(),
);
4
Dwight

ここで選択された答えはおそらく最も堅牢で最高だと思いますが、多くのセットアップをせずにパスポートの背後でテストをすばやく通過させる必要がある場合に役立つ代替手段を提供したかったのです。

重要な注意:これをたくさんやるつもりなら、これは正しい方法ではなく、他の答えの方が良いと思います。しかし、私の推定では、これはちょうどうまくいく

以下は、エンドポイントに対してユーザーPOSTを想定し、その認証トークンを使用してリクエストを行う必要がある場合の完全なテストケースです。

_<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

use App\Models\User;
use Laravel\Passport\Passport;

class MyTest extends TestCase
{
    use WithFaker, RefreshDatabase;

    public function my_test()
    {
        /**
        *
        * Without Artisan call you will get a passport 
        * "please create a personal access client" error
        */
        \Artisan::call('passport:install');

        $user = factory(User::class)->create();
        Passport::actingAs($user);

        //See Below
        $token = $user->generateToken();

        $headers = [ 'Authorization' => 'Bearer $token'];
        $payload = [
            //...
        ];



        $response = $this->json('POST', '/api/resource', $payload, $headers);

        $response->assertStatus(200)
                ->assertJson([
                    //...
                ]);

    }
}
_

わかりやすくするために、ここにUserモデルのgenerateToken()メソッドを示します。これはHasApiTokens特性を活用します。

_public function generateToken() {
    return $this->createToken('my-oauth-client-name')->accessToken; 
}
_

これはかなり大雑把で、私の意見では準備ができています。たとえば、RefreshDatabase特性を使用している場合は、すべてのメソッドでこのようなpassport:installコマンドを実行する必要があります。グローバルセットアップを介してこれを行うためのより良い方法があるかもしれませんが、私はPHPUnitにはかなり新しいので、これは私がそれをやっている方法です(今のところ)。

3
Eli Hooten

私がこれを書いたときにドワイトが言及しているPassportツールに慣れていなかったので、それはより簡単な解決策である可能性があります。しかし、ここで役立つことがあります。それはあなたのためのトークンを生成し、それをあなたのモックAPI呼び出しに適用することができます。

/**
 * @param Authenticatable $model
 * @param array $scope
 * @param bool $personalAccessToken
 * @return mixed
 */
public function makeOauthLoginToken(Authenticatable $model = null, array $scope = ['*'], $personalAccessToken = true)
{
    $tokenName = $clientName = 'testing';
    Artisan::call('passport:client', ['--personal' => true, '--name' => $clientName]);
    if (!$personalAccessToken) {
        $clientId = app(Client::class)->where('name', $clientName)->first(['id'])->id;
        Passport::$personalAccessClient = $clientId;
    }
    $userId = $model->getKey();
    return app(PersonalAccessTokenFactory::class)->make($userId, $tokenName, $scope)->accessToken;
}

次に、ヘッダーにそれを適用します:

$user = app(User::class)->first($testUserId);
$token = $this->makeOauthLoginToken($user);
$headers = ['authorization' => "Bearer $token"];
$server = $this->transformHeadersToServerVars($headers);

$body = $cookies = $files = [];
$response = $this->call($method, $uri, $body, $cookies, $files, $server);

$content = $response->getContent();
$code = $response->getStatusCode();

トークンを解析できるようにする必要がある場合は、これを試してください:

/**
 * @param string $token
 * @param Authenticatable $model
 * @return Authenticatable|null
 */
public function parsePassportToken($token, Authenticatable $model = null)
{
    if (!$model) {
        $provider = config('auth.guards.passport.provider');
        $model = config("auth.providers.$provider.model");
        $model = app($model);
    }
    //Passport's token parsing is looking to a bearer token using a protected method.  So a dummy-request is needed.
    $request = app(Request::class);
    $request->headers->add(['authorization' => "Bearer $token"]);
    //Laravel\Passport\Guards\TokenGuard::authenticateViaBearerToken() expects the user table to leverage the
    //HasApiTokens trait.  If that's missing, a query macro can satisfy its expectation for this method.
    if (!method_exists($model, 'withAccessToken')) {
        Builder::macro('withAccessToken', function ($accessToken) use ($model) {
            $model->accessToken = $accessToken;
            return $this;
        });
        /** @var TokenGuard $guard */
        $guard = Auth::guard('passport');
        return $guard->user($request)->getModel();
    }
    /** @var TokenGuard $guard */
    $guard = Auth::guard('passport');
    return $guard->user($request);
}
1
Claymore