When I try and get an access token for a user that I know does not exist I get and invalid_grant error instead of an invalid_credentials error.
My feature test fails
/** @test */
public function it_cannot_login_a_non_existent_user() //<---- FAILS
{
$response = $this->json('POST', 'oauth/token', [
'grant_type' => 'password',
'client_id' => $this->passwordClient->id,
'client_secret' => $this->passwordClient->secret,
'username' => '[email protected]',
'password' => 'secret',
'scope' => '*'
]);
$response->assertStatus(401)
->assertJson([
'error' => 'invalid_credentials'
]);
}
I can successfully login with a user that exists.
/** @test */
public function it_logs_in_a_customer() //<---- PASSES
{
$customer = factory(Customer::class)->create([
'email' => '[email protected]',
'password' => 'secret'
]);
$response = $this->json('POST', 'oauth/token', [
'grant_type' => 'password',
'client_id' => $this->passwordClient->id,
'client_secret' => $this->passwordClient->secret,
'username' => $customer->email,
'password' => 'secret',
'scope' => '*'
]);
$response->assertStatus(200)
->assertJson([
'token_type' => 'Bearer'
]);
}
Downgrading to passport ^7.5 allows both tests to pass.
Is there code we're missing here? How's the client setup?
@driesvints This is how I setup the test to create the password client
/**
* @var bool
*/
public $mockConsoleOutput = false;
/**
* @var
*/
protected $passwordClient;
/**
* Setup tests
*/
public function setUp(): void
{
parent::setUp();
$this->artisan('passport:client', ['--password' => null, '--no-interaction' => true]);
$this->passwordClient = DB::table('oauth_clients')->where('password_client', 1)->first();
}
The TestCase also uses the RefreshDatebase Trait so there's no pre existing client to worry about.
Can you post the full testcase?
@driesvints
namespace Tests;
use App\Models\User;
use App\Models\Staff;
use App\Models\Customer;
use Laravel\Passport\Passport;
use Illuminate\Foundation\Testing\TestResponse;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication, RefreshDatabase;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->seed('RolesAndPermissionsTableSeeder');
}
/**
* Send request as a user.
*
* @param $user
* @param $method
* @param $endpoint
* @param array $data
* @param array $headers
*
* @return \Illuminate\Foundation\Testing\TestResponse
*/
public function jsonAs($user, $method, $endpoint, $data = [], $headers = []): TestResponse
{
Passport::actingAs($user);
return $this->json($method, $endpoint, $data, $headers);
}
/**
* Send request as a user.
*
* @param $method
* @param $endpoint
* @param array $data
* @param array $headers
*
* @return TestResponse
*/
public function jsonAsUser($method, $endpoint, $data = [], $headers = []): TestResponse
{
$user = factory(User::class)->create();
return $this->jsonAs($user, $method, $endpoint, $data, $headers);
}
/**
* Send request as a customer.
*
* @param $method
* @param $endpoint
* @param array $data
* @param array $headers
*
* @return TestResponse
*/
public function jsonAsCustomer($method, $endpoint, $data = [], $headers = []): TestResponse
{
$customer = factory(Customer::class)->create();
return $this->jsonAs($customer, $method, $endpoint, $data, $headers);
}
/**
* Send request as a supplier.
*
* @param $method
* @param $endpoint
* @param array $data
* @param array $headers
*
* @return TestResponse
*/
public function jsonAsSupplier($method, $endpoint, $data = [], $headers = []): TestResponse
{
$supplier = factory(Supplier::class)->create();
return $this->jsonAs($supplier, $method, $endpoint, $data, $headers);
}
/**
* Mark test as incomplete.
*/
public function todo(): void
{
$testMethod = debug_backtrace()[1]['function'];
$this->markTestIncomplete('Incomplete: ' . $testMethod);
}
/**
* Mark test as incomplete.
*/
public function wip(): void
{
$testMethod = debug_backtrace()[1]['function'];
$this->markTestIncomplete('Feature is a WIP: ' . $testMethod);
}
}
namespace Tests\Feature\Api\Auth;
use Tests\TestCase;
use App\Models\Customer;
use Illuminate\Support\Facades\DB;
class LoginTest extends TestCase
{
/**
* @var bool
*/
public $mockConsoleOutput = false;
/**
* @var
*/
protected $passwordClient;
/**
* Setup tests
*/
public function setUp(): void
{
parent::setUp();
$this->artisan('passport:client', ['--password' => null, '--no-interaction' => true]);
$this->passwordClient = DB::table('oauth_clients')->where('password_client', 1)->first();
}
/** @test */
public function it_logs_in_a_customer()
{
$customer = factory(Customer::class)->create([
'email' => '[email protected]',
'password' => 'secret'
]);
$response = $this->json('POST', 'oauth/token', [
'grant_type' => 'password',
'client_id' => $this->passwordClient->id,
'client_secret' => $this->passwordClient->secret,
'username' => $customer->email,
'password' => 'secret',
'scope' => '*'
]);
$response->assertStatus(200)
->assertJson([
'token_type' => 'Bearer'
]);
}
/** @test */
public function it_cannot_login_a_non_existent_user()
{
$response = $this->json('POST', 'oauth/token', [
'grant_type' => 'password',
'client_id' => $this->passwordClient->id,
'client_secret' => $this->passwordClient->secret,
'username' => '[email protected]',
'password' => 'secret',
'scope' => '*'
]);
$response->assertStatus(401)
->assertJson([
'error' => 'invalid_credentials'
]);
}
}
This is a change in OAuth 2 server: https://github.com/thephpleague/oauth2-server/pull/967
See https://github.com/thephpleague/oauth2-server/blob/master/CHANGELOG.md#changed-1
Is there any way to map this server-side to return a more sane error? My API clients are displaying the message as it is sent by the server and this message is extremely confusing. It would be a heavy lift to redeploy an update, I would rather the server return a comprehendible message.
Most helpful comment
This is a change in OAuth 2 server: https://github.com/thephpleague/oauth2-server/pull/967
See https://github.com/thephpleague/oauth2-server/blob/master/CHANGELOG.md#changed-1