Passport: Validating Scopes on Client Credentials Grant.

Created on 10 Apr 2017  路  16Comments  路  Source: laravel/passport

It's not clear how do we check the scopes on our routes with Client Credential Grant. Whenever I try to check for the scope I always get "unauthenticated" error with both CheckScopes Middleware or CheckForAnyScope Middleware.

/**
     * Handle the incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  array  $scopes
     * @return \Illuminate\Http\Response
     */
    public function handle($request, $next, ...$scopes)
    {
        if (! $request->user() || ! $request->user()->token()) {
            throw new AuthenticationException;
        }

        foreach ($scopes as $scope) {
            if ($request->user()->tokenCan($scope)) {
                return $next($request);
            }
        }

        throw new MissingScopeException($scopes);
    }

On going through the code above of CheckForAnyScope middleware I noticed that the problem is most probably arising on the first if statement where it checks for the user and since there is no user assigned in the Client Credential Grant, the request is terminated with the "unauthenticated error". Is there a any way we can fix this and check for the scopes as we normally check with the other grant types.

Most helpful comment

First, you need to use a different middleware class when working with the Client Credentials grant. I don't think this was indicated in the docs, but the right class is Laravel\Passport\Http\Middleware\CheckClientCredentials. Add that to your Kernel::routeMiddleware array so you can use it to secure your routes that expect tokens created via this grant. As for scopes, the CheckClientCredentials::handle() method expects remaining arguments to be your scopes, so you can pass what you need for your routes and the class should do the scope validation against the supplied token for you. So basically

Kernel.php

protected $routeMiddleware = [
    ...,
    ...,
    'auth_client' => \Laravel\Passport\Http\Middleware\CheckClientCredentials::class,
];

routes/api.php

$api = app('Dingo\Api\Routing\Router');
$api->group(['middleware' => ['auth_client:scope1,scope2']], function () use ($api) {
    $api->get('index', ['uses' => 'Path\To\Controllers\MyController@index']);
});

Sorry, just quickly pulled code from a project I'm working on that uses Dingo. Laravel's routing syntax should be pretty similar though.

The passport built-in scope middleware classes are probably useful for the other grant types that include a user context.

All 16 comments

First, you need to use a different middleware class when working with the Client Credentials grant. I don't think this was indicated in the docs, but the right class is Laravel\Passport\Http\Middleware\CheckClientCredentials. Add that to your Kernel::routeMiddleware array so you can use it to secure your routes that expect tokens created via this grant. As for scopes, the CheckClientCredentials::handle() method expects remaining arguments to be your scopes, so you can pass what you need for your routes and the class should do the scope validation against the supplied token for you. So basically

Kernel.php

protected $routeMiddleware = [
    ...,
    ...,
    'auth_client' => \Laravel\Passport\Http\Middleware\CheckClientCredentials::class,
];

routes/api.php

$api = app('Dingo\Api\Routing\Router');
$api->group(['middleware' => ['auth_client:scope1,scope2']], function () use ($api) {
    $api->get('index', ['uses' => 'Path\To\Controllers\MyController@index']);
});

Sorry, just quickly pulled code from a project I'm working on that uses Dingo. Laravel's routing syntax should be pretty similar though.

The passport built-in scope middleware classes are probably useful for the other grant types that include a user context.

Thanks @georaldc, That's working for me. Actually I already did adding the middleware part in Kernal.php but I was a bit confused about how to use scopes, as Laravel Passport asks us to use CheckScopes and CheckForAnyScopes middleware and Those were returning errors for me.

Thanks Again.

Sorry, but what if we want to use some endpoint to be accessed by both Client Credentials Grant and Other Grants as well. Isn't that a problem here?

I think then you would need to create your own middleware that can distinguish the type of token that was used. I actually have the same problem at the moment, but I might just end up creating Client Grant specific endpoints and User Specific ones. Might even be easier to maintain than trying to get my code to adjust depending on the token that was sent

Yeah that's probably the one solution to create our own middleware but that'd just complicate the things. So it'd be better if I do as what you did "creating Client Grant specific endpoints and User Specific ones".

Thanks.

Thank you, I was about to reinvent the wheel creating a middleware for Client users.
:tada: :laughing:

in docs it said check scope using scopes Check For All Scopes and scope Check For Any Scopes, @georaldc auth_client:scope1,scope2 is the which one Check For All Scopes or Check For Any Scopes

https://laravel.com/docs/5.6/passport#token-scopes

But how do i add scopes to client credentials? When a user requests a token from its client credentials he can request a token for any scope. how can i limit a client to only be allowed certain scopes?

@joelharkes While requesting for the token you can add a parameter to the query with name "scope" and assign it the value of whatever scopes you wanna give it ("multiple scopes should be separated with a space").

https://laravel.com/docs/5.6/passport#token-scopes

Visit the above provided link and scroll-down to "Assigning Scopes To Tokens".

@abhishek6262 no i want to limit the scopes a client can use. But i want to properly store these scopes in access token, and not check afterwards on every request based on the client.

Its a bit hacky but I found a working solution:

class ScopeRepository extends \Laravel\Passport\Bridge\ScopeRepository
{
    public function finalizeScopes(array $scopes, $grantType, ClientEntityInterface $clientEntity, $userIdentifier = null)
    {
        if ('client_credentials' === $grantType) {
            $scopes = $this->processClientCredentialScopes($scopes, $clientEntity);
        }
        return parent::finalizeScopes($scopes, $grantType, $clientEntity, $userIdentifier);
    }

    /**
     * @param \Laravel\Passport\Bridge\Scope[] $scopes
     * @param ClientEntityInterface $clientEntity
     * @return \Laravel\Passport\Bridge\Scope[]
     */
    protected function processClientCredentialScopes($scopes, ClientEntityInterface $clientEntity)
    {
        if (0 === count($scopes) || (1 === count($scopes) && '*' === $scopes[0]->getIdentifier())) {
            $scopes = Passport::scopes()->map(function (Scope $scope) {
                return new \Laravel\Passport\Bridge\Scope($scope->id);
            })->all();
        }
        $client = app(ClientRepository::class)->find($clientEntity->getIdentifier());
        $clientScopes = explode(' ', $client->scopes);

        return collect($scopes)->filter(function (ScopeEntityInterface $scope) use ($clientScopes) {
            return in_array($scope->getIdentifier(), $clientScopes, true);
        })->all();
    }
}

than bind/overwrite this in the container:

        $this->app->bind(\Laravel\Passport\Bridge\ScopeRepository::class, ScopeRepository::class);

and the migration needed for this:

class AddScopesColumnToOauthClients extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('oauth_clients', function (Blueprint $table) {
            $table->text('scopes')->nullable()->after('role');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('oauth_clients', function (Blueprint $table) {
            $table->dropColumn('scopes');
        });
    }
}

you will get a scopes column in oauth_clients table this should now be a comma seperated list of scopes a client can use. if a client requests other scopes they will be filtered out before access token is created.

todo: return list of scopes from the api call so user knows what scopes he gets.

class BearerTokenResponse extends \League\OAuth2\Server\ResponseTypes\BearerTokenResponse
{
    protected function getExtraParams(AccessTokenEntityInterface $accessToken)
    {
        $scopes = array_map(function ($scope) {
            return $scope->getIdentifier();
        }, $accessToken->getScopes());

        return ['scopes' => $scopes];
    }
}

and in passport service provier:

    public function makeAuthorizationServer()
    {
        return new AuthorizationServer(
            $this->app->make(\Laravel\Passport\Bridge\ClientRepository::class),
            $this->app->make(\Laravel\Passport\Bridge\AccessTokenRepository::class),
            $this->app->make(\Laravel\Passport\Bridge\ScopeRepository::class),
            $this->makeCryptKey('private'),
            app('encrypter')->getKey(),
            new BearerTokenResponse()
        );
    }

now the request returns the scopes that where accepted.

I checked source and recently they added class CheckClientCredentialsForAnyScope, is an alternative to CheckForAnyScope.
It worked for me.

Without @joelharkes additional code it means that it's the client which decides what scopes he can get when requiring access tokens. What's the point then to restrict endpoint with scopes if the client can get all the scopes he want ? Do I miss something ?

If you cannot trust your API clients, so you should use the "code" grant type, instead of any other (client_credentials or password grant types).
Using the "code" grant type, the user will be notified of the scopes the client is requesting and can accept or not.

Well I didn't knew the code grand type as I sticked to Laravel 5.8 compatible Passport package.

But still I think it is not relevant, the problem is not that I can't trust how my clients store their tokens it is more than I don't want my client to be the only one to decide which scopes it should get. And client to client use case means that maybe no user model is linked to a client.

Laravel Passport supports multiple grant types, including the authorization code.
If you don't want your clients to decide which scopes to select, as they may use more than they need, so you cannot trust your clients and you should use authorization code instead, to give clear usage to the users, before allowing any client.
Normally, if the client is not created by you ar your team, so you probably should not trust it and use authorization code, instead of password or similars.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rudolfdobias picture rudolfdobias  路  3Comments

SwiTool picture SwiTool  路  3Comments

raksrivastava picture raksrivastava  路  3Comments

duccanh0022 picture duccanh0022  路  3Comments

cookiejarblush picture cookiejarblush  路  4Comments