Laravel Authentication System

Building secure user authentication and authorization in Laravel applications

Introduction to Authentication

Authentication is the process of verifying the identity of users in your application. It's a fundamental requirement for most web applications, allowing users to securely access their personal data and enabling you to restrict access to certain parts of your application.

Laravel provides a complete, secure authentication system right out of the box, saving you from having to implement these critical security features yourself. The system includes:

Think of authentication as the security checkpoint at an airport or building. It verifies that people are who they claim to be before granting access to restricted areas, just as authentication verifies users before granting them access to protected routes and resources in your application.

sequenceDiagram participant User participant LoginForm participant AuthController participant Session participant Database User->>LoginForm: Enter Credentials LoginForm->>AuthController: Submit Credentials AuthController->>Database: Verify Credentials Database-->>AuthController: Validation Result alt Invalid Credentials AuthController-->>LoginForm: Login Failed LoginForm-->>User: Show Error Message else Valid Credentials AuthController->>Session: Store Authentication AuthController-->>LoginForm: Login Successful LoginForm-->>User: Redirect to Dashboard end

Laravel's Authentication System

Laravel provides several authentication systems with slightly different approaches:

Laravel Breeze

A lightweight implementation of Laravel's authentication features including login, registration, password reset, email verification, and password confirmation. It includes simple Blade templates styled with Tailwind CSS.

Best for: Simple applications or starter projects where you need a minimal, straightforward authentication system that you can modify.

Laravel Jetstream

A more robust starter kit that includes authentication and provides additional features like two-factor authentication, session management, API support with Laravel Sanctum, team management, and more. Jetstream offers the choice of Livewire or Inertia.js for the frontend.

Best for: Applications that need advanced authentication features and modern frontend frameworks.

Laravel Fortify

A headless authentication backend implementation for Laravel. It provides the backend functionality for authentication without any frontend views, allowing you to pair it with any frontend you choose.

Best for: Applications where you want full control over the authentication frontend or are using a separate frontend framework/SPA.

Laravel UI

The original Laravel authentication scaffolding package that provides Bootstrap, Vue, or React views for authentication features. While it's still maintained, newer projects typically use one of the options above.

Best for: Applications that need to use Bootstrap or when working with legacy Laravel codebases.

These authentication systems are like different types of security systems for buildings - from basic key card access to sophisticated biometric systems. They all achieve the same core purpose but offer different levels of features and complexity.

Setting Up Authentication

Let's explore how to set up authentication in a Laravel application using Breeze, which provides a simple implementation that's easy to understand and extend:

Installing Laravel Breeze

// Install Laravel Breeze via Composer
composer require laravel/breeze --dev

// Install Breeze with Blade views
php artisan breeze:install blade

// Install Breeze with React
php artisan breeze:install react

// Install Breeze with Vue
php artisan breeze:install vue

// Install Breeze with API-only endpoints
php artisan breeze:install api

// After installation, migrate the database to create users table
php artisan migrate

// For frontend scaffolding (Blade, React, Vue), compile assets
npm install
npm run dev

When you install Breeze, Laravel creates several components for you:

After installation, you'll have a complete authentication system ready to use, with endpoints for login, registration, password reset, and email verification.

graph TD A[Laravel Breeze Installation] --> B[Controllers] A --> C[Views/Components] A --> D[Routes] A --> E[Middleware] A --> F[Email Verification] B --> G[AuthenticatedSessionController] B --> H[RegisteredUserController] B --> I[PasswordResetController] B --> J[EmailVerificationController] C --> K[Login Form] C --> L[Registration Form] C --> M[Password Reset Forms] C --> N[Email Verification Views] style A fill:#f9d77e

If you want to use the built-in authentication but with your own styling or structure, you can publish the Breeze views and modify them:

// Publish Breeze views for customization
php artisan vendor:publish --tag=laravel-breeze-views

The User Model and Authentication

At the heart of Laravel's authentication system is the User model. By default, this model implements the Authenticatable interface and uses several traits to enable authentication features:

Default User Model

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
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;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];
}

Key components of the User model:

The 'password' attribute is automatically hashed when set, thanks to Laravel's built-in mutator:

// When you do this:
$user = new User;
$user->password = 'plain-text-password';
$user->save();

// Laravel automatically hashes it before saving to the database
// You never need to manually hash passwords

The User model in Laravel's authentication system is like a secure digital identity card. It contains all the necessary information about a user, handles the secure storage of sensitive data like passwords, and provides methods for verifying the user's identity.

Using Authentication in Controllers

Laravel provides several ways to work with authentication in your controllers:

Authentication in Controllers

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class DashboardController extends Controller
{
    /**
     * Create a new controller instance.
     */
    public function __construct()
    {
        // Protect all controller methods with authentication
        $this->middleware('auth');
        
        // Or apply selectively
        // $this->middleware('auth')->only(['sensitive', 'methods']);
        // $this->middleware('auth')->except(['public', 'methods']);
    }
    
    /**
     * Show the dashboard.
     */
    public function index(Request $request)
    {
        // Get the authenticated user
        $user = Auth::user();
        // or
        $user = $request->user();
        // or
        $user = auth()->user();
        
        // Check if user is authenticated
        if (Auth::check()) {
            // User is logged in
        }
        
        // Get user ID
        $userId = Auth::id();
        
        return view('dashboard', [
            'user' => $user
        ]);
    }
    
    /**
     * Manual authentication
     */
    public function authenticate(Request $request)
    {
        $credentials = $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required'],
        ]);
        
        if (Auth::attempt($credentials)) {
            $request->session()->regenerate();
            
            return redirect()->intended('dashboard');
        }
        
        return back()->withErrors([
            'email' => 'The provided credentials do not match our records.',
        ])->onlyInput('email');
    }
    
    /**
     * Logout
     */
    public function logout(Request $request)
    {
        Auth::logout();
        
        $request->session()->invalidate();
        $request->session()->regenerateToken();
        
        return redirect('/');
    }
}

Password Confirmation

For sensitive operations, you might want to reconfirm the user's password:

// In a controller method
if (! Hash::check($request->password, $request->user()->password)) {
    return back()->withErrors([
        'password' => ['The provided password does not match our records.']
    ]);
}

// Or using the built-in middleware
public function __construct()
{
    $this->middleware('password.confirm')->only('updatePaymentMethod');
}

Using authentication in controllers is like having security personnel verify identification before allowing access to restricted areas. The middleware acts as the initial checkpoint, while the Auth facade gives you tools to check credentials and identify users throughout your application.

Authentication Guards & Providers

Laravel's authentication system is built around the concept of "guards" and "providers". Understanding these components helps you customize authentication for complex applications:

graph TD A[Authentication System] --> B[Guards] A --> C[Providers] B --> D[Web Guard] B --> E[API Guard] B --> F[Custom Guards] C --> G[Eloquent Provider] C --> H[Database Provider] C --> I[Custom Providers] style A fill:#f9d77e style B fill:#7ef9a5 style C fill:#7ecbf9

Guards

Guards determine how users are authenticated for each request. They define the method of authentication (session, token, etc.):

// In config/auth.php
'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
    
    'api' => [
        'driver' => 'sanctum', // Or 'token', 'passport'
        'provider' => 'users',
    ],
],

Providers

Providers define how users are retrieved from your persistent storage (database):

// In config/auth.php
'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\Models\User::class,
    ],
    
    // Example of a database provider (without Eloquent)
    'admins' => [
        'driver' => 'database',
        'table' => 'admins',
    ],
],

Using Different Guards

You can specify which guard to use in various authentication operations:

// Specify guard when checking authentication
if (Auth::guard('admin')->check()) {
    // User is authenticated with admin guard
}

// Specify guard in middleware
$this->middleware('auth:admin');

// Manually authenticate with specific guard
if (Auth::guard('admin')->attempt($credentials)) {
    // Authentication successful
}

// Get user from specific guard
$user = Auth::guard('admin')->user();

Multiple Authentication Systems

You can implement multiple authentication systems for different user types:

// Create a separate Admin model
class Admin extends Authenticatable
{
    // Admin-specific configuration
}

// Configure in auth.php
'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
    'admin' => [
        'driver' => 'session',
        'provider' => 'admins',
    ],
],

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

Guards and providers in Laravel authentication are like having different security systems for different parts of a building. Guards are the security checkpoints (using different methods like ID cards, fingerprints, or facial recognition), while providers are the databases of authorized personnel that the guards consult to verify identities.

Protecting Routes

Laravel provides several ways to protect routes, ensuring that only authenticated users can access certain parts of your application:

Route Middleware

// In routes/web.php
// Single protected route
Route::get('/dashboard', function () {
    // Only authenticated users can access this route
})->middleware('auth');

// Group of protected routes
Route::middleware(['auth'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::get('/profile', [ProfileController::class, 'show']);
    Route::put('/profile', [ProfileController::class, 'update']);
});

// With auth guard specified
Route::middleware(['auth:admin'])->group(function () {
    Route::get('/admin/dashboard', [AdminController::class, 'index']);
});

// Combined middleware
Route::middleware(['auth', 'verified'])->group(function () {
    // Only authenticated users with verified emails
});

Controller Middleware

class DashboardController extends Controller
{
    public function __construct()
    {
        // Apply to all methods
        $this->middleware('auth');
        
        // Apply to specific methods
        $this->middleware('verified')->only('sensitiveOperation');
        
        // Exclude from specific methods
        $this->middleware('auth')->except('preview');
    }
}

Middleware Aliases

// In app/Http/Kernel.php
protected $middlewareAliases = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
    'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
    'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];

Laravel's authentication middleware is like having security guards stationed at different entrances to a building. They check if visitors have the proper credentials before allowing them to enter protected areas. The middleware also handles redirecting unauthorized users to the login page automatically.

Email Verification

Laravel includes built-in support for email verification, ensuring that users have access to the email accounts they registered with:

Setting Up Email Verification

// 1. Implement MustVerifyEmail on the User model
namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements MustVerifyEmail
{
    // User model implementation
}

// 2. Use the 'verified' middleware to protect routes
Route::middleware(['auth', 'verified'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
});

// 3. Ensure your application has the verification routes
// Typically included with auth scaffolding
Route::get('/email/verify', function () {
    return view('auth.verify-email');
})->middleware('auth')->name('verification.notice');

Route::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request) {
    $request->fulfill();
    return redirect('/home');
})->middleware(['auth', 'signed'])->name('verification.verify');

Route::post('/email/verification-notification', function (Request $request) {
    $request->user()->sendEmailVerificationNotification();
    return back()->with('message', 'Verification link sent!');
})->middleware(['auth', 'throttle:6,1'])->name('verification.send');

Customizing Verification Emails

// Create a custom notification
php artisan make:notification CustomVerifyEmail

// In the notification class
use Illuminate\Auth\Notifications\VerifyEmail;

class CustomVerifyEmail extends VerifyEmail
{
    protected function buildMailMessage($url)
    {
        return (new MailMessage)
            ->subject('Verify Your Email Address')
            ->line('Please click the button below to verify your email address.')
            ->action('Verify Email Address', $url)
            ->line('If you did not create an account, no further action is required.');
    }
}

// In the User model
protected function sendEmailVerificationNotification()
{
    $this->notify(new \App\Notifications\CustomVerifyEmail);
}

Email verification in Laravel is like having a two-step registration process for a secure facility. First, users register with their credentials, then they must prove they have access to their claimed email address by clicking a secure link sent to that address, confirming their identity before gaining full access.

Password Reset

Laravel includes a secure password reset system that enables users to recover access to their accounts through email:

Password Reset Flow

// The password reset routes are typically included with auth scaffolding
// 1. User requests a password reset link
Route::get('/forgot-password', [PasswordResetLinkController::class, 'create'])
    ->middleware('guest')
    ->name('password.request');
    
Route::post('/forgot-password', [PasswordResetLinkController::class, 'store'])
    ->middleware('guest')
    ->name('password.email');

// 2. User clicks link in email and sets new password
Route::get('/reset-password/{token}', [NewPasswordController::class, 'create'])
    ->middleware('guest')
    ->name('password.reset');
    
Route::post('/reset-password', [NewPasswordController::class, 'store'])
    ->middleware('guest')
    ->name('password.update');

Customizing Password Reset Emails

// Create a custom notification
php artisan make:notification CustomResetPasswordNotification

// In the notification class
use Illuminate\Auth\Notifications\ResetPassword;

class CustomResetPasswordNotification extends ResetPassword
{
    protected function buildMailMessage($url)
    {
        return (new MailMessage)
            ->subject('Reset Your Password')
            ->line('You are receiving this email because we received a password reset request for your account.')
            ->action('Reset Password', $url)
            ->line('This password reset link will expire in ' . config('auth.passwords.users.expire') . ' minutes.')
            ->line('If you did not request a password reset, no further action is required.');
    }
}

// In the User model
protected function sendPasswordResetNotification($token)
{
    $this->notify(new \App\Notifications\CustomResetPasswordNotification($token));
}

The password reset system in Laravel is like having a secure identity recovery process. When users forget their "key" (password), the system verifies their identity through another trusted channel (email) before allowing them to create a new key.

Authorization with Gates and Policies

While authentication verifies who users are, authorization determines what they can do. Laravel provides a powerful system for authorization through Gates and Policies:

graph TD A[Authorization System] --> B[Gates] A --> C[Policies] B --> D[Define in AuthServiceProvider] B --> E[Simple Access Controls] C --> F[One Class Per Model] C --> G[CRUD Permission Methods] style A fill:#f9d77e style B fill:#7ef9a5 style C fill:#7ecbf9

Gates

Gates are simple closures that determine if a user can perform a given action:

// In app/Providers/AuthServiceProvider.php
public function boot()
{
    // Define gates
    Gate::define('edit-post', function (User $user, Post $post) {
        return $user->id === $post->user_id || $user->isAdmin();
    });
    
    Gate::define('publish-post', function (User $user) {
        return $user->isEditor() || $user->isAdmin();
    });
    
    // Define a gate using a class and method
    Gate::define('moderate-comments', [CommentPolicy::class, 'moderate']);
}

// Using gates in controllers
public function edit(Post $post)
{
    if (Gate::allows('edit-post', $post)) {
        // User can edit the post
    }
    
    if (Gate::denies('edit-post', $post)) {
        // User cannot edit the post
    }
    
    // Authorize or abort with 403
    Gate::authorize('edit-post', $post);
}

// Using gates in blade templates
@can('edit-post', $post)
    <a href="{{ route('posts.edit', $post) }}">Edit Post</a>
@endcan

@cannot('publish-post')
    <p>Only editors can publish posts.</p>
@endcannot

Policies

Policies organize authorization logic around a model or resource:

Creating and Registering Policies

// Generate a policy
php artisan make:policy PostPolicy --model=Post

// Register in AuthServiceProvider
protected $policies = [
    Post::class => PostPolicy::class,
];

// The policy will be auto-discovered by convention
// Post model -> PostPolicy

Policy Implementation

namespace App\Policies;

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

class PostPolicy
{
    /**
     * Determine if the given post can be viewed by the user.
     */
    public function view(User $user, Post $post)
    {
        // All users can view public posts
        if (!$post->is_private) {
            return true;
        }
        
        // Users can view their own private posts
        return $user->id === $post->user_id;
    }
    
    /**
     * Determine if the user can create posts.
     */
    public function create(User $user)
    {
        // All authenticated users can create posts
        return true;
    }
    
    /**
     * Determine if the user can update the post.
     */
    public function update(User $user, Post $post)
    {
        // Users can update their own posts
        return $user->id === $post->user_id;
    }
    
    /**
     * Determine if the user can delete the post.
     */
    public function delete(User $user, Post $post)
    {
        // Admins can delete any post
        if ($user->isAdmin()) {
            return true;
        }
        
        // Users can delete their own posts
        return $user->id === $post->user_id;
    }
    
    /**
     * Policy for publishing posts
     */
    public function publish(User $user, Post $post)
    {
        // Only editors and admins can publish
        if (!($user->isEditor() || $user->isAdmin())) {
            return false;
        }
        
        // Only published posts in your own department
        return $user->department_id === $post->department_id;
    }
    
    /**
     * Policy filter - run before all other checks
     */
    public function before(User $user, $ability)
    {
        // Superadmins can do anything
        if ($user->isSuperAdmin()) {
            return true;
        }
        
        // Null means continue to the specific policy method
        return null;
    }
}

Using Policies

// In controllers
public function update(Request $request, Post $post)
{
    // Automatically uses the registered policy
    $this->authorize('update', $post);
    
    // Update logic...
}

public function store(Request $request)
{
    // For methods that don't require a model instance
    $this->authorize('create', Post::class);
    
    // Create logic...
}

// Using policy with user
if ($user->can('update', $post)) {
    // Can update
}

if ($user->cannot('delete', $post)) {
    // Cannot delete
}

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

@cannot('publish', $post)
    <p>You don't have permission to publish.</p>
@endcannot

Gates and policies in Laravel are like having a rule book for what different users can do within your application. Gates are simple, specific rules ("only managers can approve expense reports"), while policies are comprehensive rule collections for specific types of resources ("here are all the rules about who can view, edit, and delete documents").

Role-Based Authorization

While Laravel doesn't include a role-based authorization system out of the box, you can easily implement one:

Simple Role Implementation

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

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

public function isEditor()
{
    return $this->role === 'editor';
}

// In AuthServiceProvider
Gate::define('manage-users', function (User $user) {
    return $user->isAdmin();
});

Many-to-Many Relationship for Roles

// Create roles and permissions tables
Schema::create('roles', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('label')->nullable();
    $table->timestamps();
});

Schema::create('permissions', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('label')->nullable();
    $table->timestamps();
});

Schema::create('permission_role', function (Blueprint $table) {
    $table->primary(['permission_id', 'role_id']);
    $table->foreignId('permission_id')->constrained()->onDelete('cascade');
    $table->foreignId('role_id')->constrained()->onDelete('cascade');
});

Schema::create('role_user', function (Blueprint $table) {
    $table->primary(['role_id', 'user_id']);
    $table->foreignId('role_id')->constrained()->onDelete('cascade');
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
});

// In User model
public function roles()
{
    return $this->belongsToMany(Role::class);
}

public function hasRole($role)
{
    if (is_string($role)) {
        return $this->roles->contains('name', $role);
    }
    
    // Check if user has any of the given roles
    if (is_array($role)) {
        foreach ($role as $r) {
            if ($this->hasRole($r)) {
                return true;
            }
        }
        return false;
    }
    
    // If $role is a Role model or collection
    return $role->intersect($this->roles)->isNotEmpty();
}

public function hasPermission($permission)
{
    return $this->roles->flatMap(function ($role) {
        return $role->permissions;
    })->contains('name', $permission);
}

Using Roles for Authorization

// In AuthServiceProvider
Gate::define('edit-posts', function (User $user) {
    return $user->hasRole(['editor', 'admin']);
});

Gate::define('approve-comments', function (User $user) {
    return $user->hasPermission('moderate-comments');
});

// Custom middleware for roles
class CheckRole
{
    public function handle($request, $next, $role)
    {
        if (!$request->user() || !$request->user()->hasRole($role)) {
            abort(403, 'Unauthorized action.');
        }
        
        return $next($request);
    }
}

// Register in Kernel.php
protected $middlewareAliases = [
    // ...
    'role' => \App\Http\Middleware\CheckRole::class,
];

// Use in routes
Route::middleware(['auth', 'role:admin'])->group(function () {
    Route::get('/admin/dashboard', [AdminController::class, 'index']);
});

For more complex role-based authorization, you might want to use a package like Spatie's Laravel Permission, which provides a complete, well-tested implementation:

Using Spatie's Laravel Permission

// Install via Composer
composer require spatie/laravel-permission

// Publish and run migrations
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate

// In User model
use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasRoles;
    // ...
}

// Usage
// Assign roles and permissions
$user->assignRole('writer');
$user->givePermissionTo('edit articles');

// Check roles and permissions
$user->hasRole('writer');
$user->hasPermissionTo('edit articles');
$user->hasAnyRole(['writer', 'admin']);

// In middleware
Route::middleware(['role:admin|writer'])->group(function () {
    // Routes for admins and writers
});

Route::middleware(['permission:publish articles'])->group(function () {
    // Routes for users who can publish articles
});

Role-based authorization in Laravel is like having a hierarchical security system with different access levels. Users have roles (security clearance levels), and each role grants certain permissions (access to specific areas or operations). This approach simplifies authorization by grouping permissions into logical roles that align with user responsibilities.

API Authentication

Laravel offers several options for API authentication. Here we'll look at Laravel Sanctum, which provides a lightweight solution for API tokens, SPA authentication, and mobile application authentication:

Setting Up Sanctum

// Install Sanctum
composer require laravel/sanctum

// Publish configuration
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

// Run migrations
php artisan migrate

// In User model (typically already included)
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
    // ...
}

Issuing API Tokens

// In a login controller
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.'],
        ]);
    }

    // Create token with abilities (scopes)
    $token = $user->createToken($request->device_name, ['post:create', 'post:read'])->plainTextToken;

    return response()->json(['token' => $token]);
}

// In a logout controller
public function logout(Request $request)
{
    // Revoke the token that was used to authenticate the current request
    $request->user()->currentAccessToken()->delete();
    
    // Or revoke all tokens
    // $request->user()->tokens()->delete();

    return response()->json(['message' => 'Logged out']);
}

Protecting API Routes

// In routes/api.php
// All routes in this group require authentication
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', function (Request $request) {
        return $request->user();
    });
    
    Route::get('/posts', [PostController::class, 'index']);
    Route::post('/posts', [PostController::class, 'store']);
});

// Check for specific token abilities (scopes)
Route::middleware(['auth:sanctum', 'ability:post:create'])->post('/posts', [PostController::class, 'store']);

// Multiple abilities (any one required)
Route::middleware(['auth:sanctum', 'ability:post:create,post:update'])->put('/posts/{id}', [PostController::class, 'update']);

// Multiple abilities (all required)
Route::middleware(['auth:sanctum', 'abilities:post:create,post:update'])->put('/posts/{id}', [PostController::class, 'update']);

Consuming API with Token

// Example HTTP request
axios.get('/api/user', {
    headers: {
        'Authorization': `Bearer ${token}`,
        'Accept': 'application/json'
    }
});

// Using fetch
fetch('/api/user', {
    headers: {
        'Authorization': `Bearer ${token}`,
        'Accept': 'application/json'
    }
}).then(response => response.json());

Laravel Sanctum also supports authentication for SPAs (Single Page Applications) using cookies:

SPA Authentication with Sanctum

// In config/sanctum.php, configure the stateful domains
'stateful' => [
    'localhost',
    'localhost:3000',
    'example.com',
    '*.example.com',
],

// In routes/api.php
// Route for logging in
Route::post('/login', [AuthController::class, 'login']);

// CSRF Protection is required for cookie auth
// In JavaScript, you need to include the CSRF token
axios.defaults.withCredentials = true;

API authentication with Laravel Sanctum is like having a key card system that issues temporary access cards. Each token is like a personalized key card with specific access permissions, and the system verifies these cards for each API request, ensuring that only authorized users can access protected endpoints.

Practical Example: Complete Authentication System

Let's put these concepts together with a practical example of a complete authentication system for a blog application:

Models and Migrations

// User model with roles and permissions
class User extends Authenticatable implements MustVerifyEmail
{
    use HasApiTokens, HasFactory, Notifiable;

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

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

    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];
    
    public function roles()
    {
        return $this->belongsToMany(Role::class);
    }
    
    public function hasRole($role)
    {
        return $this->roles->contains('name', $role);
    }
    
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
    
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

// Post model with policies
class Post extends Model
{
    use HasFactory;
    
    protected $fillable = [
        'title', 'content', 'user_id', 'is_published'
    ];
    
    public function user()
    {
        return $this->belongsTo(User::class);
    }
    
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

Policies

// Post Policy
class PostPolicy
{
    /**
     * Allow admins to do anything.
     */
    public function before(User $user, $ability)
    {
        if ($user->hasRole('admin')) {
            return true;
        }
    }
    
    /**
     * View any post.
     */
    public function viewAny(User $user)
    {
        return true; // Anyone can view posts
    }
    
    /**
     * View a specific post.
     */
    public function view(User $user, Post $post)
    {
        // Anyone can view published posts
        if ($post->is_published) {
            return true;
        }
        
        // Authors can view their own unpublished posts
        return $user->id === $post->user_id;
    }
    
    /**
     * Create posts.
     */
    public function create(User $user)
    {
        return $user->hasRole(['author', 'editor']);
    }
    
    /**
     * Update a post.
     */
    public function update(User $user, Post $post)
    {
        // Editors can edit any post
        if ($user->hasRole('editor')) {
            return true;
        }
        
        // Authors can edit their own posts
        return $user->id === $post->user_id;
    }
    
    /**
     * Delete a post.
     */
    public function delete(User $user, Post $post)
    {
        // Only editors and the post owner can delete posts
        return $user->hasRole('editor') || $user->id === $post->user_id;
    }
    
    /**
     * Publish a post.
     */
    public function publish(User $user, Post $post)
    {
        // Only editors can publish posts
        return $user->hasRole('editor');
    }
}

Controllers

// PostController with authorization
class PostController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth')->except(['index', 'show']);
        $this->middleware('verified')->except(['index', 'show']);
    }
    
    /**
     * Display a listing of posts.
     */
    public function index()
    {
        $posts = Post::where('is_published', true)
                     ->latest()
                     ->paginate(10);
                     
        return view('posts.index', compact('posts'));
    }
    
    /**
     * Show a specific post.
     */
    public function show(Post $post)
    {
        $this->authorize('view', $post);
        
        return view('posts.show', compact('post'));
    }
    
    /**
     * Show the form for creating a post.
     */
    public function create()
    {
        $this->authorize('create', Post::class);
        
        return view('posts.create');
    }
    
    /**
     * Store a newly created post.
     */
    public function store(StorePostRequest $request)
    {
        $this->authorize('create', Post::class);
        
        // All validation happens in the form request
        $post = new Post($request->validated());
        $post->user_id = auth()->id();
        $post->save();
        
        return redirect()->route('posts.show', $post)
                         ->with('success', 'Post created successfully!');
    }
    
    /**
     * Show the form for editing a post.
     */
    public function edit(Post $post)
    {
        $this->authorize('update', $post);
        
        return view('posts.edit', compact('post'));
    }
    
    /**
     * Update the post.
     */
    public function update(UpdatePostRequest $request, Post $post)
    {
        $this->authorize('update', $post);
        
        $post->update($request->validated());
        
        return redirect()->route('posts.show', $post)
                         ->with('success', 'Post updated successfully!');
    }
    
    /**
     * Delete the post.
     */
    public function destroy(Post $post)
    {
        $this->authorize('delete', $post);
        
        $post->delete();
        
        return redirect()->route('posts.index')
                         ->with('success', 'Post deleted successfully!');
    }
    
    /**
     * Publish a post.
     */
    public function publish(Post $post)
    {
        $this->authorize('publish', $post);
        
        $post->is_published = true;
        $post->published_at = now();
        $post->save();
        
        return redirect()->route('posts.show', $post)
                         ->with('success', 'Post published successfully!');
    }
}

Form Requests

// StorePostRequest with validation
class StorePostRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize()
    {
        return $this->user()->can('create', Post::class);
    }
    
    /**
     * Get the validation rules for the request.
     */
    public function rules()
    {
        return [
            'title' => 'required|string|max:255',
            'content' => 'required|string|min:50',
            'category_id' => 'required|exists:categories,id',
            'tags' => 'nullable|array',
            'tags.*' => 'exists:tags,id',
        ];
    }
}

Routes

// Web routes with authentication and authorization
// Public routes
Route::get('/', [HomeController::class, 'index'])->name('home');
Route::get('/posts', [PostController::class, 'index'])->name('posts.index');
Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show');

// Authentication routes (provided by Breeze)
Route::middleware('guest')->group(function () {
    Route::get('/login', [AuthenticatedSessionController::class, 'create'])->name('login');
    Route::post('/login', [AuthenticatedSessionController::class, 'store']);
    
    Route::get('/register', [RegisteredUserController::class, 'create'])->name('register');
    Route::post('/register', [RegisteredUserController::class, 'store']);
    
    Route::get('/forgot-password', [PasswordResetLinkController::class, 'create'])->name('password.request');
    Route::post('/forgot-password', [PasswordResetLinkController::class, 'store'])->name('password.email');
    
    Route::get('/reset-password/{token}', [NewPasswordController::class, 'create'])->name('password.reset');
    Route::post('/reset-password', [NewPasswordController::class, 'store'])->name('password.update');
});

// Authenticated routes
Route::middleware('auth')->group(function () {
    Route::post('/logout', [AuthenticatedSessionController::class, 'destroy'])->name('logout');
    
    Route::get('/email/verify', [EmailVerificationPromptController::class, '__invoke'])->name('verification.notice');
    Route::get('/email/verify/{id}/{hash}', [VerifyEmailController::class, '__invoke'])->middleware(['signed', 'throttle:6,1'])->name('verification.verify');
    Route::post('/email/verification-notification', [EmailVerificationNotificationController::class, 'store'])->middleware('throttle:6,1')->name('verification.send');
    
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::put('/profile', [ProfileController::class, 'update'])->name('profile.update');

    // Post management routes
    Route::get('/posts/create', [PostController::class, 'create'])->name('posts.create');
    Route::post('/posts', [PostController::class, 'store'])->name('posts.store');
    Route::get('/posts/{post}/edit', [PostController::class, 'edit'])->name('posts.edit');
    Route::put('/posts/{post}', [PostController::class, 'update'])->name('posts.update');
    Route::delete('/posts/{post}', [PostController::class, 'destroy'])->name('posts.destroy');
    
    // Publish action (editor only)
    Route::put('/posts/{post}/publish', [PostController::class, 'publish'])->name('posts.publish');
});

// Admin routes
Route::middleware(['auth', 'role:admin'])->prefix('admin')->name('admin.')->group(function () {
    Route::get('/dashboard', [AdminController::class, 'index'])->name('dashboard');
    Route::resource('users', AdminUserController::class);
    Route::resource('roles', AdminRoleController::class);
});

Blade Templates

// post/show.blade.php with authorization
@extends('layouts.app')

@section('content')
    <div class="container">
        @if(session('success'))
            <div class="alert alert-success">
                {{ session('success') }}
            </div>
        @endif
        
        <div class="post">
            <h1>{{ $post->title }}</h1>
            
            <div class="post-meta">
                <span>By {{ $post->user->name }} | {{ $post->created_at->format('F j, Y') }}</span>
                
                @unless($post->is_published)
                    <span class="badge bg-warning">Unpublished</span>
                @endunless
            </div>
            
            <div class="post-actions">
                @can('update', $post)
                    <a href="{{ route('posts.edit', $post) }}" class="btn btn-sm btn-primary">Edit</a>
                @endcan
                
                @can('delete', $post)
                    <form action="{{ route('posts.destroy', $post) }}" method="POST" class="d-inline">
                        @csrf
                        @method('DELETE')
                        <button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure?')">Delete</button>
                    </form>
                @endcan
                
                @if(!$post->is_published)
                    @can('publish', $post)
                        <form action="{{ route('posts.publish', $post) }}" method="POST" class="d-inline">
                            @csrf
                            @method('PUT')
                            <button type="submit" class="btn btn-sm btn-success">Publish</button>
                        </form>
                    @endcan
                @endif
            </div>
            
            <div class="post-content">
                {{ $post->content }}
            </div>
            
            <div class="comments-section">
                <h3>Comments</h3>
                
                @auth
                    <form action="{{ route('comments.store') }}" method="POST">
                        @csrf
                        <input type="hidden" name="post_id" value="{{ $post->id }}">
                        
                        <div class="form-group">
                            <textarea name="content" class="form-control @error('content') is-invalid @enderror" rows="3" placeholder="Leave a comment">{{ old('content') }}</textarea>
                            @error('content')
                                <div class="invalid-feedback">{{ $message }}</div>
                            @enderror
                        </div>
                        
                        <button type="submit" class="btn btn-primary">Submit Comment</button>
                    </form>
                @else
                    <p><a href="{{ route('login') }}">Log in</a> to leave a comment.</p>
                @endauth
                
                <div class="comments-list">
                    @foreach($post->comments as $comment)
                        <div class="comment">
                            <div class="comment-meta">
                                <strong>{{ $comment->user->name }}</strong> • {{ $comment->created_at->diffForHumans() }}
                            </div>
                            
                            <div class="comment-content">
                                {{ $comment->content }}
                            </div>
                            
                            @can('delete', $comment)
                                <form action="{{ route('comments.destroy', $comment) }}" method="POST" class="d-inline">
                                    @csrf
                                    @method('DELETE')
                                    <button type="submit" class="btn btn-sm btn-text text-danger">Delete</button>
                                </form>
                            @endcan
                        </div>
                    @endforeach
                </div>
            </div>
        </div>
    </div>
@endsection

Navigation with Authorization

// nav.blade.php with authorization
<nav class="navbar navbar-expand-lg navbar-light bg-light">
    <div class="container">
        <a class="navbar-brand" href="{{ route('home') }}">BlogApp</a>
        
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
            <span class="navbar-toggler-icon"></span>
        </button>
        
        <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav me-auto">
                <li class="nav-item">
                    <a class="nav-link" href="{{ route('home') }}">Home</a>
                </li>
                
                <li class="nav-item">
                    <a class="nav-link" href="{{ route('posts.index') }}">Posts</a>
                </li>
                
                @can('create', App\Models\Post::class)
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('posts.create') }}">Create Post</a>
                    </li>
                @endcan
                
                @if(auth()->user() && auth()->user()->hasRole('admin'))
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('admin.dashboard') }}">Admin Dashboard</a>
                    </li>
                @endif
            </ul>
            
            <ul class="navbar-nav">
                @guest
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('login') }}">Login</a>
                    </li>
                    
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('register') }}">Register</a>
                    </li>
                @else
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
                            {{ auth()->user()->name }}
                        </a>
                        
                        <ul class="dropdown-menu dropdown-menu-end">
                            <li>
                                <a class="dropdown-item" href="{{ route('profile.edit') }}">Profile</a>
                            </li>
                            
                            <li>
                                <hr class="dropdown-divider">
                            </li>
                            
                            <li>
                                <form action="{{ route('logout') }}" method="POST">
                                    @csrf
                                    <button type="submit" class="dropdown-item">Logout</button>
                                </form>
                            </li>
                        </ul>
                    </li>
                @endguest
            </ul>
        </div>
    </div>
</nav>

This practical example demonstrates a complete authentication and authorization system for a blog application, integrating user authentication, role-based permissions, policies for fine-grained access control, and secure form handling.

Practical Activity: User Roles and Permissions

Let's solidify your understanding with a hands-on activity:

Activity: Implement a Role and Permission System

  1. Set up the database structure:
    • Create migration for roles, permissions, and pivot tables
    • Add role and permission models with relationships
    • Add relationship methods to the User model
  2. Implement authorization logic:
    • Create middleware for role and permission checks
    • Define gates for common permissions in AuthServiceProvider
    • Create policies for model-specific authorization
  3. Create an admin interface:
    • Build CRUD controllers for roles and permissions
    • Create forms for assigning roles to users
    • Implement views with proper authorization checks
  4. Add a dashboard with role-specific content:
    • Create a dashboard controller and view
    • Display different content based on user roles
    • Implement navigation that shows only accessible items

Extension: Add a permission system that allows dynamic assignment of permissions to roles and direct assignment of permissions to users.

Best Practices for Authentication

Follow these best practices to ensure your authentication system is secure, user-friendly, and maintainable:

Security

  • Always use HTTPS in production environments
  • Implement throttling for login attempts to prevent brute force attacks
  • Use secure password reset mechanisms
  • Regularly audit and rotate API tokens
  • Implement two-factor authentication for sensitive operations
  • Always regenerate session IDs on login and state changes

User Experience

  • Provide clear error messages for login failures
  • Implement "remember me" functionality
  • Redirect users to their intended destination after login
  • Make password requirements clear during registration
  • Provide account recovery options besides password reset
  • Consider social login options for convenience

Authorization Design

  • Use policies for model-specific authorization logic
  • Implement role-based access control for group permissions
  • Design permissions to be granular but not overwhelming
  • Regularly audit authorization rules
  • Default to denying access unless explicitly granted
  • Document your authorization system for other developers

Maintenance

  • Keep authentication packages updated
  • Write tests for authentication and authorization logic
  • Use dependency injection for authorization services
  • Regularly review and prune inactive user accounts
  • Log important authentication events

Following these best practices ensures that your authentication and authorization system is not only secure but also provides a positive user experience and remains maintainable as your application grows and evolves.

Two-Factor Authentication

Two-factor authentication (2FA) adds an extra layer of security by requiring a second form of identification beyond just a password. Laravel Fortify and Jetstream include built-in support for two-factor authentication:

Enabling Two-Factor Authentication with Jetstream

// In config/jetstream.php
'features' => [
    // ...
    Features::twoFactorAuthentication([
        'confirmPassword' => true,
    ]),
],

Implementing Custom Two-Factor Authentication

// Migration for 2FA fields
Schema::table('users', function (Blueprint $table) {
    $table->text('two_factor_secret')
          ->after('password')
          ->nullable();
          
    $table->text('two_factor_recovery_codes')
          ->after('two_factor_secret')
          ->nullable();
          
    $table->timestamp('two_factor_confirmed_at')
          ->after('two_factor_recovery_codes')
          ->nullable();
});

// Using a package like pragmarx/google2fa for TOTP generation
composer require pragmarx/google2fa

// In controller for enabling 2FA
public function enableTwoFactor(Request $request)
{
    $user = $request->user();
    
    // Generate a secret
    $google2fa = app('pragmarx.google2fa');
    $secret = $google2fa->generateSecretKey();
    
    // Save to user record
    $user->two_factor_secret = encrypt($secret);
    $user->save();
    
    // Generate QR code URL
    $qrCodeUrl = $google2fa->getQRCodeUrl(
        config('app.name'),
        $user->email,
        $secret
    );
    
    return view('auth.two-factor.enable', [
        'qrCodeUrl' => $qrCodeUrl,
        'secret' => $secret,
    ]);
}

// Confirming 2FA setup with user-provided code
public function confirmTwoFactor(Request $request)
{
    $request->validate([
        'code' => 'required|string|size:6',
    ]);
    
    $user = $request->user();
    $google2fa = app('pragmarx.google2fa');
    $secret = decrypt($user->two_factor_secret);
    
    if ($google2fa->verifyKey($secret, $request->code)) {
        $user->two_factor_confirmed_at = now();
        
        // Generate recovery codes
        $user->two_factor_recovery_codes = encrypt(json_encode(
            Collection::times(8, function () {
                return Str::random(10).'-'.Str::random(10);
            })->all()
        ));
        
        $user->save();
        
        return redirect()->route('profile.show')
                         ->with('status', 'Two factor authentication enabled.');
    }
    
    return back()->withErrors([
        'code' => 'The provided code was invalid.',
    ]);
}

// In login controller, add 2FA challenge after password authentication
protected function authenticated(Request $request, $user)
{
    if ($user->two_factor_confirmed_at) {
        // Logout but remember the user ID
        auth()->logout();
        
        // Store user ID in session for 2FA challenge
        $request->session()->put('auth.2fa.user_id', $user->id);
        
        return redirect()->route('two-factor.challenge');
    }
    
    return redirect()->intended($this->redirectPath());
}

Two-factor authentication is like having both a key and a security code for a safety deposit box. Even if someone steals your key (password), they still can't access the contents without the constantly changing security code from your authenticator app.

Social Authentication

Social authentication allows users to sign in using their existing accounts with services like Google, Facebook, GitHub, etc. Laravel Socialite provides a simple interface for social authentication:

Setting Up Socialite

// Install Laravel Socialite
composer require laravel/socialite

// In config/services.php, add OAuth credentials
'github' => [
    'client_id' => env('GITHUB_CLIENT_ID'),
    'client_secret' => env('GITHUB_CLIENT_SECRET'),
    'redirect' => 'http://example.com/login/github/callback',
],

'google' => [
    'client_id' => env('GOOGLE_CLIENT_ID'),
    'client_secret' => env('GOOGLE_CLIENT_SECRET'),
    'redirect' => 'http://example.com/login/google/callback',
],

Implementing Social Login

// In routes/web.php
Route::get('/login/{provider}', [SocialLoginController::class, 'redirect'])->name('social.redirect');
Route::get('/login/{provider}/callback', [SocialLoginController::class, 'callback'])->name('social.callback');

// SocialLoginController
class SocialLoginController extends Controller
{
    /**
     * Redirect to provider for authentication.
     */
    public function redirect($provider)
    {
        // Validate provider
        if (!in_array($provider, ['github', 'google', 'facebook'])) {
            return redirect()->route('login');
        }
        
        return Socialite::driver($provider)->redirect();
    }

    /**
     * Handle provider callback.
     */
    public function callback($provider)
    {
        try {
            $socialUser = Socialite::driver($provider)->user();
        } catch (\Exception $e) {
            return redirect()->route('login')->withErrors([
                'email' => 'Social login failed. Please try again.',
            ]);
        }
        
        // Find or create user
        $user = User::where('email', $socialUser->getEmail())->first();
        
        if (!$user) {
            // Create new user
            $user = User::create([
                'name' => $socialUser->getName(),
                'email' => $socialUser->getEmail(),
                'password' => Hash::make(Str::random(24)), // Random password
                'email_verified_at' => now(), // Social logins are pre-verified
            ]);
        }
        
        // Check if this social login is linked to user
        $socialAccount = SocialAccount::where('provider', $provider)
                                      ->where('provider_id', $socialUser->getId())
                                      ->first();
                                      
        if (!$socialAccount) {
            // Link account
            SocialAccount::create([
                'user_id' => $user->id,
                'provider' => $provider,
                'provider_id' => $socialUser->getId(),
            ]);
        }
        
        // Login user
        auth()->login($user, true);
        
        return redirect()->intended('/dashboard');
    }
}

Social Account Model

// Create a model and migration for social accounts
php artisan make:model SocialAccount -m

// Migration
Schema::create('social_accounts', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->string('provider');
    $table->string('provider_id');
    $table->timestamps();
    
    $table->unique(['provider', 'provider_id']);
});

// SocialAccount model
class SocialAccount extends Model
{
    protected $fillable = [
        'user_id',
        'provider',
        'provider_id',
    ];
    
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Adding Social Login Buttons to Login Form

<form method="POST" action="{{ route('login') }}">
    @csrf
    
    <!-- Regular login form fields -->
    
    <div class="social-auth-links text-center mt-4">
        <p>- OR -</p>
        
        <a href="{{ route('social.redirect', 'github') }}" class="btn btn-block btn-github">
            <i class="fab fa-github"></i> Sign in with GitHub
        </a>
        
        <a href="{{ route('social.redirect', 'google') }}" class="btn btn-block btn-google">
            <i class="fab fa-google"></i> Sign in with Google
        </a>
    </div>
</form>

Social authentication is like using a trusted third party to vouch for your identity. Instead of creating and remembering another username and password, users can leverage an existing trusted relationship with a major service provider to quickly access your application.

Testing Authentication

Testing your authentication system ensures it works correctly and remains secure as your application evolves. Laravel's testing features make it easy to test authentication functionality:

Feature Tests for Authentication

// Generate a test
php artisan make:test AuthenticationTest

// Basic authentication test
namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class AuthenticationTest extends TestCase
{
    use RefreshDatabase;

    public function test_login_screen_can_be_rendered()
    {
        $response = $this->get('/login');

        $response->assertStatus(200);
    }

    public function test_users_can_authenticate_using_the_login_screen()
    {
        $user = User::factory()->create();

        $response = $this->post('/login', [
            'email' => $user->email,
            'password' => 'password',
        ]);

        $this->assertAuthenticated();
        $response->assertRedirect('/dashboard');
    }

    public function test_users_can_not_authenticate_with_invalid_password()
    {
        $user = User::factory()->create();

        $this->post('/login', [
            'email' => $user->email,
            'password' => 'wrong-password',
        ]);

        $this->assertGuest();
    }
    
    public function test_users_can_logout()
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->post('/logout');

        $this->assertGuest();
        $response->assertRedirect('/');
    }
}

Authorization Tests

// Policy test
class PostPolicyTest extends TestCase
{
    use RefreshDatabase;
    
    protected $admin;
    protected $editor;
    protected $author;
    protected $user;
    
    public function setUp(): void
    {
        parent::setUp();
        
        // Create users with different roles
        $this->admin = User::factory()->create();
        $this->admin->roles()->attach(Role::where('name', 'admin')->first());
        
        $this->editor = User::factory()->create();
        $this->editor->roles()->attach(Role::where('name', 'editor')->first());
        
        $this->author = User::factory()->create();
        $this->author->roles()->attach(Role::where('name', 'author')->first());
        
        $this->user = User::factory()->create();
    }
    
    public function test_admin_can_edit_any_post()
    {
        $post = Post::factory()->create([
            'user_id' => $this->author->id
        ]);
        
        $this->assertTrue($this->admin->can('update', $post));
    }
    
    public function test_editor_can_edit_any_post()
    {
        $post = Post::factory()->create([
            'user_id' => $this->author->id
        ]);
        
        $this->assertTrue($this->editor->can('update', $post));
    }
    
    public function test_author_can_edit_own_post()
    {
        $post = Post::factory()->create([
            'user_id' => $this->author->id
        ]);
        
        $this->assertTrue($this->author->can('update', $post));
    }
    
    public function test_author_cannot_edit_others_post()
    {
        $post = Post::factory()->create([
            'user_id' => $this->editor->id
        ]);
        
        $this->assertFalse($this->author->can('update', $post));
    }
    
    public function test_regular_user_cannot_edit_any_post()
    {
        $post = Post::factory()->create();
        
        $this->assertFalse($this->user->can('update', $post));
    }
    
    public function test_only_editor_can_publish_post()
    {
        $post = Post::factory()->create([
            'user_id' => $this->author->id,
            'is_published' => false
        ]);
        
        $this->assertTrue($this->editor->can('publish', $post));
        $this->assertFalse($this->author->can('publish', $post));
        $this->assertFalse($this->user->can('publish', $post));
    }
}

Test Protected Routes

class RouteAuthorizationTest extends TestCase
{
    use RefreshDatabase;
    
    public function test_dashboard_requires_authentication()
    {
        $response = $this->get('/dashboard');
        
        $response->assertRedirect('/login');
    }
    
    public function test_authenticated_user_can_access_dashboard()
    {
        $user = User::factory()->create();
        
        $response = $this->actingAs($user)->get('/dashboard');
        
        $response->assertStatus(200);
    }
    
    public function test_admin_pages_require_admin_role()
    {
        $user = User::factory()->create();
        
        $response = $this->actingAs($user)->get('/admin/dashboard');
        
        $response->assertStatus(403);
        
        // Create admin user
        $admin = User::factory()->create();
        $admin->roles()->attach(Role::where('name', 'admin')->first());
        
        $response = $this->actingAs($admin)->get('/admin/dashboard');
        
        $response->assertStatus(200);
    }
}

Testing authentication is like regularly checking the security systems in a building. By systematically testing each component, you ensure that the entire system works correctly, identifies authorized users, keeps out unauthorized ones, and enforces access rules appropriately.

Advanced Authentication Scenarios

Beyond basic authentication, Laravel supports several advanced authentication scenarios:

HTTP Basic Authentication

Simple authentication without session cookies:

Route::get('/api/user-info', function () {
    // Only for authenticated users with HTTP Basic Auth
})->middleware('auth.basic');

Single Session Authentication

Ensure users have only one active session:

Route::middleware(['auth', 'auth.session'])->group(function () {
    // Routes that enforce single session
});

Stateless API Authentication

Authentication for APIs without sessions:

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

Multi-Tenant Authentication

Authentication specific to application tenants:

// In a middleware
public function handle($request, $next)
{
    // Determine tenant from subdomain
    $tenant = Tenant::where('domain', $request->getHost())->first();
    
    if (!$tenant) {
        abort(404);
    }
    
    // Set tenant in the application
    app()->instance('tenant', $tenant);
    
    // Switch to tenant's database
    config(['database.connections.tenant.database' => $tenant->database]);
    DB::purge('tenant');
    
    // Continue request
    return $next($request);
}

Advanced authentication scenarios address specific security and architectural requirements for different types of applications, from APIs to multi-tenant systems.

Summary and Key Takeaways

With Laravel's authentication and authorization features, you can build secure, user-friendly applications that properly protect sensitive operations and data.

Further Resources