Search code examples
laraveloauth-2.0laravel-passport

Why first-party clients can't handle 'authorization_code' grant type on Laravel Passport


I am working in two apps: accounts.domain.com (Laravel app) and dash.domain.com (Not laravel, but php). I want dash users to login through accounts to use the app, so I figured I could use OAuth to achieve this.

I installed Laravel Passport and everything worked fine when getting an authorization code:

$query = http_build_query([
    'client_id' => $clientId,
    'redirect_uri' => $redirectUri,
    'response_type' => 'code',
    'scope' => '*',
    'state' => $state,
]);

return redirect('https://accounts.domain.com/oauth/authorize?'.$query);

But then I tried to get the access token:

$response = $http->post('https://accounts.domain.com/oauth/token', [
    'form_params' => [
        'grant_type' => 'authorization_code',
        'client_id' => $clientId,
        'client_secret' => $clientSecret,
        'redirect_uri' => $redirectUri,
        'code' => $code,
    ],
]);

And I got this error:

{
    "error": "invalid_client",
    "error_description": "Client authentication failed",
    "message": "Client authentication failed"
}

So I googled the error, and I found that maybe there was an error with my credentials, so I check them, tried to recreate them, and nothing.

Finally I got to this file vendor/laravel/passport/src/Bridge/ClientRepository.php and I found something really interesting in the handlesGrant method that is used to verify a client:

protected function handlesGrant($record, $grantType)
{
    // ...

    switch ($grantType) {
        case 'authorization_code':
            return ! $record->firstParty();
        // ...
        default:
            return true;
    }
}

I changed this line

return ! $record->firstParty();

To this:

return $record->firstParty();

And everything worked. So, what I can see is that, using 'grant_type' => 'authorization_code' is only valid for third party clients.

My question is: ¿Why can't first party clients use 'authorization_code' as grant type? And if they can, ¿how can I implement this without changing Laravel Passport files?


Solution

  • I stumbled across the same problem, don't know why this is the default behavior. You can easily extend the ClientRepository and rebind it to the service container:

    1. Create a file app\Passport\ClientRepository.php and put the following content:
    <?php
    
    namespace App\Passport;
    
    use Laravel\Passport\Bridge\ClientRepository as BaseClientRepository;
    
    class ClientRepository extends BaseClientRepository
    {
        /**
         * Determine if the given client can handle the given grant type.
         *
         * @param  \Laravel\Passport\Client  $record
         * @param  string  $grantType
         * @return bool
         */
        protected function handlesGrant($record, $grantType)
        {
            if (is_array($record->grant_types) && ! in_array($grantType, $record->grant_types)) {
                return false;
            }
    
            switch ($grantType) {
                case 'personal_access':
                    return $record->personal_access_client && $record->confidential();
                case 'password':
                    return $record->password_client;
                case 'client_credentials':
                    return $record->confidential();
                default:
                    return true;
            }
        }
    }
    
    1. Register your ClientRepository, to rebind it to the service container:
    <?php
    
    namespace App\Providers;
    
    use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
    use App\Passport\ClientRepository;
    
    class AuthServiceProvider extends ServiceProvider
    {
        // Other code
    
        /**
         * Register any application services.
         *
         * @return void
         */
        public function register()
        {
            $this->bindClientRepository();
        }
    
        /**
         * Register the client repository.
         *
         * @return void
         */
        protected function bindClientRepository()
        {
            $this->app->bind(\Laravel\Passport\Bridge\ClientRepository::class, ClientRepository::class);
        }
    }