web-dev-qa-db-fra.com

Comment tester l'authentification via l'API avec Laravel Passport?

J'essaie de tester l'authentification avec le passeport de Laravel et il n'y a aucun moyen ... toujours reçu un 401 de ce client n'est pas valide, je vous laisse ce que j'ai essayé:

Ma configuration phpunit est celle qui vient de la base avec 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'
        ]);
    }
}

Mon authentification pratique avec passeport à des fins de test

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

Voici comment vous pouvez l'implémenter, pour que cela fonctionne réellement.

Tout d'abord, vous devez correctement implémenter db: seeds et Installation de passeport .

Deuxièmement, vous n'avez pas besoin de créer votre propre itinéraire pour vérifier si cela fonctionne (les réponses de base Passport sont assez bien pour cela).

Voici donc une description de son fonctionnement dans mon installation (Laravel 5.5) ...

Dans mon cas, je n'ai besoin que d'un client Passport , c'est pourquoi, j'ai créé une autre route, pour l'autorisation api (api/v1/login), pour ne fournir que nom d'utilisateur et mot de passe. Vous pouvez en savoir plus ici .

Heureusement, cet exemple couvre également le test de base d'autorisation de passeport .

Donc, pour réussir vos tests, l'idée de base est:

  1. Créez des clés de passeport lors de la configuration du test.
  2. Seed db avec les utilisateurs, les rôles et autres ressources qui pourraient être nécessaires.
  3. Créer .env entrée avec PASSPORT_CLIENT_ID (facultatif - Passeport créez toujours password grant token avec id = 2 sur db vide).
  4. Utilisez cet identifiant pour récupérer le bon client_secret de db.
  5. Et puis lancez vos tests ...

Exemples de code ...

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']);
}

Remarques:

Les pouvoirs doivent bien sûr être modifiés.

PASSPORT_CLIENT_ID doit être égal à 2, comme expliqué précédemment.

La vérification JsonStructure est redondante, car nous obtenons 200 réponses, uniquement si l'autorisation réussit. Cependant, si vous vouliez une vérification supplémentaire, cela passe également ...

TestCase.php

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

Remarques:

Ici, nous créons des entrées pertinentes pour db, qui sont nécessaires dans nos tests. N'oubliez donc pas d'avoir des utilisateurs avec des rôles, etc. ici.

Notes finales ...

Cela devrait suffire pour que votre code fonctionne. Sur mon système, tout cela passe au vert et fonctionne également sur mon runner gitlab CI.

Enfin, veuillez également vérifier vos middlewares sur les itinéraires. Surtout, si vous expérimentiez avec dingo (ou jwt par thymon ) paquet.

Le seul middleware, vous pouvez envisager, s'appliquant à la route d'autorisation du passeport , est throttle pour avoir une certaine protection contre attaque par force brute .

Note latérale ...

Passeport et dingo ont totalement différents implémentations jwt .

Dans mes tests, seul Passport se comporte de la bonne façon et je suppose que c'est la raison pour laquelle dingo n'est plus maintenu.

J'espère que cela résoudra votre problème ...

19
Bart

Pour tester le passeport, vous n'avez pas besoin d'aller pour un véritable utilisateur et un mot de passe, vous pouvez en créer un.
Vous pouvez utiliser Passport::actingAs Ou par setup().

Pour actingAs vous pouvez faire comme

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

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

    $response->assertStatus(200);
}

et avec setUp() vous pouvez y parvenir en

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

Vous pouvez obtenir plus de détails ici et https://laravel.com/docs/5.6/passport#testing .

4

Laravel Passport livré avec des aides de test que vous pouvez utiliser pour tester vos points de terminaison API authentifiés.

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

Je pense que la réponse sélectionnée est probablement la plus robuste et la meilleure ici jusqu'à présent, mais je voulais fournir une alternative qui a fonctionné pour moi si vous avez juste besoin de passer rapidement des tests derrière le passeport sans beaucoup de configuration.

Remarque importante: je pense que si vous allez faire beaucoup de cela, ce n'est pas la bonne façon et d'autres réponses sont meilleures. Mais à mon avis, cela semble juste travailler

Voici un cas de test complet où je dois supposer un utilisateur, POST à un point de terminaison, et utiliser leur jeton d'autorisation pour faire la demande.

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

    }
}

Et pour plus de clarté, voici la méthode generateToken() dans le modèle User, qui exploite le trait HasApiTokens.

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

C'est assez rude et prêt à mon avis. Par exemple, si vous utilisez le trait RefreshDatabase, vous devez exécuter la commande passport: install comme celle-ci dans chaque méthode. Il y a peut-être un meilleur moyen de le faire via une configuration globale, mais je suis assez nouveau sur PHPUnit, c'est ainsi que je le fais (pour l'instant).

3
Eli Hooten

Je ne connaissais pas l'outil de passeport auquel Dwight fait référence lorsque j'ai écrit cela, il est donc possible que ce soit une solution plus simple. Mais voici quelque chose qui peut vous aider. Il produit un jeton pour vous, que vous pouvez ensuite appliquer à votre appel mock-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;
}

Ensuite, vous l'appliquez simplement aux en-têtes:

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

Si vous devez pouvoir analyser le jeton, essayez ceci:

/**
 * @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