Как проверить аутентификацию через API с помощью Laravel Passport?

Я пытаюсь проверить аутентификацию с помощью Laravel's Passport, и нет возможности ... всегда получал 401, что этот клиент недействителен, я оставлю вам то, что я пробовал:

Моя конфигурация phpunit - это та, которая исходит из базы с laravel

тесты / 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;
    }
}

тесты / 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'
        ]);
    }
}

Моя удобная аутентификация с паспортом для тестирования

маршруты / 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);
});
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Оживление вашего приложения Laravel: Понимание режима обслуживания
Оживление вашего приложения Laravel: Понимание режима обслуживания
Здравствуйте, разработчики! В сегодняшней статье мы рассмотрим важный аспект управления приложениями, который часто упускается из виду в суете...
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
Поиск нового уровня в Laravel с помощью MeiliSearch и Scout
Поиск нового уровня в Laravel с помощью MeiliSearch и Scout
Laravel Scout - это популярный пакет, который предоставляет простой и удобный способ добавить полнотекстовый поиск в ваше приложение Laravel. Он...
Освоение архитектуры микросервисов с Laravel: Лучшие практики, преимущества и советы для разработчиков
Освоение архитектуры микросервисов с Laravel: Лучшие практики, преимущества и советы для разработчиков
В последние годы архитектура микросервисов приобрела популярность как способ построения масштабируемых и гибких приложений. Laravel , популярный PHP...
Как построить CRUD-приложение в Laravel
Как построить CRUD-приложение в Laravel
Laravel - это популярный PHP-фреймворк, который позволяет быстро и легко создавать веб-приложения. Одной из наиболее распространенных задач в...
23
0
23 483
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 7

Laravel Passport на самом деле поставляется с некоторыми помощниками по тестированию, который вы можете использовать для тестирования ваших аутентифицированных конечных точек API.

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

Спасибо за ответ, но можно ли получить client_id и client_secret после того, как jwt сгенерирован этим методом? Мне нужно получить их для теста, в документации это не очень ясно: github.com/laravel/passport/blob/…

dddenis 01.05.2018 18:04

Спасибо тебе за это! По какой-то странной причине использование $ this-> json () и установка пользовательских заголовков с токенами-носителями каждый раз не работает. Только в первый раз. Он просто привязан к одному пользователю, как если бы он был кеширован внутри или что-то в этом роде. Выдергивание волос - неприятная штука. Вне тестов вел себя нормально. Но это заставило его работать! Другой тоже должен быть, но эй - другая проблема решена ;-)

Stan Smulders 28.03.2019 22:11

Есть идеи, как получить доступ к этому пользователю? Мне нужен их ID для утверждений. ОБНОВЛЕНО - Вы можете получить его с помощью Auth :: user ()

plushyObject 04.11.2019 05:39

@PlushyObject Вы можете сначала сохранить пользователя в переменной, а затем передать ее в метод actingAs.

Oluwatobi Samuel Omisakin 23.03.2020 08:15

Я также получил доступ к нему с помощью Auth :: user ()

plushyObject 25.03.2020 16:07

Я не был знаком с инструментом 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 08.05.2018 23:32

Для тестирования паспорта вам не нужно было вводить реальный логин и пароль, вы можете создать тестовый. Вы можете использовать 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.

вы также можете проверить laracasts.com/discuss/channels/testing/….

Bibhudatta Sahoo 08.05.2018 07:57
Ответ принят как подходящий

Вот как вы можете реализовать это, чтобы оно действительно работало.

Прежде всего, следует правильно реализовать db: семена и Установка паспорта.

Во-вторых, вам не нужно создавать свой собственный маршрут для проверки, работает ли он (для этого достаточно базовых ответов Заграничный пасспорт).

Итак, вот описание того, как это работало в моей установке (Laravel 5.5) ...

В моем случае мне нужен только один клиент Заграничный пасспорт, поэтому я создал другой маршрут для авторизации api (api/v1/login), чтобы указать только имя пользователя и пароль. Подробнее об этом можно прочитать здесь.

К счастью, этот пример также охватывает базовый тест Авторизация паспорта.

Итак, чтобы успешно запустить ваши тесты, основная идея:

  1. Создайте ключи паспорта на тестовой установке.
  2. Заполните базу данных пользователями, ролями и другими ресурсами, которые могут потребоваться.
  3. Создайте запись .env с PASSPORT_CLIENT_ID (необязательно - Заграничный пасспорт всегда создает password grant token с id = 2 на пустой базе данных).
  4. Используйте этот идентификатор, чтобы получить правильный 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 runner.

Наконец, проверьте также свое промежуточное ПО на маршрутах. Особенно, если вы экспериментировали с пакетом динго (или jwt по тимону).

Единственное промежуточное ПО, которое вы можете рассмотреть, применяя к маршруту авторизации Заграничный пасспорт, - это throttle, чтобы иметь некоторую защиту от атака грубой силой.

Примечание...

Заграничный пасспорт и динго имеют совершенно разные реализации jwt.

В моих тестах только Заграничный пасспорт ведет себя правильно, и я предполагаю, что это причина, по которой динго больше не поддерживается.

Надеюсь, это решит вашу проблему ...

Привет, хороший и полезный ответ. У меня есть небольшое дополнение - нет необходимости устанавливать заголовок ['Accept' => 'application / json'], когда мы используем метод $ this-> json, потому что эти заголовки объединены по умолчанию в этом методе.

Alexey Shabramov 25.01.2019 10:54

Я думаю, что выбранный ответ, вероятно, является наиболее надежным и лучшим на данный момент, но я хотел предоставить альтернативу, которая сработает для меня, если вам просто нужно быстро пройти тесты по паспорту без большой настройки.

Важное примечание: я думаю, что если вы собираетесь делать много этого, это неправильный способ, и другие ответы лучше. Но по моей оценке это действительно кажется просто работать

Вот полный тестовый пример, в котором мне нужно предположить, что пользователь выполняет 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([
                    //...
                ]);

    }
}

И для ясности, вот метод generateToken() в модели User, который использует свойство HasApiTokens.

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

На мой взгляд, это довольно грубо и готово. Например, если вы используете черту RefreshDatabase, вам нужно запускать команду «паспорт: установка», подобную этой, в каждом методе. Возможно, есть лучший способ сделать это через глобальную настройку, но я новичок в PHPUnit, поэтому я делаю это (пока) именно так.

Тестирование токенов личного доступа

Вот пример для всех, кто хочет протестировать ваш api с использованием токенов личного доступа.

Сначала настройте тестовый класс

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

Что касается метода getAccessToken(), просто используйте api интерфейса Passport.

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);

}

Оптимизация для ненужных миграций БД

Вот пример, который гарантирует, что вы по-прежнему можете писать тесты, которые не зависят от базы данных - не выполняя миграции db.

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([ 
       //...
    ]);
}

...

Таким образом, вы можете писать тесты без каких-либо миграций БД.

Другие вопросы по теме