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
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!
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 :)
Please or to participate in this conversation.