Week 13: Authentication & Authorization

Week 8's task manager built login by hand: password_hash, sessions, a manual check on every protected route. This week replaces all of it with Laravel's authentication scaffolding, then goes further than Week 8 did — Gates and Policies solve exactly the "delete another user's task" ownership gap flagged at the end of Module 1.

Module 12 of 28 Week 13 of 32 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Scaffold session-based authentication with Laravel Breeze
  • Issue API tokens with Sanctum for SPA & mobile clients
  • Enforce authorization rules with Gates & Policies

1. Laravel Breeze

Breeze is Laravel's official starter kit for authentication — it generates real, editable login/register/password-reset routes, controllers and Blade views, rather than hiding auth behind an opaque package:

terminal
composer require laravel/breeze --dev
php artisan breeze:install blade
php artisan migrate
npm install && npm run dev

This generates a users migration (if not already present), registration/login/logout routes, and Blade views for each. Under the hood it's doing exactly what Week 8 built by hand — password_hash() on registration, password_verify() and session regeneration on login — through Laravel's Auth facade instead:

the pattern Breeze generates, simplified
<?php

use Illuminate\Support\Facades\Auth;

// Login
if (Auth::attempt(['email' => $email, 'password' => $password])) {
    request()->session()->regenerate(); // exactly Week 8's session_regenerate_id(true)
    return redirect()->intended('dashboard');
}

// Anywhere in the app afterward
Auth::user();     // the currently logged-in User model, or null
Auth::id();        // just their ID
Auth::check();     // bool -- is anyone logged in?
Auth::logout();

2. The auth Middleware

Week 8's protected pages checked $_SESSION['logged_in'] manually at the top of every file. Laravel's auth middleware does the same job, applied declaratively to a route or group of routes:

routes/web.php
<?php

use Illuminate\Support\Facades\Route;

Route::middleware('auth')->group(function () {
    Route::resource('tasks', TaskController::class);
    Route::get('/dashboard', DashboardController::class);
});

// Unauthenticated visitors hitting any route in this group are
// redirected to the login page automatically -- no manual check needed
// in every controller method.

This is worth pausing on: the entire "check session, redirect if not logged in" pattern from Week 5 and Week 8 is now a one-line declaration applied to a whole group of routes at once, rather than repeated logic at the top of every handler.

3. Sanctum API Tokens

Session-based auth works for a traditional server-rendered app, but an API consumed by a mobile app or separate JavaScript frontend needs something stateless. Sanctum issues personal access tokens instead:

terminal
composer require laravel/sanctum
php artisan install:api
issuing a token
<?php

// In an API login endpoint
$user = User::where('email', $request->email)->first();

if (! $user || ! Hash::check($request->password, $user->password)) {
    return response()->json(['message' => 'Invalid credentials'], 401);
}

$token = $user->createToken('mobile-app')->plainTextToken;

return response()->json(['token' => $token]);
protecting an API route with it
<?php

use Illuminate\Support\Facades\Route;

Route::middleware('auth:sanctum')->get('/api/tasks', function (\Illuminate\Http\Request $request) {
    return $request->user()->tasks;
});

The client stores the token and sends it as Authorization: Bearer <token> on every subsequent request — covered in full in Week 15, once you build a complete REST API around it.

4. Gates

A Gate is a closure-based authorization rule, good for simple, non-model-specific checks:

app/Providers/AppServiceProvider.php
<?php

use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::define('view-admin-panel', function (User $user) {
        return $user->role === 'admin';
    });
}
checking it
<?php

if (Gate::allows('view-admin-panel')) { /* ... */ }
if (Gate::denies('view-admin-panel')) { /* ... */ }

// Or, in a controller:
Gate::authorize('view-admin-panel'); // throws a 403 automatically if denied

// Or, in a Blade view:
@can('view-admin-panel')
  <a href="/admin">Admin Panel</a>
@endcan

5. Policies

A Policy is a Gate scoped to one model — the right tool for exactly the problem flagged at the end of Week 8: "logged in" proves who someone is, not what they're allowed to touch.

terminal
php artisan make:policy TaskPolicy --model=Task
app/Policies/TaskPolicy.php
<?php

namespace App\Policies;

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

class TaskPolicy
{
    public function update(User $user, Task $task): bool
    {
        return $user->id === $task->user_id;
    }

    public function delete(User $user, Task $task): bool
    {
        return $user->id === $task->user_id;
    }
}
TaskController.php — enforcing it
<?php

public function destroy(Task $task)
{
    $this->authorize('delete', $task); // throws 403 automatically if the policy returns false

    $task->delete();
    return redirect()->route('tasks.index')->with('success', 'Task deleted.');
}

$this->authorize('delete', $task) is one line replacing the manual WHERE id = :taskId AND user_id = :currentUserId ownership check from Week 8's exercise — the exact class of bug (authentication without authorization) that quiz called out is now structurally hard to forget, because every model action routes through a policy method whose entire job is answering exactly that question.

6. Hands-on Exercise

Hands-on

Add real auth & ownership checks to the task tracker

Replace any remaining manual session checks with Laravel's auth system, then close the ownership gap from Week 8.

Requirements:

  1. Install Breeze, run its migrations, and confirm registration/login/logout work end to end.
  2. Wrap every tasks.* route in the auth middleware group.
  3. Update TaskController@store to associate new tasks with Auth::id() automatically, rather than accepting user_id from the form.
  4. Update TaskController@index to only show the logged-in user's own tasks.
  5. Generate TaskPolicy with update and delete methods checking ownership, and call $this->authorize() in the corresponding controller methods.
  6. Manually test that logging in as one user and attempting to edit/delete another user's task (by guessing its ID in the URL) returns a 403, not a successful action.
Hint

That last manual test — actually trying to break your own ownership check as a second user — is the whole point of this exercise. A policy method that's never been exercised against a genuine cross-user attempt hasn't really been verified yet.

7. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Is Laravel Breeze's authentication conceptually different from what was built by hand in Week 8?

No — it's the same mechanism (hashed passwords, session-based login state, session regeneration on privilege change) generated as real, editable code rather than a black box. Understanding Week 8's hand-built version first is exactly what makes Breeze's generated code legible rather than magic.

Q2

Why does an API consumed by a separate JavaScript frontend typically use Sanctum tokens instead of session-based auth?

Session-based auth relies on cookies tied to a specific domain, which works cleanly for a traditional server-rendered app but gets awkward across separate domains or native mobile clients that don't naturally carry cookies. A bearer token sent explicitly in an Authorization header on every request works uniformly regardless of client type — a mobile app, a separate SPA, or a third-party integration.

Q3

What's the practical difference between a Gate and a Policy?

A Gate is a standalone closure for a rule that isn't tied to a specific model — "can this user view the admin panel," for example. A Policy groups related authorization rules for one specific model (update, delete, and so on for Task), and Laravel automatically resolves which policy method to call based on the model type passed to authorize().

Q4

Why does TaskPolicy::delete() close the exact gap flagged in Week 8's final quiz?

Week 8 pointed out that authentication (being logged in) and authorization (being allowed to act on a specific resource) are separate checks, and that skipping the ownership check lets any logged-in user delete any task by guessing its ID. TaskPolicy::delete() explicitly compares $user->id === $task->user_id, and $this->authorize('delete', $task) forces that check to run before the delete happens — the same fix, now expressed as a structural pattern the framework enforces rather than something a developer has to remember to write manually on every action.