Passport: invalid_grant error for password client non existing user

Created on 6 Dec 2019  路  7Comments  路  Source: laravel/passport

  • Passport Version: 8.0.2
  • Laravel Version: 6.6.2
  • PHP Version: 7.3.11
  • Database Driver & Version: MySQL 5.7.23

Description:

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.

Steps To Reproduce:

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.

needs more info

Most helpful comment

All 7 comments

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

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MarkVilludo picture MarkVilludo  路  3Comments

duccanh0022 picture duccanh0022  路  3Comments

raksrivastava picture raksrivastava  路  3Comments

ghost picture ghost  路  3Comments

brryfrmnn picture brryfrmnn  路  3Comments