Back to Blog
Lesson 25 of the Intermediate Laravel: Real-World Application Patterns course
LaravelJune 26, 20263 min read

Implementing Laravel Policies for Robust Authorization

Learn to implement Laravel policies to centralize and simplify your authorization logic. Secure your project board resources with clean, reusable access rules.

laravelauthorizationpoliciessecuritybackendphp

Previously in this course, we explored Implementing Middleware for API Security in Laravel to verify user ownership at the route level. While middleware is excellent for broad access control, it often leads to code duplication when your authorization logic grows complex. Today, we’ll move beyond middleware by implementing policies, a dedicated layer for granular, domain-specific authorization.

Why Policies?

In our project board application, we need to decide if a user can update a task, delete a project, or add a team member. If we keep this logic inside controllers, they become bloated, hard to test, and difficult to maintain.

Laravel policies allow us to encapsulate authorization logic for specific Eloquent models into dedicated classes. This is the "Goldilocks" zone of security: it’s more flexible than simple middleware and more readable than scattered if statements throughout your service layer.

Generating and Implementing Policies

To start, generate a policy for our Task model using Artisan:

Bash
php artisan make:policy TaskPolicy --model=Task

This creates app/Policies/TaskPolicy.php. Let's implement a rule that only the task creator or a project manager can update a task.

PHP
namespace App\Policies;

use App\Models\Task;
use App\Models\User;

class TaskPolicy
{
    public function update(User $user, Task $task): bool
    {
        #6A9955">// A user can update if they own the task 
        #6A9955">// or if they are the project owner
        return $user->id === $task->user_id || 
               $user->id === $task->project->user_id;
    }
}

Laravel automatically discovers this policy because it follows the naming convention (Task -> TaskPolicy). You don't need to manually register it unless you are using a custom structure.

Authorizing Actions in Controllers

With the policy defined, we no longer need to manually check if ($user->id !== $task->user_id) in our controller. Instead, we use the authorize method provided by the AuthorizesRequests trait (included by default in base controllers).

Refactoring our TaskController becomes trivial:

PHP
public function update(Request $request, Task $task)
{
    #6A9955">// Throws an AuthorizationException if the policy returns false
    $this->authorize('update', $task);

    $this->taskService->updateTask($task, $request->validated());

    return response()->json(['message' => 'Task updated successfully']);
}

If the user isn't authorized, Laravel automatically throws an Illuminate\Auth\Access\AuthorizationException, which we handle globally as discussed in our lesson on Mastering Laravel Exception Handling.

Hands-on Exercise: Implementing Project Deletion

  1. Generate a ProjectPolicy for your Project model.
  2. Define a delete method in that policy. A project should only be deletable by the user who created it.
  3. Apply the $this->authorize('delete', $project) call inside your ProjectController@destroy method.
  4. Verify the behavior by attempting to delete a project as a user who doesn't own it; ensure you receive a 403 Forbidden response.

Common Pitfalls

  • Forgetting to pass the Model instance: Always pass the instance, not the class name, when checking instance-specific authorization (e.g., $this->authorize('update', $task)). Use the class name only for "create" actions (e.g., $this->authorize('create', Task::class)).
  • Over-complicating policies: Policies should be simple boolean checks. If your authorization logic requires complex database queries or external API calls, consider moving that logic into a dedicated "Guard" service and calling it from your policy.
  • Ignoring User/Guest states: Remember that $user can be null if you allow guest access to certain endpoints. Always type-hint ?User if your policy needs to handle unauthenticated users.

Recap

We’ve moved authorization out of our controllers and into dedicated, testable policy classes. By leveraging authorize(), we keep our controllers thin and our security logic centralized. This approach makes our project board significantly more maintainable as we add features like team roles and permissions.

Up next: We'll dive into Customizing Authentication Guards to support multi-auth scenarios, such as separating API tokens from web-based sessions.

Similar Posts