Appearance
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
| Term | Meaning |
|---|---|
| Authentication | Validating identity (login) |
| Authorization | Determining access to a resource/action |
| Guard | How a user is authenticated on each request (session, token) |
| Provider | How user data is retrieved (Eloquent, Database) |
| Middleware | Pipeline layer that blocks/redirects unauthorized requests |
| Gate | Closure-based, single ability check |
| Policy | Class grouping authorization methods per model |
| Ability | Named permission (e.g., update-post) |
| Role (custom) | Group of abilities assigned to users |
Diagram:
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>
@endauth5. 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)
- User requests reset: enters email.
- Laravel stores token in
password_reset_tokens(migration). - Email sent with signed URL containing token.
- User sets new password; token consumed.
Command scaffolds (Breeze / Jetstream) include this; for manual implementation use Password::sendResetLink() & Password::reset() helpers.
7. Middleware Summary
| Middleware | Purpose |
|---|---|
auth | Ensures authenticated user |
guest | Redirect if already logged in |
verified | Email verified requirement |
password.confirm | Re-ask password for sensitive actions |
throttle:60,1 | Rate 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 migratecomposer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrateAdd 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
| Feature | Gate | Policy |
|---|---|---|
| Definition | Closure-based ability | Class grouped by model |
| Organization | Ad-hoc / small apps | Larger apps / per resource |
| Methods | Single ability names | CRUD-style: view, create, update, delete, restore, forceDelete |
| Testing | Assert 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=Postphp artisan make:policy PostPolicy --model=PostPostPolicy 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>
@endcan10. Roles & Permissions Pattern
Laravel doesn't include roles by default; you can:
- Use a package (
spatie/laravel-permission). - 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
| Area | Practice |
|---|---|
| Passwords | Always Hash::make (bcrypt/argon2id), never md5/sha1 |
| Sessions | Regenerate on login; use HTTPS & secure cookies |
| CSRF | Enabled by default for web guard forms (@csrf) |
| Brute Force | Rate limit login & password reset endpoints |
| Inactive Sessions | Use php artisan session:table + TTL policies / remember token appropriately |
| Token Storage | Show personal access token once; hash if storing manually |
| Authorization | Always re-check on server (never trust hidden inputs) |
| Logging | Log suspicious access attempts (multiple 403) |
| Email Links | Use 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 responsePOST /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 response14. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Always logged out | Session domain / cookie misconfigured | Check SESSION_DOMAIN, browser cookie settings |
Auth::user() null in jobs | Queue not using ShouldBeUnique? Actually job runs without session | Pass user ID to job and re-fetch |
| Policy not firing | Not registered | Confirm in AuthServiceProvider::$policies |
| Gate denies unexpectedly | Wrong guard active | Auth::shouldUse('web') or specify middleware guard |
| Token unauthorized | Missing auth:sanctum middleware | Add 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.