Rely on Laravel's Authorization as much as possible
Tags: php, laravel
Published: 2023-04-13 21:58:00

---
title: Rely on Laravel's Authorization as much as possible
published: true
description: Learn why sticking with Laravel Authorization features can help you ease the maintenance and evolution of your app.
tags: php, laravel
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l5w6zqgn64xwgeqg8etq.jpg
published_at: 2023-04-13 21:58 +0000
---

Hi and welcome to this article where I will show you the benefit of Laravel's Authorization features and why it is important to stick to it as much as possible for everything related to access control.

## Summary

- [Authorization preview](#authorization-preview)
- [Example: adding another authorization layer](#example-adding-another-authorization-layer)
- [The mistake: mixing authorization layers](#the-mistake-mixing-authorization-layers)
- [Conclusion](#conclusion)

## Authorization preview

Laravel offer an elegant way to perform access control verification within your app.

Let us say you build a tech news platform where your users can contribute by posting articles. Your users can create and edit blog posts.

The route to edit a blog post is `GET /post/{post}/edit` and the route to save the changes is `PUT /post/{post}`.

Now let us imagine a malicious user have found the id of the blog post of another user, and want to edit it on behalf of the author. You want to prevent this from happening and here is how you can do it with Laravel's policies.

```php
Route::middleware("can:edit,post")->group(function() {
  Route::get("post/{post}/edit", [PostController::class, "edit"]);
  Route::put("post/{post}", [PostController::class, "update"]);
});
```

As you can see, we instructed Laravel to protect our routes by checking if the user can edit posts (`can:edit,post`). Here `can` is a pre-registered middleware. You can view it on your route kernel file at "app/Http/Kernel.php":

```php
namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
  protected $middlewareAliases = [
    // ...
    "can" => \Illuminate\Auth\Middleware\Authorize::class,
    // ...
  ];
}
```

This middleware assumes you either registered a [Policy](https://laravel.com/docs/10.x/authorization#creating-policies) (which attach to an Eloquent model) or a [Gate](https://laravel.com/docs/10.x/authorization#gates) (which can be attached to anything).

Most of the time I write authorization around models, so I like to use Policies as much as possible. Here is the policy that would prevent other person than the author from editing a blog post:

```php
namespace App\Policies;
 
use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;

class PostPolicy
{
  public function update(User $user, Post $post): Response
  {
    if ($post->author->is($user)) {
      return $this->allow();
    }

    return $this->deny("Only the author can edit this post.");
  }
}
```

Finally, policies require you to register them to tell Laravel how it should link them automatically when using the Route middleware syntax.

On your file at "app/Providers/AuthServiceProvider.php":

```php
namespace App\Providers;

use App\Models\Post;
use App\Policies\PostPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
  protected $policies = [
    Post::class => PostPolicy::class,
  ];
}
```

## Example: adding another authorization layer

Now let us imagine your tech news platform have a back office. You can get various statistics about how your app performs, you can view the blog post count, and other anaylitcs.

You only allow your team mates to browse this back office so you decide to use a permission system like [spatie/laravel-permission](https://github.com/spatie/laravel-permission).

For the moment you use two roles, "admin" and "manager". Admins can view all the back office and change the color of the website, but manager can only views pages without ability to change anything.

You protect these routes using the provided "role" middleware of this package:

```php
Route::middleware("permission:view admin")->group(function() {
  Route::get("admin", [AdminController::class, "index"]);
]);

Route::middleware("permission:update settings")->group(function() {
  Route::get("admin/settings", [AdminSettingsController::class, "edit"]);
  Route::put("admin/settings", [AdminSettingsController::class, "update"]);
});
```

And on your layout, you only want to display the admin button for admins and managers:

```blade
<ul id="menu">
  @if(user()->hasPermissionTo("view admin"))
    <li>
      <a href="{{ route("admin.index") }}">Admin</a>
    </li>
  @endif
</ul>
```

## The mistake: mixing authorization layers

For the moment our system is fine. Now we imagine admins have the ability to suspend accounts. You have added a flag on your user model for this:

```php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
  public function up(): void
  {
    Schema::table('users', function (Blueprint $table) {
      $table->boolean("suspended")->default(false);
    });
  }

  public function down(): void
  {
    Schema::table('users', function (Blueprint $table) {
      $table->dropColumn("suspended");
    });
  }
};
```

Naturally, you will create a Policy to handle it:

```php
namespace App\Policies;
 
use App\Models\User;
use Illuminate\Auth\Access\Response;

class UserPolicy
{
  public function before(User $user): Response
  {
    if ($$user->suspended) {
      return $this->deny("Your account has been suspended");
    }

    return null;
  }
}
```

Now we hit a roadblock: our authorization logic is now spread across two systems, permissions and policies. Let us fix this by keeping the UserPolicy and ditching any mention to permissions outside this policy:

```php
namespace App\Policies;

use App\Models\User;
use Illuminate\Auth\Access\Response;

class UserPolicy
{
  public function before(User $user): ?Response
  {
    if ($$user->suspended) {
      return $this->deny("Your account has been suspended");
    }

    return null;
  }

  public function viewAdmin(User $user): Response
  {
    if ($user->hasPermissionTo("view admin")) {
        return $this->allow();
    }

    return $this->deny("You do not have the permission to view this page.");
  }

  public function editSettings(User $user): Response
  {
    if ($user->hasPermissionTo("edit settings")) {
        return $this->allow();
    }

    return $this->deny("You do not have the permission to edit settings.");
  }
}
```

Then, let us change the routes to rely on Laravel's authorization system instead:

```diff
- Route::middleware("permission:view admin")->group(function() {
+ Route::middleware("can:viewAdmin")->group(function() {
    Route::get("admin", [AdminController::class, "index"]);
  ]);
```

```diff
- Route::middleware("permission:update settings")->group(function() {
+ Route::middleware("can:editSettings")->group(function() {
    Route::get("admin/settings", [AdminSettingsController::class, "edit"]);
    Route::put("admin/settings", [AdminSettingsController::class, "update"]);
  });
```

And finally, let us update the layout to use `@can` instead of `@if()`

```diff
  <ul id="menu">
-   @if(user()->hasPermissionTo("view admin"))
+   @can("viewAdmin")
      <li>
        <a href="{{ route("admin.index") }}">Admin</a>
      </li>
-   @endif
+   @endcan
  </ul>
```

## Conclusion

By sticking to the built-in capabilities of Laravel, you ensure your app is easy to maintain and to evolve in the future. 

If tomorrow you want to replace "spatie/laravel-permission" by another system (or building your own), the only part of the code you will change is the `ÙserPolicy`.

In the first part of this article I voluntarily open a nasty security breach by assuming users could discover the blog post edit route. In reality, you want to make this hard and I advice to expose UUIDs or any hard to predict identifier in your URLs instead of `/post/34/edit`. I have created a package to help you do the least possible while securing your route. Check it out.

{% github https://github.com/khalyomede/laravel-eloquent-uuid-slug %}

I hope this article helped you clarify the goal of Laravel Authorization and will help you make your app more robust. Please let me know your thoughts in the comments!

Happy access control!

Cover image by [Peter Olexa](https://www.pexels.com/@peter-olexa-2214257/).