Skip to main content

🔐 Laravel Authentication System

Authentication proves who a user is; authorization decides what they may do. Laravel delivers a complete, battle-tested system for both, so you protect your users without reinventing security primitives yourself.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain the difference between authentication and authorization, and choose a Laravel starter kit
  • Understand the User model, log users in and out, and read the current user
  • Protect routes with middleware and understand guards and providers
  • Enforce fine-grained rules with Gates, Policies, and simple roles
  • Secure an API with Sanctum tokens, plus email verification and password reset

Estimated Time: 50–65 minutes  •  Difficulty: Intermediate

Hands-on: Add a Policy so only a post's author (or an admin) can edit or delete it.

In This Lesson

Authentication vs. Authorization

These two words sound alike and are constantly confused, but they answer different questions:

  • Authentication (authn) — "Who are you?" Verifying identity, usually with email and password.
  • Authorization (authz) — "What are you allowed to do?" Deciding whether a known user may perform an action.
sequenceDiagram participant U as User participant L as Login Form participant A as Auth participant DB as Database U->>L: Enter credentials L->>A: Submit A->>DB: Verify email & password hash DB-->>A: Match / no match alt Valid A->>A: Store session, regenerate id A-->>U: Redirect to dashboard else Invalid A-->>U: Show error end
💡 Analogy: Authentication is the security guard checking your ID at the door. Authorization is the keycard that decides which floors your ID can actually open once you are inside.

📖 Key Terms

Guard: how a request is authenticated (session cookie, API token, ...).

Provider: where user records come from (usually the Eloquent User model).

Policy: a class holding authorization rules for one model.

Starter Kits

You rarely hand-write authentication from scratch. Laravel offers official scaffolding, and picking the right one saves days of work.

KitWhat it gives youBest for
BreezeLogin, register, reset, verify — simple Blade (or React/Vue/API) views styled with TailwindMost apps; easy to read and customize
JetstreamEverything in Breeze plus 2FA, session management, teams, SanctumApps needing advanced features out of the box
FortifyHeadless auth backend, no viewsCustom or SPA frontends you build yourself
# Install Breeze with Blade views
composer require laravel/breeze --dev
php artisan breeze:install blade
php artisan migrate
npm install && npm run dev

Breeze generates controllers, Blade views, routes, and middleware — a full login/register/reset/verify flow you can then read and adapt. Because it is your code after installation, nothing is hidden behind a package.

The User Model & Logging In

Authentication centers on the User model. It extends Authenticatable and, in modern Laravel, casts the password with 'hashed' so plain text is never stored.

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable implements MustVerifyEmail
{
    use HasApiTokens, HasFactory, Notifiable;

    protected $fillable = ['name', 'email', 'password'];

    protected $hidden = ['password', 'remember_token'];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password'          => 'hashed',   // auto-hash on assignment
        ];
    }
}

Manual login and logout

Starter kits handle this for you, but seeing it explicitly demystifies the process. Auth::attempt() checks the credentials, and regenerating the session on success defends against session-fixation attacks.

use Illuminate\Support\Facades\Auth;

public function login(Request $request)
{
    $credentials = $request->validate([
        'email'    => ['required', 'email'],
        'password' => ['required'],
    ]);

    if (Auth::attempt($credentials, $request->boolean('remember'))) {
        $request->session()->regenerate();          // prevent fixation
        return redirect()->intended('dashboard');   // back to the target page
    }

    return back()->withErrors([
        'email' => 'These credentials do not match our records.',
    ])->onlyInput('email');
}

public function logout(Request $request)
{
    Auth::logout();
    $request->session()->invalidate();
    $request->session()->regenerateToken();
    return redirect('/');
}

Reading the current user

$user = Auth::user();      // or $request->user(), or auth()->user()
$id   = Auth::id();
if (Auth::check()) { /* someone is logged in */ }

Protecting Routes

The auth middleware is the gate that blocks guests. Attach it to a single route, a group, or apply it in a controller's constructor. Combine it with verified to also require a confirmed email.

// A single protected route
Route::get('/dashboard', [DashboardController::class, 'index'])
    ->middleware('auth');

// A group of routes behind auth + verified email
Route::middleware(['auth', 'verified'])->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit']);
    Route::resource('posts', PostController::class);
});

// Guest-only routes (login, register) use the 'guest' middleware
Route::middleware('guest')->group(function () {
    Route::get('/login', [SessionController::class, 'create'])->name('login');
});

In Laravel 11+, controller middleware is declared with the HasMiddleware interface:

use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;

class PostController extends Controller implements HasMiddleware
{
    public static function middleware(): array
    {
        return [
            'auth',
            new Middleware('verified', except: ['index', 'show']),
        ];
    }
}

💡 redirect()->intended()

When the auth middleware bounces a guest to the login page, it remembers where they were headed. After a successful login, redirect()->intended('dashboard') sends them to that original destination instead of a generic home page.

Guards & Providers

Under the hood, authentication is configured in config/auth.php around two ideas. Guards define how a request is authenticated; providers define where users are fetched from.

flowchart TD A[Request] --> B{Guard} B -->|web| C[session driver] B -->|api| D[sanctum driver] C --> E[Provider] D --> E E -->|eloquent| F[(User model)]
// config/auth.php
'guards' => [
    'web' => ['driver' => 'session', 'provider' => 'users'],
    'api' => ['driver' => 'sanctum', 'provider' => 'users'],
],

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model'  => App\Models\User::class,
    ],
],

You reach for a specific guard when you support more than one kind of user, e.g. a separate admin login: Auth::guard('admin')->attempt(...) or the auth:admin middleware.

💡 Analogy: A guard is the method of checking identity — ID card, fingerprint, or keycode. A provider is the directory of authorized people the guard consults.

Gates & Policies

Once a user is authenticated, authorization decides what they may do. Laravel offers two complementary tools:

  • Gates — simple closures for one-off checks not tied to a model (e.g. "can access the admin area").
  • Policies — classes that group all the rules for one model (e.g. everything about who may view, update, or delete a Post).

A Policy

php artisan make:policy PostPolicy --model=Post
namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    // Runs before every check: admins bypass all rules.
    public function before(User $user, string $ability): ?bool
    {
        return $user->isAdmin() ? true : null;   // null = continue
    }

    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;      // only the author
    }

    public function delete(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }
}

Laravel auto-discovers PostPolicy for the Post model. Enforce it in a controller with authorize(), which aborts with a 403 if the check fails:

public function update(UpdatePostRequest $request, Post $post)
{
    $this->authorize('update', $post);   // 403 unless allowed

    $post->update($request->validated());
    return redirect()->route('posts.show', $post);
}

And hide UI a user cannot use, with the @can Blade directive:

@can('update', $post)
    <a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan

✅ Defense in depth

Use @can to hide buttons and authorize() to enforce the rule on the server. Hiding a link is a courtesy; the server-side check is the actual security boundary.

Roles & Permissions

Laravel has no built-in role system, but a simple one is easy. For a small app, a single role column is often enough:

// Migration
Schema::table('users', function (Blueprint $table) {
    $table->string('role')->default('user');
});

// User model
public function isAdmin(): bool
{
    return $this->role === 'admin';
}

// A Gate that reads the role
Gate::define('access-admin', fn (User $user) => $user->isAdmin());

For anything richer — many roles, granular permissions, assigning permissions at runtime — reach for the well-tested Spatie Laravel Permission package instead of growing your own:

use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasRoles;
}

// Assign and check
$user->assignRole('editor');
$user->givePermissionTo('publish articles');
$user->hasRole('editor');
$user->can('publish articles');

// Guard routes by role or permission
Route::middleware(['role:admin|editor'])->group(fn () => /* ... */);
💡 Analogy: Roles are security-clearance levels; permissions are the specific doors each level can open. Grouping permissions into roles keeps access rules aligned with real job responsibilities.

API Authentication with Sanctum

Sessions and cookies work for server-rendered pages, but APIs and mobile apps need tokens. Laravel Sanctum is the lightweight standard: it issues personal access tokens and can also cookie-authenticate a first-party SPA.

// Issue a token after verifying credentials
public function login(Request $request)
{
    $request->validate([
        'email'       => 'required|email',
        'password'    => 'required',
        'device_name' => 'required',
    ]);

    $user = User::where('email', $request->email)->first();

    if (! $user || ! Hash::check($request->password, $user->password)) {
        throw ValidationException::withMessages([
            'email' => ['The provided credentials are incorrect.'],
        ]);
    }

    $token = $user->createToken($request->device_name)->plainTextToken;
    return response()->json(['token' => $token]);
}
// Protect API routes with the sanctum guard
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', fn (Request $request) => $request->user());
    Route::apiResource('posts', PostController::class);
});

The client sends the token as a Bearer header on every request:

const res = await fetch('/api/user', {
    headers: {
        'Authorization': `Bearer ${token}`,
        'Accept': 'application/json',
    },
});
const user = await res.json();

💡 Verification & reset come free

Because the User model implements MustVerifyEmail and uses the Notifiable trait, email verification and password-reset flows are already wired by the starter kit — you mainly protect routes with the verified middleware and, if you wish, customize the notification emails.

Hands-on Exercise

🏋️ Author-Only Post Editing

Objective: Enforce that only a post's author, or an admin, can edit and delete it.

Instructions:

  1. Generate a policy: php artisan make:policy PostPolicy --model=Post.
  2. Add a before() method that returns true for admins (assume a role column with an isAdmin() helper).
  3. Implement update() and delete() to allow only the post's author.
  4. Call $this->authorize('update', $post) in the controller's edit/update methods, and authorize('delete', $post) in destroy.
  5. Wrap the Edit and Delete buttons in the view with @can('update', $post) and @can('delete', $post).
💡 Hint

The policy is auto-discovered — Post maps to PostPolicy — so you don't need to register it manually in modern Laravel. Remember that before() returning null means "no decision, keep checking the specific method." Hiding the buttons is UX; the authorize() calls are the real enforcement.

✅ Example solution
class PostPolicy
{
    public function before(User $user, string $ability): ?bool
    {
        return $user->isAdmin() ? true : null;
    }

    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }

    public function delete(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }
}

// Controller
public function edit(Post $post)
{
    $this->authorize('update', $post);
    return view('posts.edit', compact('post'));
}

Best Practices

✅ Do

  • Use a starter kit (Breeze/Jetstream) instead of hand-rolling authentication.
  • Regenerate the session on login and invalidate it on logout.
  • Throttle login attempts (throttle middleware) to blunt brute-force attacks.
  • Enforce authorization server-side with policies; use @can only to tidy the UI.
  • Serve everything over HTTPS in production and let the hashed cast handle passwords.

⚠️ Don't

  • Don't hash passwords manually if you use the hashed cast — you'll double-hash and break login.
  • Don't rely on hiding buttons alone; a hidden route is still reachable without a server check.
  • Don't store API tokens in insecure client storage or leak them in URLs.
  • Don't confuse authentication (identity) with authorization (permission) — you need both.

Summary & Quiz

🎉 Key Takeaways

  • Authentication answers "who are you?"; authorization answers "what may you do?"
  • Starter kits (Breeze, Jetstream, Fortify) scaffold login, registration, reset, and verification.
  • Middleware (auth, verified, guest) protects routes; guards and providers configure how and where users authenticate.
  • Gates and Policies handle fine-grained rules; Sanctum secures APIs with tokens.

🎯 Quick Quiz

Question 1: A logged-in user tries to edit another user's post. Which tool should decide whether that's allowed?

Question 2: Why regenerate the session with $request->session()->regenerate() right after a successful login?

Question 3: You are building a mobile app that talks to your Laravel backend. Which authentication approach fits best?

📚 Further Reading

🚀 What's Next?

That completes the Laravel arc. Next we shift to the world's most widely used PHP application by exploring how it is built, in WordPress Core Architecture.

🎉 Excellent!

You can now authenticate users and authorize their every action with confidence.