Skip to content

Meeting 15 - Authentication & Authorization

Goal: Understand how Laravel authenticates users (who you are) and authorizes actions (what you can do) using Guards, Providers, Middleware, Gates, Policies, Roles & Permissions, Tokens, and best practices.

1. Concepts

TermMeaning
AuthenticationValidating identity (login)
AuthorizationDetermining access to a resource/action
GuardHow a user is authenticated on each request (session, token)
ProviderHow user data is retrieved (Eloquent, Database)
MiddlewarePipeline layer that blocks/redirects unauthorized requests
GateClosure-based, single ability check
PolicyClass grouping authorization methods per model
AbilityNamed permission (e.g., update-post)
Role (custom)Group of abilities assigned to users

Diagram: Auth Flow

2. Configuration Overview

config/auth.php excerpt:

php
'guards' => [
  'web' => [
    'driver' => 'session',
    'provider' => 'users',
  ],
  'api' => [
    'driver' => 'sanctum', // or 'token' / 'passport'
    'provider' => 'users',
  ],
],

'providers' => [
  'users' => [
    'driver' => 'eloquent',
    'model' => App\Models\User::class,
  ],
],
'guards' => [
  'web' => [
    'driver' => 'session',
    'provider' => 'users',
  ],
  'api' => [
    'driver' => 'sanctum', // or 'token' / 'passport'
    'provider' => 'users',
  ],
],

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

Change default guard at runtime: Auth::shouldUse('api');

3. Migration & Model for Users

Default users table (from php artisan migrate) includes name, email, password, timestamps, optionally email_verified_at.

Password hashing (never store plain text):

php
use Illuminate\Support\Facades\Hash;

$user->password = Hash::make($request->password);
use Illuminate\Support\Facades\Hash;

$user->password = Hash::make($request->password);

Verify:

php
Hash::check($providedPassword, $user->password);
Hash::check($providedPassword, $user->password);

4. Basic Registration & Login (Session Guard)

Routes (routes/web.php):

php
Route::get('/register', [RegisteredUserController::class,'create']);
Route::post('/register', [RegisteredUserController::class,'store']);
Route::get('/login', [AuthenticatedSessionController::class,'create'])->name('login');
Route::post('/login', [AuthenticatedSessionController::class,'store']);
Route::post('/logout', [AuthenticatedSessionController::class,'destroy'])->middleware('auth');
Route::get('/register', [RegisteredUserController::class,'create']);
Route::post('/register', [RegisteredUserController::class,'store']);
Route::get('/login', [AuthenticatedSessionController::class,'create'])->name('login');
Route::post('/login', [AuthenticatedSessionController::class,'store']);
Route::post('/logout', [AuthenticatedSessionController::class,'destroy'])->middleware('auth');

Controller snippet (simplified):

php
class AuthenticatedSessionController extends Controller
{
  public function store(Request $request)
  {
    $credentials = $request->validate([
      'email' => ['required','email'],
      'password' => ['required']
    ]);

    if (!Auth::attempt($credentials, remember: $request->boolean('remember'))) {
      return back()->withErrors(['email' => 'Invalid credentials']);
    }

    $request->session()->regenerate(); // prevent fixation
    return redirect()->intended('/dashboard');
  }

  public function destroy(Request $request)
  {
    Auth::logout();
    $request->session()->invalidate();
    $request->session()->regenerateToken();
    return redirect('/');
  }
}
class AuthenticatedSessionController extends Controller
{
  public function store(Request $request)
  {
    $credentials = $request->validate([
      'email' => ['required','email'],
      'password' => ['required']
    ]);

    if (!Auth::attempt($credentials, remember: $request->boolean('remember'))) {
      return back()->withErrors(['email' => 'Invalid credentials']);
    }

    $request->session()->regenerate(); // prevent fixation
    return redirect()->intended('/dashboard');
  }

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

Protect dashboard route:

php
Route::get('/dashboard', DashboardController::class)->middleware('auth');
Route::get('/dashboard', DashboardController::class)->middleware('auth');

Check in Blade:

blade
@auth
  <p>Hello, {{ auth()->user()->name }}</p>
@else
  <a href="/login">Login</a>
@endauth
@auth
  <p>Hello, {{ auth()->user()->name }}</p>
@else
  <a href="/login">Login</a>
@endauth

5. Email Verification (Optional)

Migration must have email_verified_at column. Add middleware:

php
Route::get('/account', AccountController::class)
  .middleware(['auth','verified']);
Route::get('/account', AccountController::class)
  .middleware(['auth','verified']);

Trigger verification email (mustVerifyEmail interface in User model):

php
if ($user instanceof MustVerifyEmail && !$user->hasVerifiedEmail()) {
  $user->sendEmailVerificationNotification();
}
if ($user instanceof MustVerifyEmail && !$user->hasVerifiedEmail()) {
  $user->sendEmailVerificationNotification();
}

6. Password Reset Flow (Conceptual)

  1. User requests reset: enters email.
  2. Laravel stores token in password_reset_tokens (migration).
  3. Email sent with signed URL containing token.
  4. User sets new password; token consumed.

Command scaffolds (Breeze / Jetstream) include this; for manual implementation use Password::sendResetLink() & Password::reset() helpers.

7. Middleware Summary

MiddlewarePurpose
authEnsures authenticated user
guestRedirect if already logged in
verifiedEmail verified requirement
password.confirmRe-ask password for sensitive actions
throttle:60,1Rate limit (e.g., login attempts)

Apply inline: ->middleware('auth','password.confirm').

8. API Auth with Sanctum (Token Example)

Install Sanctum (if not pre-installed):

bash
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

Add middleware group in app/Http/Kernel.php (already added in recent versions): \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class for SPA.

Issue token:

php
Route::post('/api/login', function(Request $request){
  $user = User::where('email',$request->email)->first();
  if (!$user || !Hash::check($request->password, $user->password)) {
    return response()->json(['message'=>'Invalid'], 401);
  }
  return ['token' => $user->createToken('api')->plainTextToken];
});
Route::post('/api/login', function(Request $request){
  $user = User::where('email',$request->email)->first();
  if (!$user || !Hash::check($request->password, $user->password)) {
    return response()->json(['message'=>'Invalid'], 401);
  }
  return ['token' => $user->createToken('api')->plainTextToken];
});

Protect route:

php
Route::middleware('auth:sanctum')->get('/api/me', fn(Request $r) => $r->user());
Route::middleware('auth:sanctum')->get('/api/me', fn(Request $r) => $r->user());

Revoke tokens: $user->tokens()->delete();

9. Gates vs Policies

FeatureGatePolicy
DefinitionClosure-based abilityClass grouped by model
OrganizationAd-hoc / small appsLarger apps / per resource
MethodsSingle ability namesCRUD-style: view, create, update, delete, restore, forceDelete
TestingAssert Gate::allows()Call policy methods

Register gate (AuthServiceProvider):

php
Gate::define('view-admin-panel', function(User $user){
  return $user->is_admin;
});
Gate::define('view-admin-panel', function(User $user){
  return $user->is_admin;
});

Use:

php
if (Gate::denies('view-admin-panel')) abort(403);
if (Gate::denies('view-admin-panel')) abort(403);

Policy generation:

bash
php artisan make:policy PostPolicy --model=Post
php artisan make:policy PostPolicy --model=Post

PostPolicy snippet:

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

Controller helper:

php
$this->authorize('update', $post);
$this->authorize('update', $post);

Blade:

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

10. Roles & Permissions Pattern

Laravel doesn't include roles by default; you can:

  1. Use a package (spatie/laravel-permission).
  2. Or build minimal tables:
roles (id, name)
permissions (id, name)
role_user (role_id, user_id)
permission_role (permission_id, role_id)
roles (id, name)
permissions (id, name)
role_user (role_id, user_id)
permission_role (permission_id, role_id)

Attach logic:

php
public function hasRole(string $role): bool
{ return $this->roles->contains('name',$role); }

public function canDo(string $permission): bool
{ return $this->roles->flatMap->permissions->pluck('name')->contains($permission); }
public function hasRole(string $role): bool
{ return $this->roles->contains('name',$role); }

public function canDo(string $permission): bool
{ return $this->roles->flatMap->permissions->pluck('name')->contains($permission); }

Cache resolved permission sets for performance.

11. Rate Limiting

Example (login attempts) in RouteServiceProvider or routes file:

php
RateLimiter::for('login', function(Request $request){
  return Limit::perMinute(5)->by($request->ip().':'.$request->email);
});
RateLimiter::for('login', function(Request $request){
  return Limit::perMinute(5)->by($request->ip().':'.$request->email);
});

Apply middleware: ->middleware('throttle:login'). Generic: throttle:60,1 means 60 requests per 1 minute window.

12. Security Best Practices

AreaPractice
PasswordsAlways Hash::make (bcrypt/argon2id), never md5/sha1
SessionsRegenerate on login; use HTTPS & secure cookies
CSRFEnabled by default for web guard forms (@csrf)
Brute ForceRate limit login & password reset endpoints
Inactive SessionsUse php artisan session:table + TTL policies / remember token appropriately
Token StorageShow personal access token once; hash if storing manually
AuthorizationAlways re-check on server (never trust hidden inputs)
LoggingLog suspicious access attempts (multiple 403)
Email LinksUse signed URLs for sensitive actions

13. Putting It Together (Example Flow)

POST /login -> validate -> Auth::attempt() -> session regenerate -> redirect /dashboard
GET /dashboard -> 'auth' middleware -> user resolved -> controller -> policy check -> response
API: Authorization header Bearer <token> -> auth:sanctum guard -> user -> policy -> JSON response
POST /login -> validate -> Auth::attempt() -> session regenerate -> redirect /dashboard
GET /dashboard -> 'auth' middleware -> user resolved -> controller -> policy check -> response
API: Authorization header Bearer <token> -> auth:sanctum guard -> user -> policy -> JSON response

14. Troubleshooting

SymptomCauseFix
Always logged outSession domain / cookie misconfiguredCheck SESSION_DOMAIN, browser cookie settings
Auth::user() null in jobsQueue not using ShouldBeUnique? Actually job runs without sessionPass user ID to job and re-fetch
Policy not firingNot registeredConfirm in AuthServiceProvider::$policies
Gate denies unexpectedlyWrong guard activeAuth::shouldUse('web') or specify middleware guard
Token unauthorizedMissing auth:sanctum middlewareAdd to protected route group

15. Summary

Authentication answers "WHO are you?" using guards, providers, sessions or tokens. Authorization answers "CAN you do this?" via gates, policies, roles, and permissions. Keep controllers slim by centralizing permission logic in policies, rate-limit sensitive endpoints, and always hash credentials & regenerate sessions.