Skip to content

[8.x] Add tests for getting the access token with the password grant #1181

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ matrix:
allow_failures:
- env: LARAVEL=^7.0

services:
- mysql

before_install:
- phpenv config-rm xdebug.ini || true
- mysql -e 'CREATE DATABASE forge;'

install:
- travis_retry composer require "illuminate/contracts=${LARAVEL}" --dev --prefer-dist --no-interaction --no-suggest
Expand Down
13 changes: 10 additions & 3 deletions database/factories/PassportClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,15 @@
'name' => $faker->company,
'secret' => Str::random(40),
'redirect' => $faker->url,
'personal_access_client' => 0,
'password_client' => 0,
'revoked' => 0,
'personal_access_client' => false,
'password_client' => false,
'revoked' => false,
];
});

$factory->state(Client::class, 'password_client', function (Faker $faker) {
return [
'personal_access_client' => false,
'password_client' => true,
];
});
4 changes: 4 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@
<directory suffix=".php">./src/</directory>
</whitelist>
</filter>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

<php>
<env name="APP_KEY" value="AckfSECXIvnK5r28GVIWUAxmbBSjTsmF"/>
</php>
</phpunit>
189 changes: 189 additions & 0 deletions tests/Feature/AccessTokenControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?php

namespace Laravel\Passport\Tests\Feature;

use Carbon\CarbonImmutable;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Database\Eloquent\Factory;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Laravel\Passport\Client;
use Laravel\Passport\ClientRepository;
use Laravel\Passport\HasApiTokens;
use Laravel\Passport\Token;
use Laravel\Passport\TokenRepository;
use Lcobucci\JWT\Parser;

class AccessTokenControllerTest extends PassportTestCase
{
protected function setUp(): void
{
parent::setUp();

Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('email')->unique();
$table->string('password');
$table->dateTime('created_at');
$table->dateTime('updated_at');
});
}

protected function tearDown(): void
{
Schema::dropIfExists('users');

parent::tearDown();
}

protected function getUserClass(): ?string
{
return User::class;
}

public function testGettingAccessTokenWithPasswordGrant()
{
$this->withoutExceptionHandling();

$password = 'foobar123';
$user = new User();
$user->email = '[email protected]';
$user->password = $this->app->make(Hasher::class)->make($password);
$user->save();

/** @var Client $client */
$client = $this->app->make(Factory::class)->of(Client::class)->state('password_client')->create(['user_id' => $user->id]);

$response = $this->post(
'/oauth/token',
[
'grant_type' => 'password',
'client_id' => $client->id,
'client_secret' => $client->secret,
'username' => $user->email,
'password' => $password,
]
);

$response->assertOk();

$response->assertHeader('pragma', 'no-cache');
$response->assertHeader('cache-control', 'no-store, private');
$response->assertHeader('content-type', 'application/json; charset=UTF-8');

$decodedResponse = $response->decodeResponseJson();

$this->assertArrayHasKey('token_type', $decodedResponse);
$this->assertArrayHasKey('expires_in', $decodedResponse);
$this->assertArrayHasKey('access_token', $decodedResponse);
$this->assertArrayHasKey('refresh_token', $decodedResponse);
$this->assertSame('Bearer', $decodedResponse['token_type']);
$expiresInSeconds = 31622400;
$this->assertEqualsWithDelta($expiresInSeconds, $decodedResponse['expires_in'], 5);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@X-Coder264 this line was failing for us randomly. I had to update the value of the $expiresInSeconds variable to 31536000 instead. Any idea why? I also don't really know what we're testing here exactly? Why should the expires_in be the value of above?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@driesvints This tests the expiration time of the issued access token. The expiration time for the password grant is being set here:

https://github.com/laravel/passport/blob/v8.4.0/src/PassportServiceProvider.php#L116

This calls Passport::tokensExpireIn() which then returns new DateInterval('P1Y') (because there is no $date provided and its default value is null and static::$tokensExpireAt is null too).

The difference between our numbers is 86400 seconds which is exactly one day. I guess there is some weird time related stuff going on with that DateInterval which causes a random fail of that specific assertion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did the test maybe start to fail after 29.02.? If so then it is related to the leap year which is exactly why there was a one day difference between our expiration numbers 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hah, that could be yeah


$jwtAccessToken = (new Parser())->parse($decodedResponse['access_token']);
$this->assertTrue($this->app->make(ClientRepository::class)->findActive($jwtAccessToken->getClaim('aud'))->is($client));
$this->assertTrue($this->app->make('auth')->createUserProvider()->retrieveById($jwtAccessToken->getClaim('sub'))->is($user));

$token = $this->app->make(TokenRepository::class)->find($jwtAccessToken->getClaim('jti'));
$this->assertInstanceOf(Token::class, $token);
$this->assertFalse($token->revoked);
$this->assertTrue($token->user->is($user));
$this->assertTrue($token->client->is($client));
$this->assertNull($token->name);
$this->assertLessThanOrEqual(5, CarbonImmutable::now()->addSeconds($expiresInSeconds)->diffInSeconds($token->expires_at));
}

public function testGettingAccessTokenWithPasswordGrantWithInvalidPassword()
{
$password = 'foobar123';
$user = new User();
$user->email = '[email protected]';
$user->password = $this->app->make(Hasher::class)->make($password);
$user->save();

/** @var Client $client */
$client = $this->app->make(Factory::class)->of(Client::class)->state('password_client')->create(['user_id' => $user->id]);

$response = $this->post(
'/oauth/token',
[
'grant_type' => 'password',
'client_id' => $client->id,
'client_secret' => $client->secret,
'username' => $user->email,
'password' => $password.'foo',
]
);

$response->assertStatus(400);

$response->assertHeader('cache-control', 'no-cache, private');
$response->assertHeader('content-type', 'application/json');

$decodedResponse = $response->decodeResponseJson();

$this->assertArrayNotHasKey('token_type', $decodedResponse);
$this->assertArrayNotHasKey('expires_in', $decodedResponse);
$this->assertArrayNotHasKey('access_token', $decodedResponse);
$this->assertArrayNotHasKey('refresh_token', $decodedResponse);

$this->assertArrayHasKey('error', $decodedResponse);
$this->assertSame('invalid_grant', $decodedResponse['error']);
$this->assertArrayHasKey('error_description', $decodedResponse);
$this->assertArrayHasKey('hint', $decodedResponse);
$this->assertArrayHasKey('message', $decodedResponse);

$this->assertSame(0, Token::count());
}

public function testGettingAccessTokenWithPasswordGrantWithInvalidClientSecret()
{
$password = 'foobar123';
$user = new User();
$user->email = '[email protected]';
$user->password = $this->app->make(Hasher::class)->make($password);
$user->save();

/** @var Client $client */
$client = $this->app->make(Factory::class)->of(Client::class)->state('password_client')->create(['user_id' => $user->id]);

$response = $this->post(
'/oauth/token',
[
'grant_type' => 'password',
'client_id' => $client->id,
'client_secret' => $client->secret.'foo',
'username' => $user->email,
'password' => $password,
]
);

$response->assertStatus(401);

$response->assertHeader('cache-control', 'no-cache, private');
$response->assertHeader('content-type', 'application/json');

$decodedResponse = $response->decodeResponseJson();

$this->assertArrayNotHasKey('token_type', $decodedResponse);
$this->assertArrayNotHasKey('expires_in', $decodedResponse);
$this->assertArrayNotHasKey('access_token', $decodedResponse);
$this->assertArrayNotHasKey('refresh_token', $decodedResponse);

$this->assertArrayHasKey('error', $decodedResponse);
$this->assertSame('invalid_client', $decodedResponse['error']);
$this->assertArrayHasKey('error_description', $decodedResponse);
$this->assertSame('Client authentication failed', $decodedResponse['error_description']);
$this->assertArrayNotHasKey('hint', $decodedResponse);
$this->assertArrayHasKey('message', $decodedResponse);
$this->assertSame('Client authentication failed', $decodedResponse['message']);

$this->assertSame(0, Token::count());
}
}

class User extends \Illuminate\Foundation\Auth\User
{
use HasApiTokens;
}
41 changes: 40 additions & 1 deletion tests/Feature/PassportTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,64 @@
namespace Laravel\Passport\Tests\Feature;

use Illuminate\Contracts\Config\Repository;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Laravel\Passport\Passport;
use Laravel\Passport\PassportServiceProvider;
use Orchestra\Testbench\TestCase;

abstract class PassportTestCase extends TestCase
{
use DatabaseTransactions;

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

$this->withFactories(__DIR__.'/../../database/factories');

$this->artisan('migrate:fresh');

Passport::routes();

$this->artisan('passport:keys');
}

protected function getEnvironmentSetUp($app)
{
$app->make(Repository::class)->set('auth.guards.api', ['driver' => 'passport', 'provider' => 'users']);
$config = $app->make(Repository::class);

$config->set('auth.defaults.provider', 'users');

if (null !== ($userClass = $this->getUserClass())) {
$config->set('auth.providers.users.model', $userClass);
}

$config->set('auth.guards.api', ['driver' => 'passport', 'provider' => 'users']);

$config->set('database.default', 'forge');

$config->set('database.connections.forge', [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'username' => 'root',
'password' => '',
'database' => 'forge',
'prefix' => '',
]);
}

protected function getPackageProviders($app)
{
return [PassportServiceProvider::class];
}

/**
* Get the Eloquent user model class name.
*
* @return string|null
*/
protected function getUserClass(): ?string
{
return null;
}
}