Authentication and authorization

Starter-kit scaffolding, guards and providers, Gates and Policies, email verification, password reset and where the checks belong.

Guards, providers and scaffolding

// config/auth.php essentials
'guards' => [
    'web' => ['driver' => 'session', 'provider' => 'users'],
    'api' => ['driver' => 'sanctum', 'provider' => 'users'],
],
'providers' => [
    'users' => ['driver' => 'eloquent', 'model' => App\Models\User::class],
],

// routes/web.php from Breeze
Route::middleware('auth')->group(function () {
    Route::get('/dashboard', DashboardController::class)->name('dashboard');
});
Route::middleware('guest')->group(function () {
    Route::get('/login', [AuthenticatedSessionController::class, 'create']);
});

// customising the credentials used at login
if (! Auth::attempt(['email' => $email, 'password' => $password, 'active' => true], $remember)) {
    return back()->withErrors(['email' => 'Those credentials do not match.']);
}
Auth::guard('web')->logout();
request()->session()->invalidate();
request()->session()->regenerateToken();
ConcernWhere it livesNote
Who the user isGuard + providerSession guard for browsers, token guard for APIs
Is the session still validAuthenticateSession middlewareInvalidates other sessions after a password change
Email ownershipverified middlewarePlus MustVerifyEmail on the model
Password resetBroker and notifications tableTokens are hashed; rows expire via auth.passwords.users.expire
⚠️
Always regenerate the session on login ($request->session()->regenerate(), which Breeze does for you) and invalidate it on logout. Skipping this leaves the application open to session fixation.

Gates, policies and enforcement

class BookPolicy
{
    public function viewAny(User $user): bool
    {
        return true;
    }

    public function update(User $user, Book $book): bool
    {
        return $user->id === $book->author_id || $user->hasRole('editor');
    }

    public function delete(User $user, Book $book): bool
    {
        return $user->hasRole('admin');
    }
}

// controller: 403 automatically
$this->authorize('update', $book);

// or inside a form request
public function authorize(): bool
{
    return $this->user()->can('update', $this->route('book'));
}

// Blade and JS
@can('update', $book)
    <a href="{{ route('books.edit', $book) }}">Edit</a>
@endcan
@json(['canUpdate' => Auth::user()->can('update', $book)])
  • A policy is discovered by naming convention (Book to BookPolicy); register unusual pairs explicitly in a service provider.
  • authorize() throws AuthorizationException which Laravel renders as 403 - never rely on hiding a button as your only check.
  • Use Gate::before for a global admin bypass, and keep it to one line so it cannot become a policy of its own.
  • Deny by default: a policy method that returns nothing is a denial, and that is the right direction for an unhandled case.

Verification, reset and rate limits

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

Route::get('/billing', BillingController::class)
    ->middleware(['auth', 'verified', 'can:viewBilling,App\Models\Account']);

// throttling auth endpoints
RateLimiter::for('login', fn (Request $request) =>
    Limit::perMinute(5)->by($request->input('email').'|'.$request->ip()));

Throttle login by the submitted identifier as well as the IP, otherwise one attacker with many addresses freely brute-forces a single account. Notifications for reset links must be queued so a slow mail provider does not hold the request open.

FAQ

Sanctum or Passport?
Sanctum for a first-party SPA or a mobile app issuing simple personal access tokens, including cookie-based SPA auth. Passport when you must implement a full OAuth2 server with clients, scopes and grants for third parties.
Where should the authorization check go?
In the policy, called from a form request or authorize() at the controller edge. Never in the model and never only in the view - both leave a path that calls the action without a check.

Building APIs with Sanctum Controllers, Blade views and Eloquent models

Last refreshed 2026-09-18.