Zubair1
Zubair1

Reputation: 2780

Multi Auth with Laravel 5.4 and Passport

I am trying to setup multi auth with Laravel Passport, but it doesn't seem to support it. I am using the Password Grant to issue tokens which requires me to pass username/password of the user wanting access tokens.

I have 3 auth guards/providers setup, 4 in total. Users, Vendors, Admins and API

2 of the Auths need passport access, so each user needs to be able to issue tokens. But Passport automatically takes the API auth provider, but I want this to change based on which user is logging in.. If user does then the User provider and if its a vendor then the Vendor provider.

But the way Passport currently only supports only 1 user type, so its defaulting to the API provider.

Is there something better for this? or should I go with roles based authentication instead.

Upvotes: 7

Views: 11372

Answers (7)

Rejoanul Alam
Rejoanul Alam

Reputation: 5398

I have done it in Laravel 7 without any custom code as suggested other answers. I have just changed 3 file as follows

config/auth.php file (My new table name is doctors)

'guards' => [
   ...
    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
        'hash' => false,
    ],
    'api-doctors' => [
        'driver' => 'passport',
        'provider' => 'doctors',
    ],
],

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

Revise my Doctor model similarly as User model (App/Doctor.php)

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

class Doctor extends Authenticatable
{
  use HasApiTokens, Notifiable;

Finally define routes using middleware routes/api.php file as follows

//normal middleware which exist already
Route::post('/choose', 'PatientController@appointment')->middleware('auth:api');

//newly created middleware provider (at config/auth.php)
Route::post('/accept', 'Api\DoctorController@allow')->middleware('auth:api-doctors');

Now when you will create new oauth client you may use artisan passport:client --password --provider this command which prompt you in command line for choosing table before that do not forget to run

php artisan config:cache
php artisan cache:clear

Also you can create user manually in oauth_clients table by replacing provider column value users to doctors

Some hints at reference link https://laravel.com/docs/7.x/passport#customizing-the-user-provider

Upvotes: 2

Amirouche
Amirouche

Reputation: 3766

if you look for solution to the Passport Multi-Auth API I recommend you this solution

Upvotes: 0

Hossein Karami
Hossein Karami

Reputation: 816

We are waiting for Laravel to add this feature to its package but for those who want to add this feature, I suggest using this package

Upvotes: 1

jsdecena
jsdecena

Reputation: 573

I have created a small package for this issue. Here's the link for the complete doc link

But the gist is, whenever a user entity gets logged in, it checks for the guards and providers.

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

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

    'customers' => [
        'driver' => 'passport',
        'provider' => 'customers'
    ],
],

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => 'App\User',
    ],
    /**
     * This is the important part. You can create as many providers as you like but right now, 
     * we just need the customer
     */
     'customers' => [
         'driver' => 'eloquent',
         'model' => 'App\Customer',
     ],
],

You should have a controller like this:

<?php

namespace App\Http\Controllers\Auth;

use App\Customers\Customer;
use App\Customers\Exceptions\CustomerNotFoundException;
use Illuminate\Database\ModelNotFoundException;
use Laravel\Passport\Http\Controllers\AccessTokenController;
use Laravel\Passport\TokenRepository;
use League\OAuth2\Server\AuthorizationServer;
use Psr\Http\Message\ServerRequestInterface;
use Lcobucci\JWT\Parser as JwtParser;

class CustomerTokenAuthController extends AccessTokenController
{
     /**
      * The authorization server.
      *
      * @var \League\OAuth2\Server\AuthorizationServer
      */
     protected $server;

     /**
      * The token repository instance.
      *
      * @var \Laravel\Passport\TokenRepository
      */
     protected $tokens;

     /**
      * The JWT parser instance.
      *
      * @var \Lcobucci\JWT\Parser
      */
     protected $jwt;

     /**
      * Create a new controller instance.
      *
      * @param  \League\OAuth2\Server\AuthorizationServer  $server
      * @param  \Laravel\Passport\TokenRepository  $tokens
      * @param  \Lcobucci\JWT\Parser  $jwt
      */
     public function __construct(AuthorizationServer $server,
                                 TokenRepository $tokens,
                                 JwtParser $jwt)
     {
         parent::__construct($server, $tokens, $jwt);
     }

     /**
      * Override the default Laravel Passport token generation
      *
      * @param ServerRequestInterface $request
      * @return array
      * @throws UserNotFoundException
      */
     public function issueToken(ServerRequestInterface $request)
     {
         $body = (parent::issueToken($request))->getBody()->__toString();
         $token = json_decode($body, true);

         if (array_key_exists('error', $token)) {
             return response()->json([
                 'error' => $token['error'],
                 'status_code' => 401
             ], 401);
         }

        $data = $request->getParsedBody();

        $email = $data['username'];  

        switch ($data['provider']) {
            case 'customers';

                try {

                 $user = Customer::where('email', $email)->firstOrFail();

                } catch (ModelNotFoundException $e) {
                  return response()->json([
                      'error' => $e->getMessage(),
                      'status_code' => 401
                  ], 401);
                }

                break;

            default :

                try {

                 $user = User::where('email', $email)->firstOrFail();

                } catch (ModelNotFoundException $e) {
                  return response()->json([
                      'error' => $e->getMessage(),
                      'status_code' => 401
                  ], 401);
                }        
        }

        return compact('token', 'user');
    }
}

and the request should be:

POST /api/oauth/token HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache

grant_type=password&username=test%40email.com&password=secret&provider=customers

To check in your controller who is the logged in user, you can do:

auth()->guard('customers')->user()

Upvotes: 2

Niks
Niks

Reputation: 41

Laravel passport is only working with User as a provider. Even if you fetch token by adding above changes with PasswordGrant & UserRepository, while you go for API call for Post and get requests are not working with changed passport provider other than User.

Better you create multi auth with session driver if must needed as Vendors and Customers. let 'User' model only for passport whose table columns supports admin, API, vendor, etc.

Repo here laravel-multiAuth

Upvotes: 0

Ghulam Ali
Ghulam Ali

Reputation: 357

You have to modify the main library Files.

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

Find getUserEntityByUserCredentials and Copy the complete method and Paste this method below with name getEntityByUserCredentials. Donot modify the main function because it is used somewhere.

//Add the $provider variable at last or replace this line.
public function getEntityByUserCredentials($username, $password, $grantType, ClientEntityInterface $clientEntity, $provider)

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');

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

In the function validateUser add the below code after line no. 88

$provider = $this->getRequestParameter('provider', $request);

if (is_null($provider)) {
     throw OAuthServerException::invalidRequest('provider');
}

After adding replace the following code with

$user = $this->userRepository->getEntityByUserCredentials(
        $username,
        $password,
        $this->getIdentifier(),
        $client,
        $provider
    );

Now try this using postman

Add the provider field in your input field like

provider = api_vendors
OR
provider = api_admins
OR
provider = api_users
And so on....

make sure you have added your provider and set the drivers in the config/auth.php

'guards' => [
 'api_admins' => [
    'driver' => 'passport',
    'provider' => 'admins',
  ],
  'api_vendors' => [
    'driver' => 'passport',
    'provider' => 'vendors',
  ],

I hope this helps.

Upvotes: 4

Ricardo Fadini
Ricardo Fadini

Reputation: 61

If you still need.

I prefer go with roles, there is an amazing plugin for that: https://github.com/larapacks/authorization

But if you somehow needs that, you will be able to use following the steps bellow.

For multi guards, you will have to overwrite some code.

Instead of loading PassportServiceProvider, you create your own and extends the PassportServiceProvider and overwrites the method makePasswordGrant. On this method, you will change the Passport UserRepository for your own repository extended. On user repository you must to change the static model config for a dynamic one (I load from request attributes, but you can get from anywhere).

You may have to overwrite something else, but I made a test and works.

Ex:

PassportServiceProvider

namespace App\Providers;

use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Grant\PasswordGrant;
use Laravel\Passport\PassportServiceProvider as BasePassportServiceProvider;
use Laravel\Passport\Passport;

class PassportServiceProvider extends BasePassportServiceProvider
{
    /**
     * Create and configure a Password grant instance.
     *
     * @return PasswordGrant
     */
    protected function makePasswordGrant()
    {
        $grant = new PasswordGrant(
            $this->app->make(\App\Repositories\PassportUserRepository::class),
            $this->app->make(\Laravel\Passport\Bridge\RefreshTokenRepository::class)
        );

        $grant->setRefreshTokenTTL(Passport::refreshTokensExpireIn());

        return $grant;
    }

}

UserRepository

namespace App\Repositories;

use App;
use Illuminate\Http\Request;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use Laravel\Passport\Bridge\UserRepository;
use Laravel\Passport\Bridge\User;
use RuntimeException;

class PassportUserRepository extends UserRepository
{
    /**
     * {@inheritdoc}
     */
    public function getUserEntityByUserCredentials($username, $password, $grantType, ClientEntityInterface $clientEntity)
    {
        $guard = App::make(Request::class)->attributes->get('guard') ?: 'api';
        $provider = config("auth.guards.{$guard}.provider");


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


        if (method_exists($model, 'findForPassport')) {
            $user = (new $model)->findForPassport($username);
        } else {
            $user = (new $model)->where('email', $username)->first();
        }


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

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

PS: Sorry my bad english.

Upvotes: 6

Related Questions