Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Passport Multi-Auth (Implementation) #982

Open
billriess opened this issue Mar 4, 2019 · 19 comments

Comments

@billriess
Copy link

commented Mar 4, 2019

Since #161 has been locked we are no longer able to discuss the issue. Before I submit a PR I want to make sure we have a common ground on what the expected behavior is for multi-provider support in Passport. More specifically, should the provider be defined at the oauth client level? This would limit that client id/secret pair to always return from a specific provider. If the gate requested another provider than what the client has defined it would return a 401.

This is the functionality I have built into my current project and I would be more than willing to issue a PR for it but I want to make sure that is what everyone is expecting.

EDIT: I should note, that if no provider is set on the oauth client then it will follow the same logic as what is in place now, so it should be backward compatible outside of the migration.

@driesvints driesvints changed the title Passport Multi Provider Passport Multi-Auth (Implementation) Mar 5, 2019

@driesvints

This comment has been minimized.

Copy link
Member

commented Mar 5, 2019

I agree that this issue might be a good place to restart the discussion on how to implement this. I currently don't have time myself to look into this but feel free to provide thoughts here.

Warning: I'll be removing all comments which aren't related to looking for an actual implementation to prevent this issue from de-railing like the other one. Please only discuss the possible implementation for this.

@billriess

This comment has been minimized.

Copy link
Author

commented Mar 6, 2019

I think to get this started we should first determine if the multi-auth implementation should be enforced or not. Meaning, should the gate simply return the user against its own provider or should the client have a provider that enforces the user must come from a specific provider?

@pramanikriju

This comment has been minimized.

Copy link

commented Mar 11, 2019

Or to make it more dynamic, the provider can be supplied in the request and the guard check can be made against that request parameter to make it compatible with as many models as required

@afilippov1985

This comment has been minimized.

Copy link

commented Mar 12, 2019

Hello everyone.
Take a look at my proposal #987

@billriess

This comment has been minimized.

Copy link
Author

commented Mar 14, 2019

Personally, I think each oauth_client should be limited to 1 provider but I can see how this may be limiting in some use cases. If we open it up to multiple providers per client then the developer will always be responsible for returning the correct user against any of the allowed providers.

@pramanikriju

This comment has been minimized.

Copy link

commented Mar 14, 2019

Personally, I think each oauth_client should be limited to 1 provider but I can see how this may be limiting in some use cases. If we open it up to multiple providers per client then the developer will always be responsible for returning the correct user against any of the allowed providers.

But isn't that the point of having multi-auth, to have the flexibility of choosing a provider but at the same time, using fallbacks for default behavior, like using the User model as is the case now.

Personally, I too think that each oauth_client should have one provider but its better to have the implementation for multiple providers as it is might offer more functionality.

We should probably gather more opinions on this and implement the consensus then.

@laravel laravel deleted a comment from rugiguru Mar 18, 2019

@laravel laravel deleted a comment from billriess Mar 18, 2019

@driesvints

This comment has been minimized.

Copy link
Member

commented Mar 18, 2019

Please remember to keep this discussion on topic.

@pramanikriju

This comment has been minimized.

Copy link

commented Mar 24, 2019

@billriess When can we expect a PR with the functionality that you have implemented?

@billriess

This comment has been minimized.

Copy link
Author

commented Mar 24, 2019

I was hoping for some more feedback as to what way would make the most sense for everyone. I can put a PR together later this week with my implementation and see how that goes.

@nikugogoi

This comment has been minimized.

Copy link

commented Mar 28, 2019

I think the provider should be based on the routes... therefore different routes will have different providers.
Suppose there is an admin model. We can set different routes in the boot() method of AuthServiceProvider like so...

// Admin passport routes
        Passport::routes(
            null,
            [
                'as' => 'admin.',
                'prefix' => 'admin/oauth',
                'middleware' => 'passport.guard:admin',
            ]
        );

Here I am assigning a middleware(which needs to be implemented accordingly) to the the routes as to tell passport to use the admin guard which decides the provider to be used.
What do you folks think? @billriess @driesvints

@zagreusinoz

This comment has been minimized.

Copy link

commented Mar 30, 2019

I think that if you're using Passport then it should be relatively safe to assume that you're not a beginner developer, and for that reason it should be as flexible as possible even if that means that a little work is required of the developer.

I think it makes sense to have separate endpoints per your suggestion @nikugogoi for the various models so long as there is flexibility in the way those models are authenticated -- some models may not use email / password for example, we may use uuid/id and secret key or some such authentication.

As long as the implementation allows us to attach multiple guards to a single api endpoint, e.g. if i wanted either users, or admins, or whoever to be able to access the resource.

@nikugogoi

This comment has been minimized.

Copy link

commented Mar 30, 2019

@zagreusinoz for customizing the way how the models are authenticated, there is a way here https://laravel.com/docs/5.8/passport#customizing-the-username-field
Using guards, with passport as the authentication driver, on endpoints work well to restrict access resource... The problem arises during issue of tokens as passport is currently only configured for one guard. I feel there should be a way to assign guards according to different routes.
Also the I think the passport tokens table will need a change.

@billriess

This comment has been minimized.

Copy link
Author

commented Apr 5, 2019

The implementation I have in place locally in my project is that oauth_clients now has a provider column that determines the given provider. If it is null it defaults back to users but if it is set then it will only authenticate against with its defined provider. In my routes you can specify auth:api for standard user-based authorization while something like auth:customers will only authorize the customers provider.

@tmcnicholls

This comment has been minimized.

Copy link

commented Apr 9, 2019

@billriess are you able to share even a draft PR with your current implementation? This would solve some issues I'm having in a project.

I agree that tying an oauth_client to a specific provider would be the desired functionality.

@billriess

This comment has been minimized.

Copy link
Author

commented Apr 9, 2019

@tmcnicholls I've been a little busy recently with a project but I will put something together and submit it. I know PRs are viewed pretty closely here so I want to make sure the implementation I suggest and the way I implement it will be the desired approach. The way I got this working locally was changing the TokenGuard's construct to accept a $requestedProvider instead of passing an already formed Provider this is what lets me do the check. Then I can create the appropriate Provider and pass that along.

@laravel laravel deleted a comment from pramanikriju Apr 23, 2019

@andrewmclagan

This comment has been minimized.

Copy link

commented May 16, 2019

@billriess I'd like to see you go ahead with your current implementation around each oauth_client having a provider. Over the last few years my team has been looking at ways to implement multi-auth with passport - all seem a hack as there is no native support.

+1 for your implementation. I think to get things moving a fully fledged PR would go along-way. Also be very appreciated by the community.

@billriess

This comment has been minimized.

Copy link
Author

commented May 17, 2019

Sorry all, I've been placed on a very hectic project (I'm sure you all know what that's like!) I have not had a chance to write a proper PR for this. I can share the code I have in place to help you guys somewhere to start. Hopefully this will be allowed until I do get a chance to write a proper PR.

/app/Passport/PassportServiceProvider.php

<?php

namespace App\Passport;

use Illuminate\Auth\RequestGuard;
use Laravel\Passport\ClientRepository;
use Laravel\Passport\TokenRepository;
use League\OAuth2\Server\ResourceServer;

class PassportServiceProvider extends \Laravel\Passport\PassportServiceProvider
{

    /**
     * Make an instance of the token guard.
     *
     * @param  array $config
     * @return RequestGuard
     */
    protected function makeGuard(array $config)
    {
        return new RequestGuard(function ($request) use ($config) {
            return (new TokenGuard(
                $this->app->make(ResourceServer::class),
                // Instead of passing a fully generated provider, we will pass the requested provider. This way we can
                // validate the requested provider against the oauth client if need be.
                $config['provider'],
                $this->app->make(TokenRepository::class),
                $this->app->make(ClientRepository::class),
                $this->app->make('encrypter')
            ))->user($request);
        }, $this->app['request']);
    }
}

/app/Passport/TokenGuard.php

<?php

namespace App\Passport;

use Cache;
use Illuminate\Encryption\Encrypter;
use Laravel\Passport\ClientRepository;
use Laravel\Passport\TokenRepository;
use Laravel\Passport\TransientToken;
use League\OAuth2\Server\ResourceServer;

class TokenGuard extends \Laravel\Passport\Guards\TokenGuard
{
    protected $requestedProvider;

    /**
     * Create a new token guard instance.
     *
     * @param ResourceServer $server
     * @param String $requestedProvider
     * @param TokenRepository $tokens
     * @param ClientRepository $clients
     * @param Encrypter $encrypter
     */
    public function __construct(
        ResourceServer $server,
        String $requestedProvider,
        TokenRepository $tokens,
        ClientRepository $clients,
        Encrypter $encrypter
    ) {
        $this->server = $server;
        $this->tokens = $tokens;
        $this->clients = $clients;
        $this->requestedProvider = $requestedProvider;
        $this->encrypter = $encrypter;
    }

    /**
     * Authenticate the incoming request via the Bearer token.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return mixed
     */
    protected function authenticateViaBearerToken($request)
    {
        if (! $psr = $this->getPsrRequestViaBearerToken($request)) {
            return;
        }

        // Retrieve the oauth client from the access token.
        $client = Cache::rememberForever(
            'oauth_client:'.$psr->getAttribute('oauth_client_id'),
            function () use ($psr) {
                return \Laravel\Passport\Client::findOrFail($psr->getAttribute('oauth_client_id'));
            }
        );

        // If the oauth client has a defined provider make sure it matches the requested provider.
        if ($client->provider && $client->provider !== $this->requestedProvider) {
            return;
        }

        // Create a user provider based on the requested provider.
        $this->provider = \Illuminate\Support\Facades\Auth::createUserProvider($this->requestedProvider);

        // If the access token is valid we will retrieve the user according to the user ID
        // associated with the token. We will use the provider implementation which may
        // be used to retrieve users from Eloquent. Next, we'll be ready to continue.
        $user = $this->provider->retrieveById(
            $psr->getAttribute('oauth_user_id') ?: null
        );

        if (! $user) {
            return;
        }

        // Next, we will assign a token instance to this user which the developers may use
        // to determine if the token has a given scope, etc. This will be useful during
        // authorization such as within the developer's Laravel model policy classes.
        $token = $this->tokens->find(
            $psr->getAttribute('oauth_access_token_id')
        );

        $clientId = $psr->getAttribute('oauth_client_id');

        // Finally, we will verify if the client that issued this token is still valid and
        // its tokens may still be used. If not, we will bail out since we don't want a
        // user to be able to send access tokens for deleted or revoked applications.
        if ($this->clients->revoked($clientId)) {
            return;
        }

        return $token ? $user->withAccessToken($token) : null;
    }

    /**
     * Authenticate the incoming request via the token cookie.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return mixed
     */
    protected function authenticateViaCookie($request)
    {
        if (! $token = $this->getTokenViaCookie($request)) {
            return;
        }

        // Create a user provider based on the requested provider.
        $this->provider = \Illuminate\Support\Facades\Auth::createUserProvider($this->requestedProvider);

        // If this user exists, we will return this user and attach a "transient" token to
        // the user model. The transient token assumes it has all scopes since the user
        // is physically logged into the application via the application's interface.
        if ($user = $this->provider->retrieveById($token['sub'])) {
            return $user->withAccessToken(new TransientToken);
        }
    }
}

/composer.json

...
        "psr-4": {
            "App\\": "app/",
            "League\\OAuth2\\Server\\Grant\\": "app/passport"
        }
...

I have a few other Passport modifications in my project but I think this is all you need to implement multiple providers. If something doesn't work here let me know and I'll check if I'm missing something. Note that I'm caching the client to prevent the extra queries per each request. Feel free to remove that if you don't need/want that.

@pramanikriju

This comment has been minimized.

Copy link

commented Jul 29, 2019

What's the timeframe for this issue, if any at all?

@driesvints

This comment has been minimized.

Copy link
Member

commented Jul 30, 2019

@pramanikriju whenever someone submits a PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
8 participants
You can’t perform that action at this time.