Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Building Web Apps with Laravel  Module 8 – Authentication and authorization

Authorization: gates and policies

Write a gate and a policy, call them from controllers, routes and Blade, return 404 where a record is private, and test every ability to catch IDOR.

  • Intermediate
  • 15 minutes
  • Examples run with PHP 8.5.11, Composer 2.10.3, Laravel 13.34.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 5 of 15 minutes free
Free: Gates for app-wide abilities, policies for a model · A policy by convention, and four ways to call it. With a pass: 2 in-depth parts and 3 interview answers, about 10 minutes.

What you will learn

  • Write a gate for an app-wide ability and a policy for a model by naming convention
  • Use a policy with Gate::authorize, the can middleware and the @can directive in Blade
  • Choose between a 403 and a 404 from denyAsNotFound where even a record's existence is private
  • Test every ability with negative cases so an IDOR bug is caught before release

Before you start

What you’ll learn · Before you start

What you will learn

  • Write a gate for an app-wide ability and a policy for a model by naming convention
  • Use a policy with Gate::authorize, the can middleware and the @can directive in Blade
  • Choose between a 403 and a 404 from denyAsNotFound where even a record's existence is private
  • Test every ability with negative cases so an IDOR bug is caught before release

Before you start

On this page

A Pathshala guardian may read their own child’s fees but not another family’s; a teacher may edit students at their own centre but not another centre’s; only the owner may delete a record. Authentication proved who the user is, which the earlier lessons covered; authorization decides what that user may do, and Laravel answers it in two shapes.

A authorization gate: A closure, registered with Gate::define, that answers yes or no to an ability such as opening a staff back office; it always receives the user first and may take more arguments, such as a model. is a closure that takes the user and returns yes or no for an app-wide ability, such as opening the staff back office. A authorization policy: A class whose methods each answer one ability for a model, receiving the user and, for abilities about one record, that model instance; Laravel finds it by convention from the model's name. is a class whose methods answer questions about one model: may this user view, update or delete this student? The first two sections build both and print a runnable matrix of who may do what. Later parts add an owner override and the choice between a 403 and a 404. They also close the slip that lets a guardian read another child’s report card by changing a number in the URL.

Gates for app-wide abilities, policies for a model

Reach for a gate when the question has no model behind it. Gate::define('access-admin-area', fn (User $user) => ...) sits in a service provider’s boot method, and Gate::allows('access-admin-area') (or Gate::forUser($user)->allows(...)) answers it anywhere. Each ability is one closure, and Laravel always passes it the user first; a gate may take more arguments, such as a model, but the staff-area question needs none. This is the gate the matrix below asks:

app/Providers/AppServiceProvider.php: the staff-area gate PHP · policy-probe/app/Providers/AppServiceProvider.php
<?php

namespace App\Providers;

use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
    }

    public function boot(): void
    {
        // An app-wide ability that is not tied to one model: may this user open
        // the staff back office at all? A gate always receives the user first
        // (it may take more arguments) and returns a bool. Questions about one
        // record ("edit THIS student") go in StudentPolicy instead.
        Gate::define('access-admin-area', function (User $user): bool {
            return in_array($user->role, ['owner', 'teacher', 'accountant'], true);
        });
    }
}

Reach for a policy when the question is about one record. php artisan make:policy StudentPolicy --model=Student generates a class with a method per ability, and Laravel finds it by convention: App\Policies\StudentPolicy is the policy for App\Models\Student, so no registration is needed. A method such as update receives the user and the model instance, which is exactly what a per-record decision needs. Answer the table’s one question for a check of your own:

Interactive · answer and compare A gate or a policy for this check? Answer the one question. The table marks the tool that fits, with a reason under the one that does not.
Is the check about a specific record (this student, this invoice)?

Answer the question above: the table then marks what fits your case and says why the rest does not.

A gate against a policy, on four things that differ
Tool What it judges Takes a model How it is found Where abilities live
Gate How you define it: Gate::define('access-admin-area', fn (User $u) => ...) Good an app-wide ability, such as opening the back office Mixed the user first; more arguments, such as a model, are optional Mixed registered by name in a service provider Mixed one closure per ability, listed together
Policy How you define it: php artisan make:policy StudentPolicy --model=Student Good actions on one model, such as editing a student Good receives the user and the model instance Good found by convention from the model name Good one method per ability in one class
How the answers decide
  • “Is the check about a specific record (this student, this invoice)?” Yes, there is a model instance: rules out Gate. A per-record question (may this user edit THIS student?) belongs in a policy method that receives the model. A gate can take the model too, but then each such ability is a separate closure that no class groups.
  • “Is the check about a specific record (this student, this invoice)?” No, it is a whole-app permission: rules out Policy. An app-wide ability with no model (open the staff area at all) is a gate; a policy needs a model to attach to.

Key idea

A gate answers an app-wide ability in one closure that receives the user; a policy groups the abilities of one model in one class, and a method about one record receives the user and that record. If the check names a specific record, it belongs in a policy.

A policy by convention, and four ways to call it

A policy method answers one ability by comparing the model’s columns against the user asking. StudentPolicy::update returns true only when the user is a teacher whose centre_id equals the student’s; the id in the URL never decides. This is the whole class the matrix asks, one method per ability:

app/Policies/StudentPolicy.php: four abilities, found by convention PHP · policy-probe/app/Policies/StudentPolicy.php
<?php

namespace App\Policies;

use App\Models\Student;
use App\Models\User;

// Discovered by convention: App\Policies\StudentPolicy is the policy for
// App\Models\Student, so $user->can('update', $student) and
// Gate::authorize('update', $student) both land here. Each method answers one
// ability by comparing the student's columns against the user asking.
class StudentPolicy
{
    // Only staff see the student list; a guardian has no "list every student".
    public function viewAny(User $user): bool
    {
        return in_array($user->role, ['teacher', 'accountant'], true);
    }

    // Staff see students of their own centre; a guardian sees only their child.
    public function view(User $user, Student $student): bool
    {
        if (in_array($user->role, ['teacher', 'accountant'], true)) {
            return $user->centre_id === $student->centre_id;
        }

        return $user->role === 'guardian' && $user->id === $student->guardian_id;
    }

    // A teacher of the student's own centre may edit the record; nobody else.
    public function update(User $user, Student $student): bool
    {
        return $user->role === 'teacher' && $user->centre_id === $student->centre_id;
    }

    // Deleting a student is reserved for the centre owner, whom this policy does
    // not know yet; no ordinary role may delete.
    public function delete(User $user, Student $student): bool
    {
        return false;
    }
}

The same policy is reached from four places: Gate::authorize('update', $student) in a controller, $user->can('update', $student) in any PHP, the can:update,student middleware on a route, and @can('update', $student) in a Blade view. Older code often calls $this->authorize() instead, a helper that comes from the AuthorizesRequests trait. The base controller of a new Laravel 13 app is an empty class without that trait, so the documented call is the Gate facade. Follow one edit request from the URL to the policy and back:

Interactive · message by message One edit request, from URL to policy Follow a teacher's edit request as route model binding resolves the student and the policy decides.
One edit request, from URL to policy, step 5 of 5. Messages so far: Teacher to App: PUT /students/42; App to Policy: update(teacher, student 42); Policy to App: true or false; App to Teacher: 200, or 403 if denied.TeacherPUT requestAppStudent 42Policyupdate()PUT /students/42update(teacher, student 42)true or false200, or 403 if denied

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. A request names student 42. The teacher submits PUT /students/42. The route is /students/{student}, so Laravel reads 42 from the URL.
  2. Binding loads the record. Binding looks up Student 42 by its key and hands the instance to the action. A missing id would already be 404; a found id is loaded whether or not the teacher may touch it.
  3. The controller asks the policy. Before doing any work the action calls Gate::authorize('update', $student). That resolves StudentPolicy by convention and calls its update method with the teacher and the student.
  4. The policy compares columns. update returns true only when the teacher's role is teacher and their centre_id equals the student's centre_id. The id in the URL is never the thing that decides.
  5. Allowed runs; denied is 403. A true lets the controller run and answer 200. A false makes Gate::authorize throw, which Laravel renders as 403. Nothing in the action runs on a denied request.

The runnable matrix seeds three users at centre 1 and three students across two centres, then prints the gate’s answer for each user and the policy’s view, update and delete answers for every student. It is the evidence a plain description cannot give: the exact allow and deny for real rows.

Gate and StudentPolicy decisions for three roles Python · policy_matrix_session.py
from session import run

# Seeds three users of centre 1 and three students across two centres, then
# prints the app-wide gate's answer for each user and the StudentPolicy's answer
# for every role, ability and student, so the allow/deny matrix is visible on
# real seeded data.
run(
    """
php policy_matrix.php
""",
    app=True,
    overlay=("policy-probe",),
)

Output

$ php policy_matrix.php
The gate 'access-admin-area', asked with Gate::forUser($user)->allows():
  teacher     ALLOW
  accountant  ALLOW
  guardian    DENY

StudentPolicy, asked about three students:
  A = Aarav, centre 1, the guardian's own child
  B = Diya, centre 1, another family's child
  C = Imran, centre 2, another centre

teacher (centre 1)
  view    A ALLOW   B ALLOW   C DENY
  update  A ALLOW   B ALLOW   C DENY
  delete  A DENY    B DENY    C DENY

accountant (centre 1)
  view    A ALLOW   B ALLOW   C DENY
  update  A DENY    B DENY    C DENY
  delete  A DENY    B DENY    C DENY

guardian (Aarav's parent)
  view    A ALLOW   B DENY    C DENY
  update  A DENY    B DENY    C DENY
  delete  A DENY    B DENY    C DENY

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 policy_matrix_session.py

A teacher may view and edit both centre-1 students and nothing at centre 2; an accountant may view them but never edit; a guardian may view only Aarav, their own child. Deleting is refused to every role, even a teacher at the student’s own centre. The @can directive hides the edit link from a user who may not use it, but hiding a button is a convenience, never the control; the guard in the controller or route is what refuses the request.

resources/views/students/show.blade.php (the @can directive) PHP · snippets/blade-can.blade.php
{{-- The @can directive asks the same policy a controller would, so a button is
     shown only to a user who may use it. The guard in the controller or route
     is still required: hiding a button is a convenience, never the control. --}}
@can('update', $student)
    <a href="{{ route('students.edit', $student) }}">Edit this student</a>
@endcan

@cannot('update', $student)
    <p>You can view this student but not edit them.</p>
@endcannot

{{-- For an app-wide ability with no model, name the gate: --}}
@can('access-admin-area')
    <a href="{{ route('admin.dashboard') }}">Staff dashboard</a>
@endcan

A form request moves the check next to the rules: its authorize method runs before validation, so a request from the wrong user is a 403 before a single field is examined.

app/Http/Requests/UpdateStudentRequest.php PHP · snippets/form-request-authorize.php
<?php

// A form request can carry the authorization check next to the rules. authorize()
// runs before validation; returning false (or a denied Response) answers 403
// before a single rule is checked, so a request from the wrong user never
// reaches the controller.

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdateStudentRequest extends FormRequest
{
    public function authorize(): bool
    {
        // route('student') is the bound model; $this->user() is the signed-in user.
        return $this->user()->can('update', $this->route('student'));
    }

    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:120'],
        ];
    }
}

Checkpoint

Check: Gates for abilities, policies for models · A policy by convention and how to call it

  1. Question 1 of 2 You need to check whether a user may edit one specific student. Gate or policy?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A policy, because the question is about an action on one model instance

    A question about an action on a specific record is a policy method; a gate fits an app-wide ability that has no model behind it.

  2. Question 2 of 2 Which of these call the StudentPolicy's update ability? Select all that apply.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • Gate::authorize('update', $student) in a controller
    • @can('update', $student) in a Blade view
    • the can:update,student middleware on the route

    Gate::authorize, the can middleware, @can and a form request's authorize method that calls can() all reach the same policy; validation is a separate concern.

A super-admin override, and choosing between a 403 refusal and a 404 that hides a record Pro: opens with a Pro, Premium or Ultimate pass

Two refinements turn the simple matrix into something a real back office needs: a way for the owner to bypass every check, and a choice about how a refusal looks to the person refused.

In this part:

  • An owner override that leaves every other role’s checks alone
  • A refusal that explains, or one that hides the record
  • Making a hidden record answer exactly like a missing one
  • Declaring the check with controller attributes

5 runnable examples (NotFoundBodyTest.php, PHP, Python, SuperAdminAndDenyTest.php, app/Policies/StudentPolicy.php, fixed/bootstrap/app.php, routes/web.php) · about 10 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

How route model binding becomes an IDOR, and the negative tests that catch it Pro: opens with a Pro, Premium or Ultimate pass

Route model binding turns the id in a URL into a loaded record before your action runs, whether or not the signed-in user may see that record. A controller that trusts it lets any guardian read any child’s marks by typing a different number, which is an insecure direct object reference (IDOR): Access to a record through a real identifier, such as an id in a URL, that the application loaded but failed to authorize for the current user..

In this part:

  • Why binding hands over a card that is not theirs
  • Reproducing the leak in a test, then closing it
  • Negative tests for every way access leaks
  • Four rules to keep for authorization you can test

1 diagram · 2 runnable examples (Python, ReportCardIdorTest.php, app/Policies/StudentPolicy.php, routes/web.php) · about 8 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise

Build the authorization layer on your own Pathshala project:

  1. Write StudentPolicy with view, update and delete, discovered by convention, and a Gate::define('access-admin-area', ...) for the staff area. Prove the matrix with a test that seeds a teacher, an accountant and a guardian.
  2. Wrap the edit link of a student page in @can('update', $student), then send the update request directly as the accountant and confirm that Gate::authorize in the controller still answers 403.
  3. Move the same check into a form request’s authorize method and confirm that the wrong user gets a 403 before any validation error appears.

Then test yourself with the questions below.

Practice that adapts to you

Practise this lesson

5 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 5 Where is a gate defined?

    Choose one answer.

    Show the answer: Question 1 of 5

    Answer: In a service provider's boot method with Gate::define

    A gate is a closure registered with Gate::define, typically in a service provider's boot method, and called with Gate::allows.

  2. Question 2 of 5 By convention, which class is the policy for App\Models\Student?

    Choose one answer.

    Show the answer: Question 2 of 5

    Answer: App\Policies\StudentPolicy

    The guessing rule turns App\Models\Student into App\Policies\StudentPolicy, which is why make:policy --model=Student is all you need.

  3. Question 3 of 5 What arguments does a typical policy ability method such as update receive?

    Choose one answer.

    Show the answer: Question 3 of 5

    Answer: The user and the model instance

    A policy method about one record, such as update, receives the authenticated user and the model instance, so it can compare columns such as centre_id or guardian_id. Methods such as viewAny or create receive only the user.

  4. Question 4 of 5 What happens when Gate::authorize('update', $student) is called and the policy returns false?

    Choose one answer.

    Show the answer: Question 4 of 5

    Answer: It throws an AuthorizationException that Laravel renders as 403

    Gate::authorize throws on failure, which Laravel turns into a 403. Use $user->can('update', $student) when you want a boolean instead of an exception.

  5. Question 5 of 5 A teammate has written five Gate::define closures that each take a Student, such as edit-student and archive-student. Why move them into StudentPolicy?

    Choose one answer.

    Show the answer: Question 5 of 5

    Answer: A policy keeps the five abilities in one class that Laravel finds from the Student model, so they are read and tested together

    Gates and policies are both checks that run when something asks, and a gate may take a model. A policy is the better home once several abilities judge the same model: they sit in one class, found by convention from App\Models\Student, and can be tested as a set.

Interview questions

Each of these asks for a decision and its defence, as an interviewer would:

  1. Fresher to mid

    Pathshala's fee module needs four checks: opening the fees dashboard, viewing one student's instalments, recording a payment against one instalment, and exporting a centre's collections for the day. For each, decide whether it is a gate or a policy method, on which model, and where you would call it. Defend each choice.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    When should a Pathshala policy return Response::denyAsNotFound() instead of Response::deny(), and what does a policy's before() method change about the decision? Give a concrete example of each.

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    Pathshala's report-card page uses route model binding. Explain how that becomes an IDOR, how you fix it, and why a passing happy-path test suite can still ship the bug. Describe the tests you would write.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. When do you use a gate, and when a policy?

    A gate is one closure for an app-wide ability with no model behind it, such as opening the staff back office, and it receives the user first. A policy groups the abilities of one model in a class whose methods receive the user and the record. If the check names a specific record, use a policy.

  2. Where is a gate defined, and how is it asked?

    With Gate::define('access-admin-area', fn (User $user) => ...) in a service provider's boot method, and asked anywhere with Gate::allows('access-admin-area'), or Gate::forUser($user)->allows(...) for another user.

  3. How does Laravel find the policy for a model?

    By convention: App\Policies\StudentPolicy is the policy for App\Models\Student, so no registration is needed. php artisan make:policy StudentPolicy --model=Student generates it with one method per ability.

  4. What should a policy method compare to decide an update?

    The record's own columns against the user asking: StudentPolicy::update returns true only for a teacher whose centre_id equals the student's. The id in the URL never decides.

  5. From which places can the same policy be called?

    Gate::authorize('update', $student) in a controller, $user->can('update', $student) in any PHP, the can:update,student middleware on a route, @can in Blade and a form request's authorize method. A new Laravel 13 base controller lacks the AuthorizesRequests trait, so $this->authorize() needs the trait added.

  6. Is hiding the edit button with @can enough to stop an edit?

    No. Hiding a button is a convenience; the authorization call in the controller, the route middleware or the form request is what refuses the request. A form request's authorize runs before validation, so the wrong user gets a 403 before any field is checked.

Key takeaways

  • A gate is one closure for an app-wide ability that receives the user first; a policy is a class, found by convention from the model name, whose methods receive the user and, for a check on one record, that model instance.
  • A policy method decides from the record’s own columns, such as a matching centre_id, and never from the id in the URL.
  • Call a policy with Gate::authorize in a controller, $user->can in any PHP, the can middleware on a route, @can in Blade or can inside a form request’s authorize method, and every one of them reaches the same class.
  • Hiding a button with @can is a convenience; the authorization call in the controller or the route is what refuses the request.

Terms in this lesson

authorization gate
A closure, registered with Gate::define, that answers yes or no to an ability such as opening a staff back office; it always receives the user first and may take more arguments, such as a model.
authorization policy
A class whose methods each answer one ability for a model, receiving the user and, for abilities about one record, that model instance; Laravel finds it by convention from the model's name.
insecure direct object reference (IDOR)
Access to a record through a real identifier, such as an id in a URL, that the application loaded but failed to authorize for the current user.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.