如何使用Laravel Passport通过API进行身份验证测试?

24

我正在尝试使用Laravel Passport测试身份验证,但无论如何...始终收到一个客户端无效的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' => 'test@test.test',
            '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' => 'test@test.test',
            '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'
        ]);
    }
}

为了测试目的,我使用passport进行方便的身份验证。

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);
});
7个回答

43

以下是如何实现这一点,让它真正工作。

首先,您应该正确地实现 db:seedsPassport安装

第二个,您不需要创建自己的路由来验证是否有效(基本的 Passport 响应已足够)。

下面是我在我的安装(Laravel 5.5)中如何工作的描述...

在我的情况下,我只需要一个Passport客户端,所以我创建了另一个路由,用于api授权(api/v1/login),仅提供用户名和密码。您可以在此处了解更多信息。

幸运的是,这个例子也涵盖了基本的Passport授权测试

因此,要成功运行您的测试,基本思路是:

  1. 在测试设置中创建护照密钥。
  2. 使用可能需要的用户、角色和其他资源填充数据库。
  3. 创建 .env 条目,其中包含 PASSPORT_CLIENT_ID(可选 - Passport 在空数据库上始终使用 id = 2 创建 密码授权令牌)。
  4. 使用此 ID 从数据库中获取正确的client_secret。
  5. 然后运行您的测试...

代码示例...

ApiLoginTest.php

/**
* @group apilogintests
*/    
public function testApiLogin() {
    $body = [
        'username' => 'admin@admin.com',
        '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' => 'admin@admin.com',
        '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]);
}

注:

在这里,我们正在创建数据库中需要的相关条目,这些条目在我们的测试中是必需的。所以请记住要在此处种植有角色等用户。

最后的注意事项...

这应该足以让你的代码运行起来。在我的系统上,所有这些都能通过测试并且在我的gitlab CI runner上也能正常工作。

最后,请确保您的路由中间件也正确设置。特别是,如果您正在尝试使用dingo(或jwt by thymon)包。

唯一需要考虑应用到Passport授权路由的中间件是throttle,以保护免受暴力攻击的影响。

附带说明...

Passportdingo有完全不同的JWT实现。

在我的测试中,只有Passport表现得正确,我认为这就是为什么dingo不再维护的原因。

希望它能解决您的问题...


你好,非常好而且有用的回答。 我有一个小补充 - 当我们使用 $this->json 方法时,没有必要设置 header ['Accept' => 'application/json'],因为这个 header 在此方法中默认被合并。 - Alexey Shabramov

20

1
谢谢您的回答,但是使用这种方法生成jwt后是否有可能获取client_id和client_secret?我需要在测试中获取它们,但文档并没有说明得很清楚:https://github.com/laravel/passport/blob/f4c2ef7bc1bc48f0cf29f1550fad04e03d74ac97/src/Passport.php#L275 - dddenis
1
谢谢你!出于某种奇怪的原因,每次使用 $this->json() 并设置自定义标头和令牌时都不起作用。只有第一次有效。它似乎会内部缓存一个用户,非常令人抓狂。在测试之外,它表现得很好。但是这个方法让它工作了!另一个方法也应该可以,但是嘿 - 又解决了一个问题;-) - Stan Smulders
有没有想法如何访问这个用户?我需要他们的ID来进行断言。更新 - 您可以使用Auth :: user()获取它。 - plushyObject
@PlushyObject 你可以先将用户保存在一个变量中,然后将其传递到 actingAs 方法中。 - Oluwatobi Samuel Omisakin
我使用 Auth::user() 访问它。 - plushyObject

9

我认为选出的答案到目前为止可能是最健壮和最好的,但如果您只需要快速地让测试在passport身后通过而不需要进行大量设置,那么我想提供一个对我有效的备选方案。

重要提示:我认为如果您要频繁执行此操作,这并不是正确的方式,其他答案更好。但是在我的估计中,它似乎仅仅是有效的

以下是一个完整的测试用例,其中我需要假设一个用户,向一个端点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 还相当陌生,所以现在就这么做了。


Passport::actingAs($user); 对我很有帮助。谢谢! - Dreadnaut

6

进行passport测试时无需使用真实的用户名和密码,您可以创建测试用户。


您可以通过Passport::actingAssetup()来实现。

对于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获取更多详细信息。


您可以在 https://laracasts.com/discuss/channels/testing/testing-a-route-behind-authapi-passport-middleware?page=1 上查看。 - Bibhudatta Sahoo

2

当我写这篇文章时,我不熟悉Dwight所提到的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);
}

你确定创建令牌的系统使用的是与解析它的系统相同的APP_KEY吗? - kmuenkel

1

优化无必要的数据库迁移

下面是一个示例,确保您仍然能够编写不依赖于数据库的测试 - 不运行数据库迁移。

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\Schema;
use Laravel\Passport\ClientRepository;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;

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

        if (Schema::hasTable('oauth_clients')) {
            resolve(ClientRepository::class)->createPersonalAccessClient(
                null, config('app.name') . ' Personal Access Client', config('app.url')
            );
        }
    }
}

然后在你的测试中:
...

use RefreshDatabase;

/**
 * Test login
 *
 * @return void
 */
public function test_login()
{
    $this->withExceptionHandling();
    $user = factory(User::class)->create([
        'password' => 'secret'
    ]);

    $response = $this->json('POST', route('api.auth.login'), [
        'email' => $user->email,
        'password' => 'secret',
    ]);

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

...

通过这种方式,您可以编写没有任何数据库迁移的测试。


1

测试个人访问令牌

这是一个示例,供那些想使用个人访问令牌测试您的API的人参考。

首先,设置测试类。

protected function setUp(): void
{
    parent::setUp();
    $this->actingAs(User::first());
    $this->access_token = $this->getAccessToken();
}

关于getAccessToken()方法,只需使用Passport前端API即可。
private function getAccessToken()
{
    $response = $this->post('/oauth/personal-access-tokens',[
        'name' => 'temp-test-token'
    ]);

    return $response->json('accessToken');
}

只是简单地说:
public function the_personal_access_token_allows_us_to_use_the_api()
{
    $response = $this->get('/api/user', [
        'Authorization' => "Bearer $this->access_token"
    ]);


    $response->assertStatus(200);

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接