skip to content

In a Laravel 13 car-dealership app where sales staff and customers sign in separately, how would you configure a second admin guard, and what traps come with it?

level: seniorimportance: must knowfreq 50%

answer

  1. one guard plus one provider per audience
  2. Staff model extends Foundation\Auth\User
  3. auth:admin on the back-office routes
  4. separate login_ session keys, one session
  5. a second password broker if staff reset

basics

~20 s

Add a staff provider (eloquent, App\Models\Staff) and an admin guard (session driver, provider staff) to config/auth.php, protect back-office routes with auth:admin, and name the guard explicitly in shared code. Both guards share one session under separate keys.

solid answer

~40 s

Create a `Staff` model that extends `Illuminate\Foundation\Auth\User` and a `staff` table, then add a provider `'staff' => ['driver' => 'eloquent', 'model' => Staff::class]` and a guard `'admin' => ['driver' => 'session', 'provider' => 'staff']` in `config/auth.php`. Protect the back office with `auth:admin` and sign staff in through `Auth::guard('admin')`. The traps: unnamed calls such as `Auth::user()` still read the default `web` guard outside `auth:admin` routes; both session guards live in one session under different `login_<guard>_<hash>` keys, so one browser can be signed in as a customer and as staff at once, and a logout that invalidates the session ends both; the guest redirect needs to send staff to the staff login; and password resets need a second broker whose `provider` is `staff`. Consider whether one users table with a role would be simpler before splitting.

code

php · 20 lines
php
<?php

// config/auth.php (excerpt)
use App\Models\Staff;
use App\Models\User;

return [
    'defaults' => [
        'guard' => env('AUTH_GUARD', 'web'),
        'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
    ],
    'guards' => [
        'web' => ['driver' => 'session', 'provider' => 'users'],
        'admin' => ['driver' => 'session', 'provider' => 'staff'],
    ],
    'providers' => [
        'users' => ['driver' => 'eloquent', 'model' => env('AUTH_MODEL', User::class)],
        'staff' => ['driver' => 'eloquent', 'model' => Staff::class],
    ],
];

go deeper

for a junior

Know that config/auth.php can hold more than one guard, each with its own provider, and that auth:admin protects routes with a named guard.

for a middle

Walk through adding the staff provider, the admin guard, the Staff model extending Foundation\Auth\User, and signing staff in through Auth::guard('admin').

for a senior

Name the traps before they bite: unnamed calls reading web, shared session keys, guest redirects, a second password broker and policies receiving a Staff instance.

for a principal

Weigh two identity tables against one table with roles, considering reporting, shared features, package support and the cost of every future auth feature doubling.

## When a second guard is the right tool A car-dealership application has two audiences who never overlap: **customers** browsing stock and booking test drives, and **sales staff** managing inventory and deals in a back office. When the two live in **different tables with different fields**, and staff must never be able to sign in through the customer form (or the reverse), Laravel's answer is a **second guard backed by a second user provider**. If they are really the same kind of person with different permissions, one `users` table with a role and gates or policies is usually simpler; the split is worth it when identity itself differs. ## The configuration 1. Create the model and table. `App\Models\Staff` extends `Illuminate\Foundation\Auth\User`, the same base class the skeleton's `User` uses, which brings `Authenticatable`, `Authorizable`, `CanResetPassword` and `MustVerifyEmail` behaviour. Give the table a `password` column and a nullable `remember_token` if staff may tick "remember me". 2. Add a **provider** in `config/auth.php`: `'staff' => ['driver' => 'eloquent', 'model' => App\Models\Staff::class]`. 3. Add a **guard**: `'admin' => ['driver' => 'session', 'provider' => 'staff']`. 4. Leave `defaults.guard` on `web`, because most routes serve customers. 5. Put the back office behind `auth:admin`, and sign staff in through `Auth::guard('admin')`. | Audience | Guard | Driver | Provider | Model | |---|---|---|---|---| | Customers | `web` | `session` | `users` | `App\Models\User` | | Sales staff | `admin` | `session` | `staff` | `App\Models\Staff` | ## How two session guards coexist Both guards use the same session store, but `SessionGuard` names its session key after the guard: `login_` + guard name + `_` + a SHA-1 of the guard class, and its remember-me cookie `remember_` + guard name + the same hash. So `web` and `admin` never overwrite each other. Consequences worth stating in an interview: - One browser can be signed in as a customer **and** as a staff member at the same time. - Signing out of one guard through that guard's `logout()` leaves the other signed in, but a logout route that also invalidates the whole session flushes both guards' keys. - Session driver choice, lifetime and cookie settings are shared; a stricter staff session lifetime needs something beyond `config/auth.php`. ## The traps - **Unnamed calls read the default guard.** `Auth::user()`, `@auth` and `$request->user()` resolve `web` unless `auth:admin` ran on that route (it calls `Auth::shouldUse('admin')`). Shared layouts and services should name the guard: `Auth::guard('admin')->user()`. - **The unauthenticated redirect.** When `auth` rejects a request it throws `AuthenticationException`, and the default redirect goes to the route named `login`. Staff should land on the staff login; the closure you pass to `redirectGuestsTo` in `bootstrap/app.php` receives the request, so it can branch on the URL prefix. - **Password resets.** The `passwords` section of `config/auth.php` holds brokers, each tied to a provider. Staff resets need a second broker whose `provider` is `staff`, or reset links will look staff up in `users`. - **Authorization sees a different class.** Behind `auth:admin`, policies receive a `Staff` instance, so a policy method whose first parameter is type-hinted `User` fails with a type error. - **Traits are per model.** `Illuminate\Foundation\Auth\User` does not include `Notifiable` (the skeleton's `User` adds it), and Sanctum tokens belong to whichever model uses `HasApiTokens`, so add those traits to `Staff` if the back office needs mail or tokens. ## A quick sanity checklist - Does every back-office route carry `auth:admin`, not bare `auth`? - Does every shared view name its guard? - Does the staff login use `Auth::guard('admin')`, never the default? - Is there a staff password broker and a staff guest redirect?

  • Would you ever use one provider for both guards instead of a Staff model?
    Yes, when staff and customers are the same kind of record distinguished by a role: two guards can point at the same `users` provider, but then nothing stops a customer's credentials from working on the staff login unless you add a condition to the sign-in. At that point a single guard plus a role checked by gates is usually clearer.
  • Why does a staff member's customer session survive when they sign out of the back office?
    Each session guard keeps its own `login_<guard>_<hash>` key, so `Auth::guard('admin')->logout()` removes only the admin key. The customer key stays unless the logout route also invalidates the whole session.
  • What does the password broker configuration need for staff resets?
    A second entry under `passwords`, for example `staff`, whose `provider` is `staff`, so reset requests look the email up in the staff table. The reset flow then targets that broker by name.

saying these in an interview costs you the question

  • A second guard needs a second session driver or cookie to avoid collisions.
  • Setting defaults.guard to admin is how you protect only the back office.
  • Signing out of the admin guard always signs the customer out too.
  • The staff model can extend the plain Eloquent Model and still sign in.
  • Password resets automatically follow whichever guard the user signed in with.