DEV Community

Philip Perry
Philip Perry

Posted on

Using Laravel Policy with middleware to protect routes

So my goal was to only allow the logged in user to view or make changes to data that belongs to him. How do I know which data belongs to him? In the model LanguagePack I save his user id. Any other data like the WordList model has a relation to language pack.

A user accesses the data via an url like this:

/languagepack/wordlist/1
Enter fullscreen mode Exit fullscreen mode

So I don't want him to be able to change the id to 2 if that language pack wasn't created by him and then see and edit that data.

To do this I created a policy class that looks like this:

<?php

namespace App\Policies;

use App\Models\LanguagePack;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;

class LanguagePackPolicy
{
    use HandlesAuthorization;

    public function view(User $user, LanguagePack $languagePack)
    {
        return $user->id === $languagePack->userid;
    }    

    public function update(User $user, LanguagePack $languagePack)
    {
        return $user->id === $languagePack->userid;
    }    

    public function delete(User $user, LanguagePack $languagePack)
    {
        return $user->id === $languagePack->user_id;
    }    
}
Enter fullscreen mode Exit fullscreen mode

Then I created a middleware for getting the language pack model from the route and running the policy check by authorizing actions. If the action is allowed, it will continue the request, otherwise it throws a 403 error and aborts the request.

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class AuthorizeLanguagePack
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse)  $next
     * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
     */
    public function handle(Request $request, Closure $next)
    {
        $languagePack = $request->route('languagePack'); 

        if ($languagePack) {
            $user = $request->user();

            if ($user->can('view', $languagePack) ||
                $user->can('update', $languagePack) ||
                $user->can('delete', $languagePack)) {
                return $next($request);
            }

            abort(403, 'Unauthorized');
        }       

        return $next($request);
    }
}
Enter fullscreen mode Exit fullscreen mode

The middleware has to be registered in the file src/app/Http/Kernel.php like this:

protected $routeMiddleware = [
     //add to the list of route middlewares
     'authorize.languagepack' => \App\Http\Middleware\AuthorizeLanguagePack::class,
    ];
Enter fullscreen mode Exit fullscreen mode

And finally we add the middleware key authorize.languagepack to the routes in web.php:

Route::middleware(['auth', 'authorize.languagepack'])->group(function () {
 Route::get('/dashboard', [DashboardController::class, 'index'])->name('home');    
    Route::get('languagepack/create', [LanguageInfoController::class, 'create']);
    Route::get('languagepack/edit/{languagePack}', [LanguageInfoController::class, 'edit']);
    Route::get('languagepack/wordlist/{languagePack}', [WordlistController::class, 'edit']);
})
Enter fullscreen mode Exit fullscreen mode

There are probably other ways to achieve the same result. Let me know in the comments if you know of a better way...

Top comments (2)

Collapse
 
slimgee profile image
Given Ncube

Love your article, it's a great start and it could be improved we get rid of the custom middleware and first authorize directly into the controller like in the example below

public function update(Request $request, LanguagePack $pack)
{
    $this->authorize('update', $pack);
    //updating stuff
}
Enter fullscreen mode Exit fullscreen mode

Alternatively in the controller's constructor, assuming your routes are "resourceful"

public function __construct()
{
    $this->authorizeResource('langaugePack');
}
Enter fullscreen mode Exit fullscreen mode

Also Laravel already has the can middleware built in by default so you can easily do this

Route::get('languagepack/edit/{languagePack}',[LanguageInfoController::class, 'edit'])
    ->middleware('can:update,languagepack');
Enter fullscreen mode Exit fullscreen mode

I hope this helps, also check out this article on an opinionated way to implement Laravel's authorization

Collapse
 
programmingdecoded profile image
Philip Perry

@slimgee Thanks for your comment. In my case it made sense to use a middleware as I don't want to add $this->authorizeResource('languagePack'); to each controller class or use the built-in middleware for each route (I have many more routes in than I listed in the example). Although I guess if I had created a base controller class, I could have just added it once... There are often many solutions to the same problem and I'm glad for your comment as it will help others who stumble accross it to see what other options exist.