How to Use Laravel 11 Gates and Policies to Secure Your Application Data through Your Controllers and Routes
Introduction
Securing your application data is a crucial aspect of building robust web applications. Laravel 11 provides a powerful mechanism to handle authorization through Gates and Policies. This post will guide you through the process of using Gates and Policies to secure your application data within a controller, using a Post
model as an example.
What are Gates and Policies?
Gates and Policies are two complementary features in Laravel that help in authorizing actions. Gates provide a simple closure-based approach to authorization, whereas Policies are dedicated classes that group authorization logic for a specific model or resource.
Defining a Policy
In Laravel, a Policy is a class that contains authorization logic for a particular model. Let’s define a PostPolicy
to manage authorization for viewing, updating, and deleting posts.
PostPolicy.php
<?php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Post $post): bool
{
return $user->isAdmin() || $user->isEditor() || $post->author_id == $user->id;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Post $post): bool
{
return $user->isAdmin() || $user->isEditor() || $post->author_id === $user->id;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Post $post): bool
{
return $user->isAdmin() || $user->isEditor() || $post->author_id === $user->id;
}
}
In this PostPolicy class, the methods view
, update
, and delete
determine if a user can perform these actions based on their role (admin or editor) or if they are the author of the post.
Applying Policies in a Controller
To use these policies in your controller, you will leverage the Gate::authorize
method. This method checks the authorization and throws an exception if the user is not authorized.
PostController.php
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use InvalidArgumentException;
class PostsController extends Controller
{
public function find(int $postId)
{
$post = Post::find($postId);
Gate::authorize('view', $post);
return response()->json($post);
}
public function update(Request $request, int $postId)
{
$data = $request->only(['title', 'body']);
$post = Post::with('author')->find($postId);
Gate::authorize('update', $post);
$post->update($data);
return response()->json($post);
}
public function delete(int $postId)
{
$post = Post::find($postId);
Gate::authorize('delete', $post);
$deleted = $post->delete();
if (!$deleted) {
throw new InvalidArgumentException('Could Not Delete Post');
}
return response()->json();
}
}
In this PostsController
class:
- The
find
method retrieves a post and authorizes the ‘view’ action. - The
update
method updates a post after authorizing the ‘update’ action. - The
delete
method deletes a post after authorizing the ‘delete’ action.
If the current user is not authorized to perform the action, Gate::authorize
will throw an AuthorizationException
, effectively preventing unauthorized access.
Manually Handling Authorization Failures
While Gate::authorize is convenient, it throws an exception when the check fails, but there are scenarios where you might want to manually handle the failure of an authorization check. For example, you might want to return a custom error message or perform some other action.
Manual Authorization Check
Here’s how you can manually handle authorization failures in a controller method:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use InvalidArgumentException;
class PostsController extends Controller
{
public function create(Request $request)
{
if ($request->user()->cannot('create', Post::class)) {
abort(403, 'You are not authorized to create a post');
}
// Proceed with post creation
$data = $request->only(['title', 'body']);
$post = Post::create($data);
return response()->json($post);
}
// Other methods (find, update, delete)...
}
In this create method, we manually check if the user has permission to create a post using $request->user()->cannot('create', Post::class)
. If the user is not authorized, we use abort(403, ‘You are not authorized to create a post’) to return a 403 Forbidden response with a custom message.
Defining Gates
Gates can be defined in the AuthServiceProvider
or anywhere in your application bootstrapping process. Here’s how to define gates using both closure and policy methods.
Closure-Based Gates
You can define a gate using a closure in the AuthServiceProvider
:
use Illuminate\Support\Facades\Gate;
use App\Models\Post;
use App\Models\User;
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Gate::define('update', function (User $user, Post $post) {
return $user->id === $post->author_id;
});
}
This gate checks if the user’s ID matches the author ID of the post.
Policy-Based Gates
You can also define a gate to use a specific method from a policy:
use Illuminate\Support\Facades\Gate;
use App\Policies\PostPolicy;
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Gate::define('update', [PostPolicy::class, 'update']);
}
This gate uses the update
method from the PostPolicy
class to perform the authorization check.
We have attached the PostPolicy as a handler for the PostModel, but if we created a policy class using a convention of [ModelName][Policy]
, this definition would not be necessary if the gate name update
and policy method handler update
are the same.
This is because using the convention to name your policy Laravel will discover the policy and link it to the model used in the name automatically, given that it is in the policies folder. However, if for example in this example above we wanted to not use the covention and use a policy class like DraftPostPolicy
and our post model is called Post, then Laravel will not pick it up automatically, that is when the code snippet above is neccessary, so instead we’d do something like:
use Illuminate\Support\Facades\Gate;
use App\Policies\DraftPostPolicy;
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Gate::define('update', [DraftPostPolicy::class, 'update']);
}
Using Gates at the Route Level
Laravel also allows you to apply gates directly in route definitions using middlewares. This approach is helpful for simple authorization checks that can be defined directly within your route declarations.
Middleware-Based Authorization in Routes
You can use the can
middleware to check authorizations at the route level:
use App\Models\Post;
Route::put('/post/{post}', function (Post $post) {
// The current user may update the post...
})->middleware('can:update,post');
Route::post('/post', function () {
// The current user may create posts...
})->middleware('can:create,App\Models\Post');
In these examples:
- The
PUT /post/{post}
route uses thecan:update,post
middleware to ensure the user can update the specified post. - The
POST /post
route uses thecan:create,App\Models\Post
middleware to ensure the user can create a new post.
Route Method Authorization
You can also use the can
method directly in your route definition:
use App\Models\Post;
Route::post('/post', function () {
// The current user may create posts...
})->can('create', Post::class);
This example checks if the current user can create a post before executing the route’s closure.
Using Gates at the Blade Template Level
Laravel’s Blade templating engine makes it easy to include authorization checks directly in your views. You can use the @can
, @cannot
, @elsecan
, and @else
directives to conditionally display content based on the user’s permissions.
Blade Example
Here’s how you can use Gates at the Blade template level:
@can('update', $post)
<!-- The current user can update the post... -->
@endcan
@can('create', App\Models\Post::class)
<!-- The current user can create new posts... -->
@endcan
@cannot('delete', $post)
<!-- The current user cannot delete this post... -->
@endcannot
@elsecan('create', App\Models\Post::class)
<!-- The current user can create new posts... -->
@else
<!-- The current user cannot update or create posts... -->
@endcan
In this example:
@can('update', $post)
checks if the current user can update the given post.@can('create', App\Models\Post::class)
checks if the current user can create a new post.@cannot('delete', $post)
checks if the current user cannot delete the given post.@elsecan('create', App\Models\Post::class)
provides an alternative block if the user can create a post but not update or delete.@else
is used as a fallback for users who do not meet any specified conditions.
Registering Policies
To ensure Laravel knows about your policies, you need to register them in the AuthServiceProvider.
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
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
Post::class => PostPolicy::class,
];
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
}
}
Here, we map the Post model to the PostPolicy class.
We have attached the PostPolicy as a handler for the Post Model, but if we created a policy class using a convention of [ModelName][Policy]
, this definition in the AuthServiceProvider
would not be necessary as laravel will discover it automatically, given that it is in the policies folder. However, if for example in this example above we wanted to not use the covention and use a policy class like DraftPostPolicy
and our post model is called Post, then Laravel will not pick it up automatically, that is when the code snippet above is neccessary, so instead we’d do something like:
protected $policies = [
Post::class => DraftPostPolicy::class,
];
Conclusion
By using Laravel 11’s Gates and Policies, you can implement robust security measures in your application. Policies allow you to encapsulate authorization logic in dedicated classes, keeping your controllers clean and focused. The Gate::authorize
method ensures that only authorized users can perform specific actions, throwing exceptions for unauthorized attempts. Additionally, manually handling authorization failures provides flexibility to customize responses and actions based on your application’s needs. Defining gates in the AuthServiceProvider
allows you to centralize and manage your authorization logic effectively.
Applying authorization at the route level using middleware and the can
method ensures a clean and declarative approach to securing your routes. This method enhances the readability and maintainability of your route definitions while providing robust security. Furthermore, using gates within Blade templates ensures that only authorized content is rendered, enhancing the user experience and security of your application.
This approach not only secures your application data but also makes your authorization logic reusable and easy to maintain. Happy coding!