Authorization decides what an authenticated user may do. Laravel separates authentication (who) from authorization (what) using gates for simple checks and policies for model-centric rules.
Policies for Model Actions
Generate policies with php artisan make:policy PostPolicy –model=Post. Methods map to actions: viewAny, view, create, update, delete.
Register policies in AuthServiceProvider or rely on auto-discovery when naming conventions match.
Step 1: Define policy methods
public function update(User $user, Post $post): bool { return $user->id === $post->user_id; } returns true only for authors.
Return false or throw AuthorizationException for denials. Use viewAny for index authorization before querying lists.
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
Step 2: Authorize in controllers
$this->authorize('update', $post) calls the policy before update logic runs. authorizeResource in constructor maps REST actions automatically.
Form requests can implement authorize() returning $this->user()->can('update', $this->route('post')) for single entry points.
$this->authorize('update', $post);
Step 3: Blade @can directives
@can('update', $post) shows edit buttons only for permitted users. @cannot and @else provide alternative markup.
Avoid expensive policy queries inside tight loops—precompute permissions or use gates with cached roles.
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan
Step 4: Middleware can and authorize
Route::put('/posts/{post}', …)->middleware('can:update,post') resolves policy from route parameter. Use for API routes without controller authorize calls.
can middleware accepts gate names or policy abilities with model route parameters.
->middleware('can:update,post')
Gates for General Abilities
Gates define closures in AuthServiceProvider boot() for abilities not tied to one model, like access-admin-dashboard.
Gates can return Response objects with messages instead of boolean false for richer denial feedback.
Step 1: Define gates in boot
Gate::define('access-admin', fn (User $user) => $user->is_admin); centralizes admin checks.
Use Gate::before to grant super-admin all abilities without listing every policy method.
Gate::define('access-admin', fn (User $user) => $user->role === 'admin');
Step 2: Check gates in code
Gate::allows('access-admin') or $user->can('access-admin') returns boolean. @can('access-admin') works in Blade without a model.
abort_unless(Gate::allows('access-admin'), 403) stops unauthorized access early in controller methods.
abort_unless(auth()->user()->can('access-admin'), 403);
Step 3: Gate responses with messages
Return Response::deny('Subscription required') from gates to expose reasons to JSON clients or Inertia pages.
Policies may also return Response::denyWithStatus(402) for payment-required style APIs.
return Response::deny('You need an active plan.');
Step 4: Inline gates for prototypes
Gate::define('edit-settings', fn ($user) => …) is fine early on, but move stable rules into policies as models mature.
Document abilities in a central enum or constants class so frontend teams know which permissions exist.
Gate::define('manage-billing', fn ($user) => $user->subscribed());
Roles, Teams, and Testing Authorization
Many apps layer roles on users via packages like spatie/laravel-permission or custom role tables checked inside policies.
Test policies with actingAs and assert true/false from gate results to prevent authorization regressions.
Step 1: Role checks inside policies
return $user->hasRole('editor') || $user->id === $post->user_id; combines role-based and resource ownership rules.
Keep role names in config or database seeds. Avoid hard-coding magic strings across dozens of policies.
return $user->hasRole('admin') || $user->id === $post->user_id;
Step 2: Policy before hook
public function before(User $user, string $ability) { if ($user->isSuperAdmin()) return true; } skips other checks for admins.
Returning null continues to other methods. Returning false denies immediately.
public function before(User $user) {
return $user->is_admin ? true : null;
}
Step 3: Testing with Pest or PHPUnit
$user = User::factory()->create(); $this->actingAs($user)->get(route('posts.edit', $post))->assertForbidden(); proves policy enforcement.
Use Policy::fake() sparingly; prefer integration tests hitting real policies.
$this->assertTrue($user->can('update', $post));
Step 4: Authorize resource controllers
$this->authorizeResource(Post::class, 'post'); in constructor maps index to viewAny and destroy to delete automatically.
Ensure route parameter name matches the second argument so Laravel resolves models correctly.
$this->authorizeResource(Post::class, 'post');
Aadyanex designs Laravel authorization with explicit policies, audited admin actions, and least-privilege roles for regulated industries.
