#auth #development #laravel #php

Imagine you have a Laravel web application with different types of users. Let's you can have both internal as well as external users. Let's also assume that for logging, depending on which guard is used, you want to either allow only internal or exteral users to login. There might be places where you want to allow both.

Shouldn't be hard, except if you happen to have a global scope that filters out all external users. So when you try to login as an external user, you get a ModelNotFoundException because the global scope filters out all external users.

Let's first take a look at how the global scope was configured.

In my apps, I prefer to define them as a class and then add them to the model in the booted method. This way, I can easily reuse them or find them.

app/Scopes/OnlyInternalUsersScope.php

namespace App\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

final class OnlyInternalUsersScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('type', 'internal');
    }
}

app/Models/User.php

namespace App\Models;

use App\Scopes\OnlyInternalUsersScope;

final class User extends Authenticatable
{
    protected static function booted(): void
    {
        self::addGlobalScope(new OnlyInternalUsersScope());
    }
}

At this point, whenever you try to find a user, it will only return users with the type internal.

To configure the authentication, we need to take some extra steps. The first thing I did is to create a class that extends EloquentUserProvider.

It's implementation is pretty simple. It just overrides the retrieveById method and removes the global scope. Doing so will enable it to search for all users, regardless of their type.

app/Providers/AllUsersProvider.php

namespace App\Providers;

use App\Scopes\OnlyInternalUsersScope;
use Illuminate\Auth\EloquentUserProvider;

class AllUsersProvider extends EloquentUserProvider
{
    public function retrieveById($identifier)
    {
        return $this->createModel()
            ->newQuery()
            ->withoutGlobalScope(OnlyInternalUsersScope::class)
            ->find($identifier);
    }
}

The next step is to add a new auth provider to the AuthServiceProvider. In my scenario, I called it external and uses the provider which we just created.

app/Providers/AuthServiceProvider.php

namespace App\Providers;

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

class AuthServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Auth::provider('all-users', function ($app, $config) {
            return new AllUsersProvider($app['hash'], $config['model']);
        });
    }
}

The final step is to configure the auth guards and providers. In the example below, I've added the external guard and a provider called "all-users". The web guard is the default one and is used for internal users. The all-users guard is used for external and internal users and uses the driver we have just defined.

config/auth.php

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

        'all-users' => [
            'driver' => 'session',
            'provider' => 'all-users',
        ],
    ],

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

        'all-users' => [
            'driver' => 'all-users',
            'model' => App\User::class,
        ],
    ],
];

inspired by a post on laracasts.com