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

MarkusHH78's avatar

Custom User Provider

Hi, i need help with creating custom user provider in Laravel 8

What i need:

  • User credentials are stored in an external DB
  • After submitting login form i check if is valid user by custom API
  • If valid i need to login this user in my laravel system.
  • Want use Auth::check() for example.
  • User IS NOT stored in locale DB

I have googled a lot and i think i need a custom user provider but i don't understand the official docs :-( "adding-custom-user-providers"

Is there a simple way to achieve this? Is there a good tutorial?

Any help welcome!

Markus

0 likes
29 replies
rodrigo.pedra's avatar

Have you tried using closure request guards?

https://laravel.com/docs/8.x/authentication#closure-request-guards

Auth::viaRequest('custom', function (Request $request) {
    $response = Http::post('https://example.com/authenticate', [
        'email' => $request->input('email'),
        'password' => $request->input('password'),
    ]);

    if (! $response->ok()) {
        return null;
    }
    
    // new User won't be saved to your local database
    // if needed new up a custom object that implements:
    // \Illuminate\Contracts\Auth\Authenticatable
    return new User([
        'name' => $request->input('name'),
        'email' => $request->input('email'),
    ]);
});

To learn more about the HTTP Client refer to:

https://laravel.com/docs/8.x/http-client

Then in your route definition:

Route::get('/dashboard', [DashboardController::class, 'index'])
    ->middleware('auth:custom');

Or define this new guard as the default on your ./config/auth.php file:

    'defaults' => [
        'guard' => 'custom', // <<< changed here
        'passwords' => 'users', // <<< can leave this as is
    ],

Hope this helps.

MarkusHH78's avatar

Hm, i cant post code/ links on first day here :-(

I put this Auth::viaRequest to my AuthServiceProvider boot() function for testing. Guard i set to "custom" but no i got this error: Auth guard [custom] is not defined

rodrigo.pedra's avatar

My bad, my explanation might be misleading.

As per docs, linked above, the Auth::viaRequest will register a new Guard driver, not a new guard.

So you need to add a new guard to your ./config/auth.php

'guards' => [
    'custom' => [
        'driver' => 'custom',
    ],
],

Then setting it as the default guard should work.

rodrigo.pedra's avatar

Thanks for the heads up, I added a note in the other thread I linked on my first comment.

MarkusHH78's avatar

Ok, this works!

Next error :-(

Add [name] to fillable property to allow mass assignment on [Illuminate\Foundation\Auth\User].

I have NO user model

MarkusHH78's avatar

Does this create a new USer in my locale DB?

return new User([ 'name' => 'Markus', 'email' => 'xxx', ]);

Or does this only creates a "Session-User" ?

MarkusHH78's avatar

Sorry i do not get it :-(

Once again step by step:

Login form:

{{ csrf_field() }}
<input type="text" name="name" value=""><br>
<input type="text" name="password" value=""><br>
<input type="submit" value="Submit">
MarkusHH78's avatar

After Submit send post data (name and Password) to CustomLoginController=>doLogin():

In this function i want to check if user exists in external DB:

$user = GET FROM EXTERNAL;

If user exists do something like that:

Auth::viaRequest('custom', function (Request $request) {
        $userLoggedin = new User([
            'name' => $request->input('name'),
            'email' => $request->input('email'),
        ]);
    });
MarkusHH78's avatar

And then if $userLoggedin is valid Laravel user redirect to secret page:

if(Auth::check()) { return redirect('secret'); }

rodrigo.pedra's avatar
Level 56

Ok, I might have been carried over by my response on the other thread that was posted on a very close time.

So let's add a custom user provider.

Step 1 - Create a custom user provider

Create the class below in your project.

Please read the comments in the code carefully.

<?php

namespace App;

use Illuminate\Auth\GenericUser;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Support\Facades\Http;

class ExternalApiUserProvider implements UserProvider
{
    public function retrieveById($identifier)
    {
        // This method is called from subsequent calls until the session expires.
        //
        // As you don't have a local users database we are going
        // to assume the identifier saved into the session is fine.
        //
        // Session cookies are encrypted by default
        //
        // This avoid calling the external service on every navigation.
        //
        // The downside is that if the user is not authorized anymore
        // in the external service, you won't know until their session expires.
        //
        // Ideally you should set a lower session duration so user
        // gets logged out quickier.
        //
        // An alternative is to save encrypted the user's credentials
        // and call the external service every time.
        //
        // But that would make a external API call on every request,
        // making your app slower. But is the most secure way.
        //
        // If you want I can make an modified version exemplifying 
        // how you could do this.
        return new GenericUser([
            'id' => $identifier,
            'email' => $identifier,
        ]);
    }

    public function retrieveByToken($identifier, $token)
    {
        return null;
    }

    public function updateRememberToken(Authenticatable $user, $token)
    {
    }

    public function retrieveByCredentials(array $credentials)
    {
        if (! array_key_exists('email', $credentials)) {
            return null;
        }

        // GenericUser is a class from Laravel Auth System
        return new GenericUser([
            'id' => $credentials['email'],
            'email' => $credentials['email'],
        ]);
    }

    public function validateCredentials(Authenticatable $user, array $credentials)
    {
        if (! array_key_exists('password', $credentials)) {
            return false;
        }

        // This is a simplified usage of Laravel's HTTP Client to call the external API
        // You might need to send more info to the external service.
        // Please refer to the HTTP Client docs to learn how to use it properly.
        $response = Http::post('https://example.com/authenticate', [
            // $user is the GenericUser instance created in
            // the retrieveByCredentials() method above.
            'email' => $user->email,
            'password' => $credentials['password'],
        ]);

        return $response->ok();
    }
}

HTTP Client docs: https://laravel.com/docs/8.x/http-client

Step 2 - Register the User Provider with the Auth Manager

Add this to your app's AuthServiceProvider's boot method:

<?php

namespace App\Providers;

use App\ExternalApiUserProvider;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Auth;

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        // 'App\Models\Model' => 'App\Policies\ModelPolicy',
    ];

    public function boot()
    {
        $this->registerPolicies();
        
        // you can choose a different name
        Auth::provider('external', function ($app, array $config) {
            return new ExternalApiUserProvider();
        });
    }
}

Step 3 - Configure ./config/auth.php

<?php

return [

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

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'external',  // <<< CHANGED HERE
        ],

        'api' => [
            'driver' => 'token',
            'provider' => 'users',
            'hash' => false,
        ],
    ],

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

        /// ADDED here
        'external' => [
            'driver' => 'external',
        ],
    ],

// ... keep the other configs
];

Step 4 - CustomLoginController@doLogin

On first login you should call Auth::attempt() instead of Auth::check().

Auth::check() is meant to be used after a user is already logged in.

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Auth;

class CustomLoginController extends Controller
{
    // ... other methods

    public function doLogin()
    {
        if (Auth::attempt(request()->only('email', 'password'))) {
            return redirect('secret');
        }

        return back()->withErrors([
            'email' => 'invalid credentials',
        ]);
    }
}

Step 5 - Guard the secret route

Don't forget to guard the secret route:

Route::get('/secret', [SecretController::class, 'show'])->middeware('auth');

Hope this helps.

12 likes
robmorrison's avatar

@rodrigo.pedra Thanks for taking the time to sketch out this solution. It was very helpful and got me past a sticking point, and enabled me to understand the auth process a little better. Prior to seeing this I had spent too much time spinning my wheels and was wondering if I would ever find a working solution.

1 like
MarkusHH78's avatar

Hey, thank your very much for this detailed tutorial! Works like a charm :-)

One litte thing...

The api request returns the whole user data like first name, last name, email etc.

I need to store this user data in the new generic user:

return new GenericUser([
'id' => $credentials['email'],
'email' => $credentials['email'],
'firstname' => 'Markus',
'lastname' => 'Smith',
ect...
]);

Later i want access them by Auth()->user()->firstname ect.

rodrigo.pedra's avatar

Try thie code below. And as always, read the comments in it.

<?php

namespace App;

use Illuminate\Auth\GenericUser;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Contracts\Encryption\EncryptException;
use Illuminate\Support\Facades\Http;

class ExternalApiUserProvider implements UserProvider
{
    private Encrypter $encrypter;

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

    public function retrieveById($identifier)
    {
        try {
            $payload = $this->encrypter->decrypt($identifier);
            $payload = \json_decode($payload, true, \JSON_THROW_ON_ERROR);
        } catch (DecryptException $exception) {
            return null;
        } catch (\JsonException $exception) {
            return null;
        }

        $payload['id'] = $identifier;
        
        // Another strategy, more secure, is to use the encrypted credentials
        // and retrieve the user from the external API on every request.
        // This way if the users gets unauthorized on the external API
        // you don't need to wait on their session to expire to reflect
        // this on your local system

        return new GenericUser($payload);
    }

    public function retrieveByToken($identifier, $token)
    {
        return null;
    }

    public function updateRememberToken(Authenticatable $user, $token)
    {
    }

    public function retrieveByCredentials(array $credentials)
    {
        // I broke into two if clauses to avoid line breaks here
        // you can use an "or" operator if you prefer to use
        // a single if
        if (! array_key_exists('email', $credentials)) {
            return null;
        }

        if (! array_key_exists('password', $credentials)) {
            return null;
        }

        // moved this check here as you want to use the response
        // as the user data
        $response = Http::post('https://example.com/authenticate', [
            'email' => $credentials['email'],
            'password' => $credentials['password'],
        ]);

        if (! $response->ok()) {
            return null;
        }

        // gets user data
        $payload = $response->json();

        // we'll use the encrypted user data as the GenericUser identifier
        // this will be saved into session and used in retrieveById()
        // method to hydrate the user on a next request
        try {
            $identifier = \json_encode($payload, \JSON_THROW_ON_ERROR);
            
            // You can skip ecnrypting and decrypting as the session
            // is already encrypted. So effectively this identifier will
            // be double encrypted.
            $payload['id'] = $this->encrypter->encrypt($identifier);
        } catch (\JsonException $exception) {
            return null;
        } catch (EncryptException $exception) {
            return null;
        }

        return new GenericUser($payload);
    }

    public function validateCredentials(Authenticatable $user, array $credentials)
    {
        // as if retrieveByCredentials() returns null this method never gets called
        // and as we moved the authentication to the external provider there
        // we can assume if this method gets called it is because a user
        // was successfully retrieved from the external API

        return true;
    }
}
1 like
MarkusHH78's avatar

Thanks for your reply!

I got this Error:

syntax error, unexpected 'Encrypter' (T_STRING), expecting function (T_FUNCTION) or const (T_CONST)

Something went wrong with this line i think: private Encrypter $encrypter;

For info: i have saved the class in a app/Providers/ExternalApiUserProvider.php

MarkusHH78's avatar

Ok forget it. I use Crypt::decryptString and Crypt::encryptString. That works fine!

But other question: Auth::user() now returns this format:

$a = {Illuminate\Auth\GenericUser} [1]
 attributes = {array} [11]
  id = {int} 19
  level = {int} 2
  qc_view = {int} 0
  password_h = "xxxxx"
  username = "xxxxxx"
  knr = "290002"
  vertreter = {int} 0
  productview = "0"
  lock_customer = {int} 0
  lock_customer_from = {int} 0
  lock_customer_to = {int} 0



I don't want the "attributes" array. I only want the userObject!
MarkusHH78's avatar

Ok i think this is irrelevant because i can access the user data like this:

{{ Auth::user()->username }}

rodrigo.pedra's avatar

Yes, this is how GenericUser is implemented.

Also Models are implemented like this too in Laravel.

They keep the attributes in an array and allow to access they properties through the __get() magic method.

1 like
MarkusHH78's avatar

Then, last question for today:

Auth::logout();

I got this error: Undefined index: remember_token

rodrigo.pedra's avatar

Tha is because GenericUser has a hardcoded remember_token reference.

Either extend this class to remove that reference and use the extended class instead of GenericUser, or add an empty remember_token field to the payload like this:

<?php

namespace App;

use Illuminate\Auth\GenericUser;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Contracts\Encryption\EncryptException;
use Illuminate\Support\Facades\Http;

class ExternalApiUserProvider implements UserProvider
{
    private Encrypter $encrypter;

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

    public function retrieveById($identifier)
    {
        try {
            $payload = $this->encrypter->decrypt($identifier);
            $payload = \json_decode($payload, true, \JSON_THROW_ON_ERROR);
        } catch (DecryptException $exception) {
            return null;
        } catch (\JsonException $exception) {
            return null;
        }

        $payload['id'] = $identifier;

        /////////////////////////////////////////////////////
        // ADDED HERE
        /////////////////////////////////////////////////////
        $payload['remember_token'] = '';

        return new GenericUser($payload);
    }

    public function retrieveByToken($identifier, $token)
    {
        return null;
    }

    public function updateRememberToken(Authenticatable $user, $token)
    {
    }

    public function retrieveByCredentials(array $credentials)
    {
        if (! array_key_exists('email', $credentials)) {
            return null;
        }

        if (! array_key_exists('password', $credentials)) {
            return null;
        }

        $response = Http::post('https://example.com/authenticate', [
            'email' => $credentials['email'],
            'password' => $credentials['password'],
        ]);

        if (! $response->ok()) {
            return null;
        }

        $payload = $response->json();

        try {
            $identifier = \json_encode($payload, \JSON_THROW_ON_ERROR);

            $payload['id'] = $this->encrypter->encrypt($identifier);
        } catch (\JsonException $exception) {
            return null;
        } catch (EncryptException $exception) {
            return null;
        }

        /////////////////////////////////////////////////////
        // ADDED HERE
        /////////////////////////////////////////////////////
        $payload['remember_token'] = '';

        return new GenericUser($payload);
    }

    public function validateCredentials(Authenticatable $user, array $credentials)
    {
        return true;
    }
}
1 like
laravel-student's avatar

Hi, Thank you very much for this code. This saves lot of my time. Fantastic and great work. One question, how we refresh the session using refresh token from the external API.

MarkusHH78's avatar

Ok that works now.

Man, thank you very much for your help!!

Is it possible to send you a private message?

Bejkrools's avatar

I have issue with combine this The custom user provider with JWT authorization. I can authorize user with credential with external API, but afterwards I can't authorize it with JWT token. I always get Unauthorized message.

App\Models\ApiUser

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;

class ApiUser extends Authenticatable implements JWTSubject
{
    protected $fillable = ['id', 'name'];

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return [];
    }
}

App\Providers\ApiUserProvider

namespace App\Providers;

use App\Models\ApiUser;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Encryption\EncryptException;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Http;
use Tymon\JWTAuth\Facades\JWTAuth;

class ApiUserProvider implements UserProvider
{

    public function retrieveById($identifier)
    {
        //identifier value is 0.
        die('retrieveById');
    }

    public function retrieveByToken($identifier, $token)
    {
        die('retrieveByToken');
    }

    public function updateRememberToken(Authenticatable $user, $token)
    {
        die('updateRememberToken');
    }

    public function retrieveByCredentials(array $credentials)
    {
        if (! array_key_exists('login', $credentials)) {return null;}
        if (! array_key_exists('password', $credentials)) {return null;}

        $response = $this->loginWithApi($credentials['login'], $credentials['password']);

        if (!$response->ok()) {return null;}

        $userData = $response->json();
        $payload['name'] = $userData['data']['player']['name'];
        try {
            $identifier = \json_encode($payload, \JSON_THROW_ON_ERROR);
            $payload['id'] = Crypt::encryptString($identifier);
        } catch (\JsonException $exception) {
            return null;
        } catch (EncryptException $exception) {
            return null;
        }

        return new ApiUser($payload);
    }


    public function validateCredentials(Authenticatable $user, array $credentials)
    {
        return true;
    }

    private function loginWithApi(string $login, string $password): Response
    {
        ...
    }
}

I worries about User id property (from auth()->user()). In resnpodWithToken() method this value is 0.

class AuthController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth:api', ['except' => ['login']]);
    }

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

        if (! $token = auth()->attempt($credentials)) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        return $this->respondWithToken($token);
    }

    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type'   => 'bearer',
            'expires_in'   => auth()->factory()->getTTL() * 60,
            'user'         => auth()->user(),
        ]);
    }
	
	public function me()
    {
        return response()->json(auth()->user());
    }
}
class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [];

    public function boot()
    {
        $this->registerPolicies();

        Auth::provider('ApiUserProvider', function ($app, array $config) {
            return new ApiUserProvider();
        });
    }
}
'defaults' => [
        'guard' => 'api',
        'passwords' => 'users',
    ],

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'ApiUserProvider',
        ],
        'api' => [
            'driver' => 'jwt',
            'provider' => 'ApiUserProvider',
        ],
    ],

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
        'ApiUserProvider' => [
            'driver' => 'ApiUserProvider',
        ]
    ],
dietcheese's avatar

To add to this, if you're trying to use Laravel's auth scaffolding with a remote API backend, your user model will need the following methods:

public function getAuthIdentifier(){}

public function getAuthIdentifierName(){}

public function getAuthPassword(){}

Info on those methods: https://laravel.com/api/8.x/Illuminate/Contracts/Auth/Authenticatable.html

If you are planning on using the Password Reset functionality, you may need to override the default PasswordBroker with a custom PasswordBroker and implement the getEmailForPasswordReset() and sendPasswordResetNotification() methods on your user model.

mikeminish's avatar

I am also using custom user provider. I login to my laravel using API of another laravel installation. I am just wondering how is it possible to save the user data returned from successfull login? This installation does not have a database.

danh65's avatar

I am trying to authenticate users from our own sso auth server and am not able to get it to work. I've done everything in the steps outlined in this thread but can't get it to work. I'm getting a 401 Unauthorized from any api endpoint I try.

The problem does not seem to be from the custom user provider (which I'm calling 'cam'), as I'm not seeing any of the functions being called. Rather, it seems I am missing some configuration or something.

In my AuthServiceProvider.php file I have the Auth::provider specified as: Auth::provider('cam', function ($app, array $config) { logger('cam provider callback function, config: ' . json_encode($config)); return new CamUserProvider(); });

In config/auth.php I have the providers array as: 'providers' => [ 'users' => [ 'driver' => 'cam' ] ],

The route I'm trying to access is: Route::middleware('auth:sanctum')->get('/user', function (Request $request) { return $request->user(); });

The client retrieves a jwt from the auth server and then sends a post request the api backend through a sign_in route and that function retrieves the user object from the auth server and calls Auth::login($user). That all is working and I can see it's setting a Cookie in the Response. I'm not including any of that code here for simplify.

Then when I try to access the above route, I get a 401 Unauthorized error. Any idea what I am missing? Thanks in advance!

Please or to participate in this conversation.