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 #161

Closed
SidharthRaveendran opened this issue Oct 27, 2016 · 80 comments
Closed

Passport Multi-Auth #161

SidharthRaveendran opened this issue Oct 27, 2016 · 80 comments

Comments

@SidharthRaveendran
Copy link

Would passport be implementing Multi-Auth system? or is there another way to incorporate the multi-auth into the app.

I have created Multiple Models which use Laravel's Auth system to implement proper multi-auth system. I'm not sure how to use passport for the same.

@eepawan
Copy link

eepawan commented Nov 28, 2016

I am also looking for same. Anyone here to help?

@jenky
Copy link

jenky commented Dec 12, 2016

if (is_null($model = config('auth.providers.users.model'))) {

This package always uses users provider model in config/auth to authenticate, so I guess there are no hacks for now?

@yanko-belov
Copy link

+1

@gauravmak
Copy link
Contributor

With #216 , it is dynamic now so multiple guards can be added and used with passport now I believe.

@SidharthRaveendran
Copy link
Author

Still we cannot implement multi-auth using laravel using this update.

@zubair1
Copy link

zubair1 commented Feb 19, 2017

I managed to get access tokens from different auth providers in laravel, but I had to modify:


File: vendor\laravel\passport\src\Bridge\UserRepository.php

  • Copy / Paste getUserEntityByUserCredentials to make a duplicate of it and name it getEntityByUserCredentials

  • Then, in the new duplicated function, find the below:
    $provider = config('auth.guards.api.provider');
    and Replace it with:
    $provider = config('auth.guards.'.$provider.'.provider');


File: vendor\league\oauth2-server\src\Grant\PasswordGrant.php

  • Find Line: 94
  • Update with the following code:
        $user = $this->userRepository->getEntityByUserCredentials(
            $username,
            $password,
            $this->getIdentifier(),
            $client,
            $provider
        );

After doing this you'll be able to pass an extra key/value pair to your access token request, like for example:

grant_type => password,
client_id => someclientid
client_secret => somesecret,
username => someuser,
password => somepass,
client_scope => *,
provider => api

In this example, I set provider to 'api', which is default. But now I can pass other auth providers as part of the request. So, just to show another example, I could do the following:

grant_type => password,
client_id => someclientid
client_secret => somesecret,
username => someuser,
password => somepass,
client_scope => *,
provider => api_admins

Now I used 'api_admins' auth provider to authenticate tokens.

P.S:
I replaced the functions with new ones instead of overwriting because there was another third-party library involved league\oauth2-server, besides the laravel/passport library. If I were to recommend a proper way to implement this would be to extend the getUserEntityByUserCredentials function so that it could accept our new argument, but for that you would have to overwrite the interface function definition too, it's located at vendor\league\oauth2-server\src\Repositories\RepositoryInterface.php

Hope this helps someone else

@MohammedSabbah
Copy link

@zubair1 zubair1 I have tried your method and it worked, let me rewrite the steps more clearly,

  1. File: vendor\laravel\passport\src\Bridge\UserRepository.php
  • Copy / Paste getUserEntityByUserCredentials to make a duplicate of it and name it getEntityByUserCredentials

  • Then, in the new duplicated function, find the below:
    $provider = config('auth.guards.api.provider');
    and Replace it with:
    $provider = config('auth.guards.'.$theNewProvider.'.provider');

  • After that add $theNewProvider to getEntityByUserCredentials arguments as follow:

      public function getEntityByUserCredentials($username, $password, $grantType, 
      ClientEntityInterface $clientEntity, $theNewProvider)
    

2)File: vendor\league\oauth2-server\src\Grant\PasswordGrant.php

  • In the function validateUser update with the following code:

      $user = $this->userRepository->getEntityByUserCredentials(
          $username,
          $password,
          $this->getIdentifier(),
          $client,
          $theNewProvider
    
  • add the following variable to the same function:

     $theNewProvider = $this->getRequestParameter('theNewProvider', $request);
    
     if (is_null($theNewProvider)) {
     throw OAuthServerException::invalidRequest('theNewProvider');
     }
    
  1. Try it with Postman with the following:

url: http://127.0.0.1:8000/oauth/token
Headers:
Content-Type: application/json

body:

{
"username":"test@gmail.com",
"password":"secret",
"grant_type": "password",
    "client_id": 2,
    "client_secret": "string",
    "theNewProvider": "admin"
}

Do not forget to do the following as well:

  • Update the model, for example in the admin model add the following

      use Laravel\Passport\HasApiTokens;
      class Admin extends Authenticatable
      {
           use Notifiable,HasApiTokens;
    
  • In auth.php:

      'guards' => [
      'admin' => [
          'driver' => 'passport',
          'provider' => 'admins',
      ],
    

@reisnobre
Copy link

Nothing new until now?

@renanwilliam
Copy link

renanwilliam commented Jun 14, 2017

I have made a very simple hack to do it:

  1. Add a new custom user provider in config/auth.php using a model that extends Authenticatable class and use HasRoles, HasApiTokens traits.
<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Authentication Defaults
    |--------------------------------------------------------------------------
    |
    | This option controls the default authentication "guard" and password
    | reset options for your application. You may change these defaults
    | as required, but they're a perfect start for most applications.
    |
    */

    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

    /*
    |--------------------------------------------------------------------------
    | Authentication Guards
    |--------------------------------------------------------------------------
    |
    | Next, you may define every authentication guard for your application.
    | Of course, a great default configuration has been defined for you
    | here which uses session storage and the Eloquent user provider.
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | Supported: "session", "token"
    |
    */

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | If you have multiple user tables or models you may configure multiple
    | sources which represent each model / table. These sources may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],

        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Administrator::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Resetting Passwords
    |--------------------------------------------------------------------------
    |
    | You may specify multiple password reset configurations if you have more
    | than one user table or model in the application and you want to have
    | separate password reset settings based on the specific user types.
    |
    | The expire time is the number of minutes that the reset token should be
    | considered valid. This security feature keeps tokens short-lived so
    | they have less time to be guessed. You may change this as needed.
    |
    */

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
        ],

        'admins' => [
            'provider' => 'admins',
            'table' => 'password_resets',
            'expire' => 60,
        ],
    ],

];
  1. Create a new custom middleware PassportCustomProvider like that:
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Config;

class PassportCustomProvider
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $params = $request->all();
        if (array_key_exists('provider', $params)) {
            Config::set('auth.guards.api.provider', $params['provider']);
        }
        return $next($request);
    }
}
  1. Register the middleware as a route middleware:
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */
    protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ];

    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            'throttle:60,1',
            'bindings',
        ],
    ];

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'passport-administrators' => \App\Http\Middleware\PassportCustomProvider::class,
    ];
}
  1. Encapsulate the passport routes with this middleware in AuthServiceProvider:
<?php

namespace App\Providers;

use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Passport;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Route::group(['middleware' => 'passport-administrators'], function () {
            Passport::routes();
        });
    }

}
  1. Add the 'provider' param in your request at /oauth/token:
POST /oauth/token HTTP/1.1
Host: localhost
Accept: application/json, text/plain, */*
Content-Type: application/json;charset=UTF-8
Cache-Control: no-cache

{
	"username":"user@domain.com",
	"password":"password",
	"grant_type" : "password",
	"client_id": "client-id",
	"client_secret" : "client-secret",
	"provider" : "admins"
}

The problem after this step is the token saved in oauth_access_tokens only contains the administrator ID. When we use the token received to authenticate, it not look at Administrator models.

@renanwilliam
Copy link

renanwilliam commented Jun 14, 2017

Complementing the above comment, to make it functional for Bearer tokens it's necessary the following steps:

  1. Create a migration to save the relationship between access tokens and providers:
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class OauthAccessTokenProviders extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('oauth_access_token_providers', function (Blueprint $table) {
            $table->string('oauth_access_token_id', 100)->primary();
            $table->string('provider');
            $table->timestamps();

            $table->foreign('oauth_access_token_id')
                ->references('id')->on('oauth_access_tokens')
                ->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('oauth_access_token_providers');
    }
}
  1. Add a event listener in EventServiceProvider:
<?php

namespace App\Providers;

use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        'Laravel\Passport\Events\AccessTokenCreated' => [
            'App\Listeners\PassportAccessTokenCreated',
        ],
    ];

    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        parent::boot();
    }
}
  1. Put this following code in your listener, to save the relationship between access tokens and providers:
<?php

namespace App\Listeners;

use App\Events\Laravel\Passport\Events\AccessTokenCreated;
use Carbon\Carbon;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class PassportAccessTokenCreated
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  \Laravel\Passport\Events\AccessTokenCreated $event
     * @return void
     */
    public function handle(\Laravel\Passport\Events\AccessTokenCreated $event)
    {
        $provider = \Config::get('auth.guards.api.provider');
        DB::table('oauth_access_token_providers')->insert([
            "oauth_access_token_id" => $event->tokenId,
            "provider" => $provider,
            "created_at" => new Carbon(),
            "updated_at" => new Carbon(),
        ]);
    }
}
  1. Create a new global middleware to handle the custom providers setup for each request:
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\DB;
use League\OAuth2\Server\ResourceServer;
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;

class PassportCustomProviderAccessToken
{

    private $server;

    public function __construct(ResourceServer $server)
    {
        $this->server = $server;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $psr = (new DiactorosFactory)->createRequest($request);

        try {
            $psr = $this->server->validateAuthenticatedRequest($psr);
            $token_id = $psr->getAttribute('oauth_access_token_id');
            if ($token_id) {
                $access_token = DB::table('oauth_access_token_providers')->where('oauth_access_token_id',
                    $token_id)->first();

                if ($access_token) {
                    \Config::set('auth.guards.api.provider', $access_token->provider);
                }
            }
        } catch (\Exception $e) {

        }

        return $next($request);
    }
}
  1. Finally, register your middleware as global HTTP middleware at app/Http/Kernel
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */
    protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        \App\Http\Middleware\PassportCustomProviderAccessToken::class
    ];

    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            'throttle:60,1',
            'bindings',
        ],
    ];

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'passport-custom-provider' => \App\Http\Middleware\PassportCustomProvider::class
    ];
}

It's only missing the refresh token and the cookie login handles.

@ghulamali2612
Copy link

@zubair1 Thanks Zubair your solution worked.

@zubair1
Copy link

zubair1 commented Jun 20, 2017

@MohammedSabbah - Glad it worked for you and thanks for simplifying the steps. I hope it helps others out.

@ghulamali2612 - You're welcome, glad it helped :)

@sanprodev
Copy link

sanprodev commented Jul 4, 2017

@zubair1, @MohammedSabbah, @ghulamali2612

Your Solution Works, i can get access token for Admin model, but when i test it in post man http://localhost:8000/api/user i'm getting the user of User model not the Admin models uses

https://stackoverflow.com/questions/44903323/laravel-api-returning-different-model-user

thank you

@ElioTohm
Copy link

ElioTohm commented Jul 5, 2017

@sanprodev In config/auth.php under guards -> api change the provider to Admin
'api' => [ 'driver' => 'passport', 'provider' => 'admin ', //in the link provided it's client ],

@rockmantist
Copy link

@renanwilliam i've followed your step and managed to get multi auth passport working.

but i found another problem when using javascript client, i always get unauthenticated error for Admin auth.
i use Consuming Your API with Javascript (section from laravel 5.4 docs) and have added \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class to the new guard but still no luck.

anyone facing same problem here ?

@renanwilliam
Copy link

Hi @rockmantist.

The token was not validated in PassportCustomProviderAccessToken middleware?:

try {
      $psr = $this->server->validateAuthenticatedRequest($psr); //is it validated here?
      $token_id = $psr->getAttribute('oauth_access_token_id');
      if ($token_id) {
         $access_token = DB::table('oauth_access_token_providers')->where('oauth_access_token_id', 
         $token_id)->first();

         if ($access_token) {
             \Config::set('auth.guards.api.provider', $access_token->provider);
          }
      }
} catch (\Exception $e) { }

@matiazar
Copy link

Hi.. thanks 4 all... but what about when passport make any changes to their code.. and you made a composer update? It will overwrite all your files.. and your app will be down. Im right?!

@renanwilliam
Copy link

@matiazar using the approach that I described above there's no changes in passport code

@storesbuzz
Copy link

Thanks @MohammedSabbah it worked for login. But facing issue with posting data. Will there a change in token passing while posting data?

@storesbuzz
Copy link

Hi @rockmantist does it worked for you?

@rockmantist
Copy link

@renanwilliam sorry for the late reply, using the middleware above didn't work. i end up creating a new middleware, it's basically the exact same as CreateFreshApiToken but i modified it to force using my other guard (admin).

the only problem now is, it still authenticated on another guard, for example i perform authentication on guard web, get the token, try to authenticate to admin guard, it's authenticated (i guess it's because both have the same row with same id or primary key value).

@Fahad032
Copy link

@rockmantist does the guard means auth()->guard('admin')->user() here, I am searching for a solution and just reached here.

@rockmantist
Copy link

@Fahad032 in my case it's guard auth()->guard('auth:api')->user() for frontend and auth()->guard('auth:api_admin')->user() for admin.

@Sharvadze
Copy link

+1
Same problem here, can't use passport for other provider

@VitruxPT
Copy link

What about being able to change login or impersonate other users with passport? I have one app that needs to make requests in the name of the user and I can't use their access tokens since the value is different on the database, and I also can't use Auth::once or Auth::onceById while in the auth:api middleware.
The user doesn't login to that App, but I still need the app to be able to execute actions to my API just like if it was the user.

For example, just checking a user in my api with that app:

  • Endpoint: /v2/user?id=12345
    The request would be made with my app's access token and then a custom middleware would check if it was my app and check if there was an id in the request. If there was an Id then it would change the login for the next actions (ex: go to controllers) with the user's Id.

So in the end, that endpoint would show the info from user 12345 instead of my app's account info.

@storesbuzz
Copy link

Laravel passport is only working with User as a provider. Even if you fetch token by adding above changes Post and get requests are not working with changed passport provider than User.

Better you create multi auth with session driver and let 'User' model only for passport.
Repo here https://github.com/storesbuzz/laravel-multiAuth

@sanprodev
Copy link

@storesbuzz The above approach with provider parameter works fine with all the CRUD methods.i have implemented in 2 projects which are in production.

@rbuckingham
Copy link

anyone see a simpler solution for just the laravel_token cookie functionality of Passport (https://laravel.com/docs/5.5/passport#consuming-your-api-with-javascript) as I don't need the whole Oauth token functionality.

Laravel_token auth to my api works for my regular users but I dont think I can get the laravel_token cookie added for my multiauth 'admins' that use a different guard provider and model without doing something really ugly, I'm hoping for this functionality only I can do something cleaner.

@sowork
Copy link

sowork commented Sep 21, 2017

@renanwilliam I think the PassportCustomProviderAccessToken kernel class middlewareGroups inside the best bar
'api' => [
             \ App \ Http \ Middleware \ PassportAccessTokenValidate :: class,
             'throttle: 60,1',
             'bindings',
         ],

@driesvints
Copy link
Member

@ReneGustavo Try one of the following channels:

@prodigy7
Copy link

Any news? I hesitate, using any "workaround" when maybe the functionality will be implemented and force me to change my code. Maybe it made sense, vote for one of the implementations like https://github.com/sfelix-martins/passport-multiauth for integration?
I know some scenarios where multiauth would be very useful and over 50 comments in that discussion here show also, that there is a need for this feature.

@prodigy7
Copy link

Okay, maybe one point for discussion: So far I see, the sfelix-martins solution use same /oauth/token for requesting tokens for different auth users with "provider" parameter.
I think, a expected behaviour or setup is, using different urls for different auth (tables). But using a additional parameter would made the solution incompatible with other oauth client implementations. Something like /oauth/token and /admin/oauth/token I think would made more sense.

@thomas-obernberger
Copy link

is there any chance to get this feature in the near future?

@afilippov1985
Copy link
Contributor

afilippov1985 commented Feb 3, 2019

My solution for Multi-Auth:

  1. Create middleware (app/Http/Middleware/SetPassportAuthGuard.php)
<?php

namespace App\Http\Middleware;

use Closure;

class SetPassportAuthGuard
{
    public function handle($request, Closure $next, $guard = 'api')
    {
        app('config')->set('auth.passport.guard', $guard); // save current guard name in config
        return $next($request);
    }
}
  1. Register route middleware (in app/Http/Kernel.php)
protected $routeMiddleware = [
    //
    'passport' => \App\Http\Middleware\SetPassportAuthGuard::class,
    //
];
  1. Add routes (app/routes/api.php)
// route for issue tokens
Route::group(['middleware' => ['passport:myguard1']], function() {
    Route::post('/token1', '\Laravel\Passport\Http\Controllers\AccessTokenController@issueToken');
});

// resource route
Route::group(['middleware' => ['auth:myguard1']], function() {
    Route::get('test1', function(Request $request) {
        return response()->json($request->user());
    });
});
  1. Register alias in app/config/app.php to replace Laravel\Passport\Bridge\UserRepository with your own implementation
'aliases' => [
    //
    'Laravel\Passport\Bridge\UserRepository' => App\PassportUserRepository::class,
    //
]
  1. New UserRepository
<?php

namespace App;

use Illuminate\Auth\EloquentUserProvider;
use Laravel\Passport\Bridge\User;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\UserEntityInterface;
use League\OAuth2\Server\Repositories\UserRepositoryInterface;

class PassportUserRepository implements UserRepositoryInterface
{
    /**
     * {@inheritdoc}
     */
    public function getUserEntityByUserCredentials($username, $password, $grantType, ClientEntityInterface $clientEntity)
    {
        $guard = config('auth.passport.guard'); // obtain current guard name from config
        $provider = config('auth.guards.'.$guard.'.provider');
        $userProvider = app('auth')->createUserProvider($provider);
        
        if ($userProvider instanceof EloquentUserProvider &&
            method_exists($model = $userProvider->getModel(), 'findForPassport')) {
            $user = (new $model)->findForPassport($username);
        } else {
            $user = $userProvider->retrieveById($username);
        }

        if (!$user) {
            return;
        }

        if (method_exists($user, 'validateForPassportPasswordGrant')) {
            if (!$user->validateForPassportPasswordGrant($password)) {
                return;
            }
        } else {
            if (!$userProvider->validateCredentials($user, ['password' => $password])) {
                return;
            }
        }

        if ($user instanceof UserEntityInterface) {
            return $user;
        }

        return new User($user->getAuthIdentifier());
    }
}

@drpiou
Copy link

drpiou commented Feb 5, 2019

Many thanks to you @afilippov1985, I finally get it working with your solution that I think it's the most elegant way to implement custom passport guards. This said, I just wanted to expose my solution because I needed custom username/email column and get it working with passport proxy.

  1. Create middleware (app/Http/Middleware/SetPassportAuthGuard.php)
<?php

namespace App\Http\Middleware;

use Closure;

class SetPassportAuthGuard
{
    public function handle($request, Closure $next, $guard = '')
    {
        app('config')->set('auth.passport.guard', $guard);

        return $next($request);
    }
}
  1. Register route middleware (app/Http/Kernel.php)
protected $routeMiddleware = [
    //
    'passport' => \App\Http\Middleware\SetPassportAuthGuard::class,
    //
];
  1. Add middleware in routes (app/routes/xxx.php)
/*
|--------------------------------------------------------------------------
| AUTH
|--------------------------------------------------------------------------
|
*/

Route::group(['prefix' => 'auth', 'middleware' => ['passport:admin-api']], function ()
{
    Route::post('/login', 'AuthController@login');
    Route::post('/refresh-token', 'AuthController@refreshToken');
});

/*
|--------------------------------------------------------------------------
| LOGGED IN ROUTES
|--------------------------------------------------------------------------
|
*/

Route::group(['middleware' => ['auth:admin-api']], function ()
{
    //

    Route::post('/fetch/something/protected', 'SomeController@fetchSomething');

    //
});

If you use a passport proxy like me, do the following, otherwise skip to step 5.

4a. Add middleware in AuthServiceProvider (app/Providers/AuthServiceProvider.php)

public function boot()
{
    //

    Route::group(['middleware' => ['passport:'.request()->guard]], function ()
    {
        Passport::routes(function ($router) {
            $router->forAccessTokens();
            $router->forTransientTokens();
        });
    });

    //
}

4b. Add guard in passport proxy

$response = $http->post(app_url('/oauth/token'), ['form_params' => array_merge($data, [
    'client_id' => (string) env('PASSWORD_CLIENT_ID'),
    'client_secret' => (string) env('PASSWORD_CLIENT_SECRET'),
    'grant_type' => $grant_type,
    'guard' => 'admin-api',
    'scope' => '',
])]);
  1. Create PassportUserRepository (app\OAuth\PassportUserRepository.php)
<?php

namespace App\OAuth;

use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Hashing\Hasher;
use Laravel\Passport\Bridge\User;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Repositories\UserRepositoryInterface;

class PassportUserRepository implements UserRepositoryInterface
{
    /**
     * The hasher implementation.
     *
     * @var \Illuminate\Contracts\Hashing\Hasher
     */
    protected $hasher;

    /**
     * Create a new repository instance.
     *
     * @param  \Illuminate\Contracts\Hashing\Hasher  $hasher
     * @return void
     */
    public function __construct(Hasher $hasher)
    {
        $this->hasher = $hasher;
    }

    /**
     * {@inheritdoc}
     */
    public function getUserEntityByUserCredentials($username, $password, $grantType, ClientEntityInterface $clientEntity)
    {
        $guard = config('auth.passport.guard');
        $provider = config('auth.guards.'.$guard.'.provider');

        if (is_null($model = config('auth.providers.'.$provider.'.model'))) {
            throw new RuntimeException('Unable to determine authentication model from configuration.');
        }

        if (method_exists($model, 'findForPassport')) {
            $user = (new $model)->findForPassport($username);
        } else {
            $user = (new $model)->where(config('auth.providers.'.$provider.'.passport.username') ?: 'email', $username)->first();
        }

        if (! $user) {
            return;
        } elseif (method_exists($user, 'validateForPassportPasswordGrant')) {
            if (! $user->validateForPassportPasswordGrant($password)) {
                return;
            }
        } elseif (! $this->hasher->check($password, $user->getAuthPassword())) {
            return;
        }

        return new User($user->getAuthIdentifier());
    }
}
  1. Register alias in app/config/app.php to replace Laravel\Passport\Bridge\UserRepository
'aliases' => [
    //
    'Laravel\Passport\Bridge\UserRepository' => App\OAuth\PassportUserRepository::class,
    //
]

I also had to composer dump-autoload after adding this.

  1. Configure guards (app/config/auth.php)
'guards' => [
    'web-api' => [
        'driver' => 'passport',
        'provider' => 'users',
    ],
    'admin-api' => [
        'driver' => 'passport',
        'provider' => 'admin_users',
    ],
],

'passport' => [
    'guard' => '',
],

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\Model\User::class,
        'passport' => [
            'username' => 'email',
        ],
    ],
    'admin_users' => [
        'driver' => 'eloquent',
        'model' => App\Model\AdminUser::class,
        'passport' => [
            'username' => 'username',
        ],
    ],
],

@afilippov1985
Copy link
Contributor

@drpiou You can define findForPassport method in your user models. It is more clean way.

// App\Model\User
public function findForPassport($username)
{
    return $this->where('email', $username)->first();
}

// App\Model\AdminUser
public function findForPassport($username)
{
    return $this->where('username', $username)->first();
    // may be also cheking is_admin flag
    // return $this->where('username', $username)->where('is_admin', 1)->first();
}

@NanaBarimah
Copy link

Thanks very much @drpiou, I followed your procedure and I still get "Unauthenticated" when I protected my api with the middleware('admin-api'). Can you please help me out?

@NanaBarimah
Copy link

I have followed @afilippov1985 steps and it is still not working for me.

@riyas1012
Copy link

riyas1012 commented Feb 13, 2019

Hi @renanwilliam I followed your steps, Admin registration working good and i got token. while use Admin login i got UnAuthorised error only.

This is my Api url and input for login in postman
http://localhost/api/admin/login

{
"email":"xxxxxxx@xxxx.xxx",
"password":xxxxxxxx,
}
In AdminController this is my login function.

public function login(Request $request)
{
$credentials = [
'email' => $request->email,
'password' => $request->password
];

    if (auth()->attempt($credentials)) {
        $token = auth()->user()->createToken('Admin')->accessToken;
        return response()->json(['token' => $token], 200);
    } else {
        return response()->json(['error' => 'UnAuthorised'], 401);
    }
}

This is my routes/api.php

Route::post('admin/login', 'AdminController@login');
Route::post('admin/register', 'AdminController@register');

Please help.

@renanwilliam
Copy link

Hi @riyas1012 ,

The solution that I described don't have these steps that you are mentioning.
I have used the standard passport route /oauth/token and middlewares to get it working.

@billriess
Copy link
Contributor

billriess commented Feb 14, 2019

The easiest way to add multi-auth into passport would be to add middleware to check the provider.

app/Http/Middleware/CheckProvider.php

<?php

namespace App\Http\Middleware;

use Closure;

class CheckProvider
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $validator = validator()->make($request->all(), [
            'provider' => 'in:admins'
        ]);

        if ($validator->fails()) {
            return response()->json([
                'error' => 'invalid_provider',
                'message' => 'This provider is invalid.'
            ], 422);
        }

        if ($request->input('grant_type') === 'password' && $request->input('provider')) {
            config(['auth.guards.api.provider' => $request->input('provider')]);
        }

        return $next($request);
    }
}

app/Http/Kernel.php

protected $routeMiddleware = [
        ...
        'provider' => \App\Http\Middleware\CheckProvider::class,
    ];

app/Providers/AuthServiceProvider.php

public function boot()
    {
        ...
        Passport::routes(null, ['middleware' => 'provider']);
    }

config/auth.php

'guards' => [
        ...
        'admins' => [
            'driver' => 'passport',
            'provider' => 'admins',
        ],
    ],

'providers' => [
        ...
        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Admin::class,
        ],
    ],

'passwords' => [
        ...
        'admins' => [
            'provider' => 'admins',
            'table' => 'password_resets',
            'expire' => 60
        ],
    ],

app/Admin.php

<?php

namespace App;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;

class Admin extends Authenticatable
{
    use HasApiTokens, Notifiable;
    //
}

routes/api.php

Route::middleware('auth:admins')->get('/admin', function (Request $request) {
    return Auth::user();
});

Now just pass provider with the value admins with the rest of the payload to /oauth/token. This will authenticate against the Admin model and return a valid JWT. If your Admin model doesn't have an 'email' field, you can add this to your model:

public function findForPassport($username)
{
    return $this->where('username', $username)->first();
}

@riyas1012
Copy link

riyas1012 commented Feb 15, 2019

Hi @riyas1012 ,

The solution that I described don't have these steps that you are mentioning.
I have used the standard passport route /oauth/token and middlewares to get it working.

@renanwilliam Sorry its my mistake. Now i got it Thanks.

@msankar1991
Copy link

@afilippov1985 : After access token expiration how it will give a new access token for that user.

@drpiou : As per your code there is no need to add the provider in the request always right?

@afilippov1985
Copy link
Contributor

@msankar1991
After access token expiration you will get 401 http error
Then you should use refresh token to retrive new access token / refresh token pair

POST https://example.org/api/your-token-endpoint
Accept: application/json
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&client_id=CLIENT_ID&client_secret=CLIENT_SECRET&refresh_token=def50200c6...long...string...

or get new access token / refresh token pair passing user credentials

POST https://example.org/api/your-token-endpoint
Accept: application/json
Content-Type: application/x-www-form-urlencoded

grant_type=password&client_id=CLIENT_ID&client_secret=CLIENT_SECRET&username=USER&password=PASSWORD

Then re-send request using fresh access token

@msankar1991
Copy link

@afilippov1985 Thanks for the updates. And one more clarification each every api request need to authenticate user identity so this case I'm passing to access_token with Authorization header always. But how can I validate this token is valid or not.

@riyas1012
Copy link

Hi @renanwilliam ,
How to identify whether its admin or user by token?

@renanwilliam
Copy link

Hi @renanwilliam ,
How to identify whether its admin or user by token?

#161 (comment)

@afilippov1985
Copy link
Contributor

@quarkcloudio
Copy link

quarkcloudio commented Feb 28, 2019

@afilippov1985

i used Auth::guard('admin')->attempt(['username' => $username, 'password' => $password]);
have this error
Method Illuminate\Auth\RequestGuard::attempt does not exist.

@afilippov1985
Copy link
Contributor

This code will work for session guard driver, but not for passport. You can't do manual authentication this way with Passport.

@quarkcloudio
Copy link

quarkcloudio commented Feb 28, 2019

@afilippov1985
My back-end code `
$loginResult = Auth::guard('admin')->attempt(['username' => $username, 'password' => $password]);

    if($loginResult) {

        $result['token'] = $user->createToken('FullStack')->accessToken;

        return $this->success('ok','',$result);

    }`

Front end send request like this Authorization: Bearer +token ,but unable to authenticate

@quarkcloudio
Copy link

This code will work for session guard driver, but not for passport. You can't do manual authentication this way with Passport.

I have changed session guard driver

@driesvints
Copy link
Member

Hello everyone. Due to the fact that this thread is receiving lots of comments on how to circumvent this or do an alternative implementation and not discuss the feature at hand I've decided to lock the thread. Please use a support channel if you wish to discuss these things.

We're still open to prs if anyone wants to take a stab at implementing this. If you do, please provide a thorough explanation of the changes and tests. If you're introducing breaking changes please target master.

@laravel laravel locked as off-topic and limited conversation to collaborators Feb 28, 2019
@driesvints
Copy link
Member

New issue that discusses a possible implementation can be found here: #982

@driesvints
Copy link
Member

For anyone still interested in the feature please take a look at the proposed PR #1220 and provide feedback on it, thanks.

@driesvints
Copy link
Member

PR for multi guard is merged on master. You all can thank @billriess for getting this in.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests