Andy Noelker
Andy Noelker

Reputation: 11269

Use multiple Auth guards for one Policy

I have implemented multiple Auth guards in a Laravel 5.4 project (one of for admins and the other for regular users). This has worked successfully so far and both admins and users are able to log in. I am now trying to implement a Policy class that works for both Auth guards. This is because I have certain models that I want all administrators to edit and only users who own the model to be able to edit. So I have defined a policy with this method.

App\Policies\ModelPolicy

public function update(User $user, Model $model)
{
    if ($user->id === $model->user_id) {
        return true;
    }

    if (Auth::guard('admin')->check()) {
        return true;
    }

    return false;
}

Then in whatever controller method I have for my model:

App\Http\Controllers\ModelController

public function update(Model $model)
{
    $this->authorize('update', $model);

    // update model
}

This works perfectly if a regular user is logged in. However, when an admin user is logged in, it doesn't even reach the policy (I know this from error logging). I am guessing that the Policy class does something to automatically deny a request if the default guard in Auth::check() fails. However, since it is valid for my users to have one of several guards (not just the default), I need to bypass this behavior.

I know I could implement the admin logic in my controller method and only use the policy if I know I am dealing with a non-admin:

public function update(Model $model)
{
    if (!Auth::guard('admin')->check()) {
        $this->authorize('update', $model);
    }

    // update model
}

However, this can quickly spiral out of control if my admin condition is more complicated than simply being logged in. More importantly, all of this logic belongs in a Policy, not muddying up my controller.

How is it possible to use the same Policy class for multiple authentication guards?

Upvotes: 7

Views: 4849

Answers (2)

Andy Noelker
Andy Noelker

Reputation: 11269

I ended up overriding the authorize method on the base controller class to make the correct Guard the default Guard. Then, the $user argument passed into my policy will be an instance of whichever Auth guard the current user is logged in as.

app/Http/Controllers/Controller.php

use Auth

class Controller extends BaseController
{
    use DispatchesJobs, ValidatesRequests;
    use AuthorizesRequests {
        authorize as protected baseAuthorize;
    }

    public function authorize($ability, $arguments = [])
    {
        if (Auth::guard('admin')->check()) {
            Auth::shouldUse('admin');
        }

        $this->baseAuthorize($ability, $arguments);
    }
}

Now that the Policy will be passed in either my User model or my Admin model, I need to make sure that I remove the type-hinting for the argument and check the type of the model that is passed in. I don't need to do any Auth::check() because I know that the $user that is passed in must be a logged in user of the type that I want.

App\Policies\ModelPolicy

use App\User;

public function update($user, Model $model)
{
    if ($user instanceof User) {
        return $user->id == $userId;
    }

    // Is an Admin
    return true;
}

And now I have access to desired Auth guard to do whatever I want with it in my Policy.

Upvotes: 7

madpoet
madpoet

Reputation: 1063

You can override the "authorize" method in your common controller (/app/Http/Controllers/Controller.php):

class Controller extends BaseController
{
    use AuthorizesResources, DispatchesJobs, ValidatesRequests;
    use AuthorizesRequests {
        authorize as protected laravelAuthorize;
    }

    public function authorize($ability, $arguments = [])
    {
        if (!Auth::guard('admin')->check()) {
            $this->laravelAuthorize($ability, $arguments);
        }
    }
}

Upvotes: 0

Related Questions