Passport: Specific scopes for specific clients

Created on 18 Nov 2016  路  13Comments  路  Source: laravel/passport

Hi,

sorry if this question is duplicated. I did not found anything related to this topic.

Is it possible to create clients with immmutable and predefined scopes? Here goes an example:

I have one service called Tags. It has 3 imaginary endpoints:

  • Get all -> Properly protected with token permission tags.read.
  • Create Tag -> Properly protected with token permission tags.create.
  • Delete Tag -> Properly protected with token permission tags.delete.

If I create a client and I let him to choose the scopes for the auth token request, the client can bypass my security with a simple asterisk in the scopes request attribute and I only wanted him to, for example, read and create tags.

So:

  • Am I misunderstanding something with oAuth?
  • Can't I block a client to do only the things I want him to do?
  • Are my scopes always public and free to choose for the client?

Thanks in advance, and excuse my english if I failed somewhere.

Most helpful comment

If you want full flexibility, try extending and overriding the src/Bridge/ScopeRepository class.

You can load your custom repository instead of the default one simply by binding it into the container:

// in App\Providers\AppServiceProvider:

use App\ScopeRepository;
use Illuminate\Support\ServiceProvider;
use Laravel\Passport\Bridge\ScopeRepository as PassportScopeRepository;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(PassportScopeRepository::class, ScopeRepository::class);
    }
}

By overriding the getScopeEntityByIdentifier method, you can fetch your scopes from anywhere you like, applying almost any logic you need in order to limit what scopes can be set against a token.

For example:

public function getScopeEntityByIdentifier($identifier)
{
    if (Passport::hasScope($identifier) && Auth::user()->allowedScopes->has($identifier)) {
        return new Scope($identifier);
    }
}

Just make sure that you return a src/Bridge/Scope entity instance for valid scopes.

This works because getScopeEntityByIdentifier is called by the underlying OAuth library when validating scopes, so you can benefit from that logic whilst keeping all parts of your solution (DB and JWTs) in a consistent state.

Solutions that revolve around modifying the state of a token _after_ it has been issued (and requested scopes assigned) is potentially open to unexpected 'attacks' because the actual token issued to a client may have those scope(s). If they become aware of that, they might realise they can use that authorisation to perform unintended actions... unless you're very thorough in blocking them.

All 13 comments

Hey @Aferz, the only way a client can use the * scope is by using the Password Grant. The Password Grant is only meant for first party clients (aka clients that you control). Any third party clients would have to specify a list of scopes that you make available.

I see... One more thing:

Based in my previous example, let's say I've a few third party clients in my app and I only want them to read & edit tags, so I show them the available scopes in the authorization page.

My problem is I can't avoid the client inspects the DOM, looks my scopes names (tags.read and tags.edit) and uses logic to guess others scopes (tags.delete), bypassing my security.

Is a bad design naming the scopes in a logical way ? Should I obfuscate my scopes when displaying them to the client with any kind of key-value dictionary ?

What do you think @craigpaul ?

Thank you!

@Aferz So the way Passport stands right now there is no out of the box way to limit scopes to a specific client. I don't know if that is something @taylorotwell wants for Passport, but it wouldn't be too hard to add something like that in.

Basically (if you go the database route) you would need a model for the scopes, a pivot table between oauth_clients and the new scopes table, and a middleware that would check those scopes against the scopes that belong to that client and use it on the proper routes.

Again, that could be easily implemented outside of Passport as well, really up to you as the developer. As for the bad design naming question, I don't think its bad at all, I think the names you picked for those actions are quite perfect and would keep your client's code looking quite nice (in regards to scopes anyway).

All clear now.

I think I'll try to make a pull request to implement this feature unless @taylorotwell doesn't want such thing.

Thanks for your support @craigpaul.

@Aferz did you have any luck creating a pull request? I know this is a while back now!

If not, I'll look at implementing this feature! I found it very useful in the previously recommended OAuth2 Laravel package, so much so we even created an interface to administer it.

@mstephens No I didn't. You can create the PR for the feature if you wish.

Cheers!

@Aferz How did you solve it at last? is like @craigpaul to say?

@wj5346599 I didn't use passport token scopes at all. I used the package laravel-permissions to deal with permissions/roles instead. It worked very well for me.

@Aferz well, thank you.

Just FYI I solved this problem as follows:

1) Create a user_scopes table attached to your users which has the scopes you want to assign to that user

2) Overload the withAccessToken() method in your user Class, which is part of the HasApiTokens trait. Use this hook to pull your scopes from the database for that user and assign them to the access token:

    public function withAccessToken($accessToken)
    {
        $this->accessToken = $accessToken;

        $token = $this->token();
        $token->scopes = $this->user_scopes;
        $token->save();

        return $this;
    }

Now it doesn't matter what scopes are requested, the token attached to the user is always re-written to only have the scopes attached to the user. (note in the example above user_scopes is a getter attribute that just returns an array of scope ids)

If you want full flexibility, try extending and overriding the src/Bridge/ScopeRepository class.

You can load your custom repository instead of the default one simply by binding it into the container:

// in App\Providers\AppServiceProvider:

use App\ScopeRepository;
use Illuminate\Support\ServiceProvider;
use Laravel\Passport\Bridge\ScopeRepository as PassportScopeRepository;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(PassportScopeRepository::class, ScopeRepository::class);
    }
}

By overriding the getScopeEntityByIdentifier method, you can fetch your scopes from anywhere you like, applying almost any logic you need in order to limit what scopes can be set against a token.

For example:

public function getScopeEntityByIdentifier($identifier)
{
    if (Passport::hasScope($identifier) && Auth::user()->allowedScopes->has($identifier)) {
        return new Scope($identifier);
    }
}

Just make sure that you return a src/Bridge/Scope entity instance for valid scopes.

This works because getScopeEntityByIdentifier is called by the underlying OAuth library when validating scopes, so you can benefit from that logic whilst keeping all parts of your solution (DB and JWTs) in a consistent state.

Solutions that revolve around modifying the state of a token _after_ it has been issued (and requested scopes assigned) is potentially open to unexpected 'attacks' because the actual token issued to a client may have those scope(s). If they become aware of that, they might realise they can use that authorisation to perform unintended actions... unless you're very thorough in blocking them.

@simonhamp would it be more secure to override/extend the finalizeScopes() method of that same class?

The oauth2.thephpleague.com documentation mentions :

This method is called right before an access token or authorization code is created.

Given a client, grant type and optional user identifier validate the set of scopes requested are valid and optionally append additional scopes or remove requested scopes.

This method is useful for integrating with your own app鈥檚 permissions system.

Since the scopes are attached to the token BEFORE it is created, the client would not be aware of these "unwanted" scopes...

As @simonhamp said you can check if the requested scopes are valid in getScopeEntityByIdentifier and return the scope object but for checking if user is allowed to take the scope you should go to finalizeScopes method.You'll have the user id there and there is a point that in this part you are just validating scopes and user isn't authenticated yet so you cant use Auth facade.I wrote the below code for finalizeScopes

public function finalizeScopes(array $scopes, $grantType, ClientEntityInterface $clientEntity, $userIdentifier = null)
{
    $finalizedScopes = [];
    if (reset($scopes)->getIdentifier() == '*')
        $scopes = $this->getAllScopes(); 

    foreach ($scopes as $scope) {
        if (in_array($scope->getIdentifier(), User::find($userIdentifier)->permissions))
            array_push($finalizedScopes, $scope);
    }

    return $finalizedScopes;
}
 /**
 * @return ScopeEntityInterface[]
 */
private function getAllScopes()
{
    $scopes = [];
    foreach (Passport::scopes() as $passportScope)
        array_push($scopes, $this->getScopeEntityByIdentifier($passportScope->id));

    return $scopes;
}

If you don't want to check user permissions you should just write return $scopes; in the method

Was this page helpful?
0 / 5 - 0 ratings

Related issues

s4uron picture s4uron  路  3Comments

ghost picture ghost  路  3Comments

mehrancodes picture mehrancodes  路  3Comments

seriousjelly picture seriousjelly  路  3Comments

MarkVilludo picture MarkVilludo  路  3Comments