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:
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:
<?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:
<?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:
composer require laravel/sanctum
php artisan install:api
<?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]);
<?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:
<?php
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::define('view-admin-panel', function (User $user) {
return $user->role === 'admin';
});
}
<?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.
php artisan make:policy TaskPolicy --model=Task
<?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;
}
}
<?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
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:
- Install Breeze, run its migrations, and confirm registration/login/logout work end to end.
- Wrap every
tasks.*route in theauthmiddleware group. - Update
TaskController@storeto associate new tasks withAuth::id()automatically, rather than acceptinguser_idfrom the form. - Update
TaskController@indexto only show the logged-in user's own tasks. - Generate
TaskPolicywithupdateanddeletemethods checking ownership, and call$this->authorize()in the corresponding controller methods. - 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.
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?
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?
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?
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?
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.