Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

mewtonium's avatar

Passport - omit standard user authentication and use custom logic

Hello!

Is it possible within Passport, to omit the standard user authentication and layer in custom logic (which doesn't use a database) instead?

To give some context: I want to use my Laravel app as a proxy to an external API. We can't store any user information on the server, so will need to forward the username/password to that API to 'login', before issuing tokens.

Ideally, I'd like to utilise Passport to handle issuing and authorising access/refresh tokens but instead base this on the result of the external API response, rather than looking up the user in the database.

Is this possible? Or should I look into my own solution?

Any advice is appreciated! Thanks!

0 likes
15 replies
corbosman's avatar

Hi, this is definitely possible. We do the same except instead of an API we use a Radius server to authenticate users. What you need to do is write your own Auth Guard and perhaps User Provider. Then register those in config/auth.php

mewtonium's avatar

I've been researching into your suggestion and I've come across so much information that is overly verbose its sending my head into a spin!

Do you know of any concise references or tutorials that go about setting this up as you've advised?

Also, it's been suggested that I could have the below and it could work - is it possible? I want to leverage Passport but authenticate users with my own logic, so I would only need to write a user provider? I'm struggling to see how this can work while keeping the api guard driver as passport

'guards' => [
    'api' => [
        'driver' => 'passport',
        'provider' => 'remote',
        'hash' => false
    ]
]

'providers' => [
    'remote' => [
        'driver' => 'external-api'
    ]
]
corbosman's avatar

Maybe I misunderstood your question. Which oauth2 grant are you using? What exactly are you authenticating?

mewtonium's avatar

I was looking to use the password client. So I'll send the username and password along with the client ID and secret, I want to use an external API within the app to check that the username and password are OK, but want to leverage Passport in issuing and authenticating tokens. So based on the result of the external API call, the tokens are issued/denied

I can't call the external API directly from the browser because of CORS, so the app is essentially a proxy for another API. Essentially, I can't use the users table to check the details supplied.

It may be that using Passport this way can't be done and I have to try a different route. Maybe implement a custom JWT guard/provider, would be another route?

corbosman's avatar

This is what I thought you meant. The api guard is meant to protect a resource endpoint that you want to protect with tokens. The oauth2 flow itself does not go through the api guard. Here is my auth.php config.

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

   'guards' => [
        'api' => [
            'driver' => 'passport',
            'provider' => 'portal',
        ],

        'portal' => [
            'driver' => 'xs4all-radius',
            'provider' => 'portal'
        ]
    ],

So my default guard is 'portal', which uses the 'xs4all-radius' Guard driver for authentication, and a 'portal' UserProvider. These serve the oauth2 flow. When I want to protect api endpoints with the generated tokens that your clients get from passport, you protect them with the auth:api middleware. Like this:

Route::middleware('auth:api')->get('/user', ['as' => 'api.user', 'uses' => 'ApiController@user']);
Route::middleware('auth:api')->get('/groups', ['as' => 'api.groups', 'uses' => 'ApiController@groups']);

Does that make sense in your situation?

mewtonium's avatar

Thanks for this. I don't suppose you could post your providers config and also the guard and user provider you wrote for reference? I just want to make sure I'm going in the right direction with this as it sounds like what you've done is exactly what I need, but I'm getting myself all confused 😂

mewtonium's avatar

@corbosman is this something you can provide? I've managed to get something working with Laravel JWT Auth but it's come at the cost of the OAuth flow, which isn't ideal but it's further than I've managed to get so far

corbosman's avatar

Hi, not sure what you mean with providers config. If you mean AuthServiceProvider, I have this.

        app('auth')->extend('xs4all-radius', function ($app, $name, array $config) {
            return new RadiusGuard($name, app('auth')->createUserProvider($config['provider']), $app->make('session.store'));
        });

        app('auth')->provider('portal', function ($app, array $config) {
            return new PortalDatabaseProvider($app->make($config['model']));
        });

The RadiusGuard just extends the SessionGuard.

<?php namespace App\Auth;

class RadiusGuard extends SessionGuard {

    /**
     * Attempt to authenticate a user using the given credentials.
     *
     * @param  array $credentials
     * @param  bool $remember
     * @param  bool $login
     * @return bool
     */
    public function attempt(array $credentials = [], $remember = false, $login = true)
    {
       // do your stuff here.
    }

}

My UserProvider is an implementation of Illuminate\Contracts\Auth\UserProvider. You have to make sure you implement all the methods, or at least provide stub methods so the interface is happy.

mewtonium's avatar

Sorry, what I meant was the providers config in config/auth.php

Below is the UserProvider I currently have written. I think where I'm struggling is with where the authorisation logic should be handled, and where the external API authentication logic should be handled - i.e. which should use the guard and which the provider?

So are you authenticating the user (i.e. your API call) in your guard, storing in session, then using your provider to handle logging into Passport via the password client and issuing tokens? I've noticed that you're passing $config['model'] into your provider initialisation. Is a database required for what I need to achieve?

Just finding it hard to see how an external API and Passport can work together

<?php

namespace App\Auth\Bigcommerce;

use App\Services\Bigcommerce\Bigcommerce;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider as UserContract;
use Illuminate\Auth\GenericUser;

class UserProvider implements UserContract
{
    /**
     * Bigcommerce service instance
     *
     * @var \App\Services\Bigcommerce\Bigcommerce
     */
    public $bigcommerce;

    /**
     * The provider instance.
     *
     * @param \App\Services\Bigcommerce\Bigcommerce $bigcommerce
     * @return void
     */
    public function __construct(Bigcommerce $bigcommerce)
    {
        $this->bigcommerce = $bigcommerce;
    }

     /**
     * Retrieve a user by their unique identifier.
     *
     * @param  mixed  $identifier
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveById($identifier)
    {
        // @todo Need to implement...
    }

    /**
     * Retrieve a user by the given credentials.
     *
     * @param  array  $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByCredentials(array $credentials)
    {
        try {
            $customer = $this->bigcommerce
                ->setVersion('v3')
                ->getCustomers([
                    'email:in' => $credentials['username'],
                ]);
        } catch (\Exception $e) {
            return null;
        }

        if (count($customer) === 0) {
            return null;
        }

        return $this->getGenericUser($customer[0]);
    }

    /**
     * Validate a user against the given credentials.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  array  $credentials
     * @return bool
     */
    public function validateCredentials(Authenticatable $user, array $credentials)
    {
        try {
            $validPassword = $this->bigcommerce
                ->setVersion('v2')
                ->validateCustomerPassword(
                    $user->getAuthIdentifier(),
                    $credentials['password']
                );
        } catch (\Exception $e) {
            return false;
        }

        return $validPassword['success'] ?? false;
    }

    /**
     * Gets an instance of a generic user.
     *
     * @param array|null $user
     * @return \Illuminate\Auth\GenericUser|null
     */
    public function getGenericUser($user)
    {
        return !is_null($user) ? new GenericUser($user) : null;
    }

    /**
     * Stub methods to satisfy contract
     */
    public function retrieveByToken($identifier, $token) {}
    public function updateRememberToken(Authenticatable $user, $token) {}
}
corbosman's avatar
Level 20

Hi, first of all, sorry, i was wrong. I hadn't used the password grant before and wasn't aware of how exactly it works. We only use the authcode and implicit grant. So, i spent some time to figure this out, and I think this works. Now bear with me, this is complicated.

First of al, it's important to know that the password grant gives you an option to verify the username and password. Check this section and this section. While reading through the code in passport, I noticed they also offer a single method to do both, which could be useful. You can find the relevant section in the code here.

With regards to the User Provider to use, I saw in the code that it used the provider set on the api guard. So, let's start.

First we need to configure config/auth.php. The important bit is to set the api.provider field to a custom user provider. A user provider is simply a class according to a standard interface that can return user objects based on credentials.


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

        'api' => [
            'driver' => 'passport',
            'provider' => 'my-provider',
        ],
    ],

We also need to make sure that laravel knows about this provider. You do that in AuthServiceProvider:

        app('auth')->provider('my-provider', function ($app, array $config) {
            return new MyUserProvider(new Client);
        });

This tells laravel that when you want to use my-provider, it needs to instantiate the MyUserProvider class, with Client as a parameter. I'm just providing the Client bit as an example that you can dependency inject whatever you want into MyUserProvider. In your case, maybe you want to inject Bigcommerce. You would do that here.

Let's check out this MyUserProvider.

<?php

namespace App\Auth;

use App\MyUser;
use Illuminate\Contracts\Auth\UserProvider;

class MyUserProvider implements UserProvider
{
    protected $client;

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

    public function retrieveById($identifier)
    {
        return new MyUser([
            'id' => 1,
            'username' => 'foo',
            'email'=> '[email protected]'
        ]);
    }

    public function retrieveByCredentials(array $credentials)
    {
        return new MyUser([
            'id' => 1,
            'username' => 'foo',
            'email'=> '[email protected]'
        ]);
    }

    public function retrieveByToken($identifier, $token) {}
    public function updateRememberToken($user, $token) {}
    public function validateCredentials($user, array $credentials) {}

}

So this class needs to implement a UserProvider contract. I think all you really need is RetrieveById, but im not 100% positive. Just add some logging to see which methods are being called. As you can see, this class returns a MyUser class. I am just hardcoding the fields, you would implement your API calls here and fetch the fields you'd need. You can have whatever fields you want, but I believe passport expects there to be an 'id' field. If you can't have an id field, let me know, I think there is a way around it.

This MyUser class looks like this.

<?php

namespace App;

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

class MyUser extends Authenticatable
{
    use HasApiTokens;

    protected $fillable = [
        'id', 'username', 'email'
    ];


    public function findAndValidateForPassport($username, $password)
    {
        $attributes = [
            'id' => 1,
            'username' => 'foo',
            'email' => '[email protected]'
        ];

        return new static ($attributes);
    }

    public function findForPassport($username)
    {
        logger('finding for passport');
    }

    public function validateForPassportPasswordGrant($password)
    {
        logger('validateforpassport');
    }
}

The important part here is the 'HasApiTokens' trait. This adds a whole bunch of methods that passport uses. You also see I implemented the methods here that the docs tell you to use. Here you would add your code again that would fetch the user from your API, and then instantiates a new version of itself with those attributes. Since you now have this in 2 locations, maybe you can have some kind of class that does this for you. You would use that class here and in the UserProvider.

You may wonder why you need this in 2 places. This is because the password grant needs these findAndValidateForPassport() methods to implement your custom users. But the token guard, which protects your api routes, simply uses the api user provider to get the same data.

I think we now have all the required sections. If I now POST to /oath/token, I get back a token that's linked to user-id 1 (hardcoded). I see that in the database:

                                        id                                        | user_id | client_id | name |                         scopes                          | revoked |     created_at      |     updated_at      |     expires_at      
----------------------------------------------------------------------------------+---------+-----------+------+---------------------------------------------------------+---------+---------------------+---------------------+---------------------
 be100001d3286563cfeb7fb18f188b8d43fd816cf9f4080fe7f9eb836ea64236822aff13e87a1be1 |       1 |        30 |      | []                                                      | f       | 2020-01-21 13:46:25 | 2020-01-21 13:46:25 | 2020-01-22 13:46:25

Now I can add api routes protected by this token:

Route::middleware('auth:api')->get('/foo', function() {
    return 'bar';
});

And I tested this, if I call /api/foo, with the access token as a bearer token, i get 'bar'. How this works is, the auth:api middleware sees that you want to use MyProvider for api. Passport sees in the token that it's linked to user-id 1 (that's encoded in the token), here's the above token decoded:

{
  "aud": "30",
  "jti": "be100001d3286563cfeb7fb18f188b8d43fd816cf9f4080fe7f9eb836ea64236822aff13e87a1be1",
  "iat": 1579610785,
  "nbf": 1579610785,
  "exp": 1579697185,
  "sub": "1",
  "scopes": [],
}

It then asks your custom UserProvider if this user is valid, and if so, it finds the token in the database. If the token is valid, you get access.

I hope you could follow this. At least now I know how this part of passport works :)

1 like
corbosman's avatar

Btw, you can probably create a MyUser that does not extend Authenticatable, but you'd have to figure out the details on which methods passport tries to call, and implement those yourself.

mewtonium's avatar

Thanks for this. I've been pulled onto another project for the rest of today but will go through your suggestions above tomorrow and let you know how I get on. Appreciate the help :)

mewtonium's avatar

I worked through your suggestion yesterday and (aside from some weird errors being thrown when login fails) it's finally working :) I'm adding the call to the external API in findAndValidateForPassport() which is on my custom User class which inherits Illuminate\Foundation\Auth\User and only retrieveById() needed to be implemented in my custom user provider. For cleanliness, I may refactor by splitting the code between findForPassport() and validateForPassportPasswordGrant(), but that's not hugely important right now

At the moment, I'm doing an external API call in retrieveById() also but I'm now looking into authorizing the user after logging in without the need to keep making additional requests to the Bigcommerce API again for protected routes. Initially, I'm thinking after logging into the password client, I create a session token, pass that back to the front end along with the access tokens, and sending that with subsequent requests to protected routes. I will look this up in the database and return an instance of my new User class in retrieveById() - does that sound reasonable to you?

A difference to point out, is that I needed to set the model value in my custom provider config else en exception was thrown after this check in UserRepository - did you also need to do this?

'external' => [
    'driver' => 'bigcommerce',
    'model' => App\Auth\Bigcommerce\User::class,
]

Just want to give a huge thanks for your help with this :)

corbosman's avatar

You are absolutely right, I forgot to add that to the post. I had this in my code that I forgot to paste. Sorry about that.

        'my-provider' => [
            'driver' => 'my-provider',
            'model' => App\MyUser::class
        ],

With regards to having to do API calls to bigcommerce, in the past I did that by caching the requests in redis. You could surround the calls to bigcommerce in a cache()->remember(...) call. But I guess you could also use a session token. The thing is, the auth:api middleware needs to be able to convert an id that's encoded in the token into a user. As long as you can grab that user from someplace, some local table, cache, or from the api, doesnt matter. Whatever works for you.

Please or to participate in this conversation.