24

I'm trying to test the authentication with Laravel's Passport and there's no way... always received a 401 of that client is invalid, I'll leave you what I've tried:

My phpunit configuration is the one that comes from base with 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'
        ]);
    }
}

My handy authentication with passport for testing purposes

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);
});
dddenis
  • 480
  • 1
  • 8
  • 14

7 Answers7

43

This is how you can implement this, to make it actually work.

First of all, you should properly implement db:seeds and Passport installation.

Second one, you don't need, to create your own route to verify, if that works (basic Passport responses are fine enough, for that).

So here is a description, on how it worked in my installation (Laravel 5.5)...

In my case, I need only one Passport client, that's why, I created another route, for api authorization (api/v1/login), to only supply username and password. You can read more about it here.

Fortunately this example covers basic Passport authorization test also.

So to successfully run your tests, the basic idea is:

  1. Create passport keys on test setup.
  2. Seed db with users, roles and other resources which might be needed.
  3. Create .env entry with PASSPORT_CLIENT_ID (optional - Passport always create password grant token with id = 2 on empty db).
  4. Use this id to fetch proper client_secret from db.
  5. And then run your tests...

Code examples...

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

Notes:

Credentials need to be modified of course.

PASSPORT_CLIENT_ID needs to be 2, as explained before.

JsonStructure verification is redundant, since we get 200 response, only if authorization succeeds. However, if you wanted additional verification, this also passes...

TestCase.php

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

Notes:

Here we are creating relevant entries to db, which are needed in our tests. So remember, to have users with roles etc. seeded here.

Final notes...

This should be enough, to get your code working. On my system, all this passes green and also works on my gitlab CI runner.

Finally, please check your middlewares on routes also. Especially, if you were experimenting with dingo (or jwt by thymon) package.

The only middleware, you may consider, applying to Passport authorization route, is throttle to have some protection from brute force attack.

Side note...

Passport and dingo have totally different jwt implementations.

In my tests, only Passport behaves the right way and I assume, that this is the reason, why dingo is not maintained anymore.

Hope it will solve your problem...

Bart
  • 1,889
  • 1
  • 21
  • 38
  • Hi, nice and usefull answer. I have a small addition - there is no need to set the header ['Accept' => 'application/json'] when we are using $this->json method, because this header are merged by default in this method. – Alexey Shabramov Jan 25 '19 at 09:54
20

Laravel Passport actually ships with some testing helpers which you can use to test your authenticated API endpoints.

Passport::actingAs(
    factory(User::class)->create(),
);
Dwight
  • 12,120
  • 6
  • 51
  • 64
  • 1
    Thanks for your answer, but is it possible to get the client_id and client_secret once the jwt is generated with this method? I need to get them for my test, the documentation doesn't make it very clear: https://github.com/laravel/passport/blob/f4c2ef7bc1bc48f0cf29f1550fad04e03d74ac97/src/Passport.php#L275 – dddenis May 01 '18 at 16:04
  • 1
    Thank you for this! For some weird reason using $this->json() and setting custom headers with bearer tokens each time does not work. Only the first time. It just sticks to one user, as if it's cached internally or something. Hair-pulling frustrating stuff. It behaved fine outside the tests. This made it work though! The other one should have as well but hey - another problem solved ;-) – Stan Smulders Mar 28 '19 at 21:11
  • Any idea how to access this user? I need their ID for the assertions. UPDATED - You can grab it with Auth::user() – plushyObject Nov 04 '19 at 04:39
  • @PlushyObject You can save the user at first in a variable then pass it into `actingAs` method. – Oluwatobi Samuel Omisakin Mar 23 '20 at 07:15
  • I accessed it with Auth::user(), as well – plushyObject Mar 25 '20 at 15:07
9

I think the selected answer is likely the most robust and best here so far, but I wanted to provide an alternative that worked for me if you just need to quickly get tests passing behind passport without a lot of setup.

Important note: I think if you're going to do a lot of this, this isn't the right way and other answers are better. But in my estimation this does seem to just work

Here's a full test case where I need to assume a user, POST to an endpoint, and use their Authorization token to make the request.

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

    }
}

And for clarity, here is the generateToken() method in the User model, which leverages the HasApiTokens trait.

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

This is fairly rough and ready in my opinion. For example it if you're using the RefreshDatabase trait you have to run the passport:install command like this in every method. There may be a better way to do this via global setup, but I'm fairly new to PHPUnit so this is how I'm doing it (for now).

Eli Hooten
  • 994
  • 2
  • 9
  • 23
6

For Testing passport you did not need to go for real user and password you can create test one.
You can use Passport::actingAs or by setup().

For actingAs you can do like

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

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

    $response->assertStatus(200);
}

and with setUp() you can achieve this by

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

You can get more details Here and https://laravel.com/docs/5.6/passport#testing.

Bibhudatta Sahoo
  • 4,808
  • 2
  • 27
  • 51
2

I wasn't familiar with the Passport tool that Dwight is referring to when I wrote this, so it's possible that's a simpler solution. But here's something that may help. It produces a token for you, that you can then apply to your mock-API call.

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

Then you an just apply it to the headers:

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

If you need to be able to parse the token, try this:

/**
 * @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);
}
kmuenkel
  • 2,659
  • 1
  • 19
  • 20
1

Testing Personal Access Tokens

Here is an example for any who are looking to test your api using personal access tokens.

First, set up the test class

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

As for the getAccessToken() method, just use the Passport frontend api

private function getAccessToken()
{
    $response = $this->post('/oauth/personal-access-tokens',[
        'name' => 'temp-test-token'
    ]);

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

And simply:

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

}
Beefjeff
  • 371
  • 4
  • 12
1

Optimise for unnecessary DB migrations

Here is an example that ensures that you are still able to write tests that do not depend on the database - not running db migrations.

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

Then in your test:

...

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

...

This way you are able to write tests that do not have any db migrations

castle
  • 73
  • 3