Login, Registration & Logout
Guards, hashing, remember-me, password resets and protecting routes.
Almost every app needs to know who is making the request. Laravel's authentication system answers that with three pieces: guards (how a user is identified on each request), providers (where users are loaded from) and the session (how the login is remembered between requests).
This site uses all three. The admin panel at /admin is a real login form backed by Laravel's web guard, so the code below is the code that protects this site.
How a login works
A web login is a short conversation between the browser, the auth middleware and the session:
Step through it below. Each step shows what the browser sends, what Laravel does and what ends up in the session:
Guards and providers
Guards and providers are configured in config/auth.php. A fresh Laravel app, including this one, ships with one web guard that stores the login in the session and loads users with Eloquent:
config/auth.php
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', User::class),
],
],
| Piece | Answers | Common values |
|---|---|---|
| Guard | How do I recognise the user on this request? | session (web pages), token / Sanctum (APIs) |
| Provider | Where do I load the user from? | eloquent (a model), database (a table) |
| Password broker | How are reset tokens stored? | password_reset_tokens table |
You only add guards when you have genuinely different kinds of user, such as customers and staff stored in separate tables.
Storing passwords safely
Never store a password you can read back. Laravel hashes with bcrypt (or argon2) and checks with Hash::check(). This app's User model uses the hashed cast, so assigning a plain password hashes it automatically:
app/Models/User.php
protected $hidden = ['password', 'remember_token'];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
$hidden keeps the hash out of toArray() and JSON responses. The admin account itself is created from .env values by a seeder, and the hashed cast does the hashing:
database/seeders/AdminUserSeeder.php
public function run(): void
{
$email = config('admin.email');
$password = config('admin.password');
if (blank($email) || blank($password)) {
$this->command?->warn('ADMIN_EMAIL / ADMIN_PASSWORD not set — no admin account created.');
return;
}
User::updateOrCreate(
['email' => $email],
['name' => config('admin.name'), 'password' => $password],
);
}
The two Hash methods you will use directly:
$hash = Hash::make('secret'); // $2y$12$K3z... (different every time: random salt)
Hash::check('secret', $hash); // true
Hash::needsRehash($hash); // true after you raise the bcrypt cost
Logging in and out
A login controller is three calls: Auth::attempt(), session()->regenerate() and redirect()->intended(). Illustrative — this site's login form is provided by Filament, which does the same thing internally:
public function store(Request $request): RedirectResponse
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (! Auth::attempt($credentials, remember: $request->boolean('remember'))) {
return back()
->withErrors(['email' => 'These credentials do not match our records.'])
->onlyInput('email');
}
$request->session()->regenerate(); // stops session fixation
return redirect()->intended('/dashboard');
}
public function destroy(Request $request): RedirectResponse
{
Auth::logout();
$request->session()->invalidate(); // wipe the session data
$request->session()->regenerateToken(); // new CSRF token
return redirect('/');
}
Always regenerate the session id after login. If you skip it, an attacker who planted a session id in the victim's browser before login would share the logged-in session. That attack is called session fixation.
Use the same error message for "unknown email" and "wrong password", so the form can't be used to find out which emails have accounts.
Registration
Registering is "validate, create, log in". Illustrative — this site has no public sign-up:
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'confirmed', Password::defaults()],
]);
$user = User::create($validated); // 'hashed' cast hashes the password
event(new Registered($user)); // sends the verification email if enabled
Auth::login($user);
return redirect('/dashboard');
}
Password::defaults() lets you set the rules in one place, for example in AppServiceProvider::boot():
Password::defaults(fn () => app()->isProduction()
? Password::min(12)->mixedCase()->numbers()->uncompromised()
: Password::min(8));
Protecting routes
Put the auth middleware on anything that needs a logged-in user, and guest on pages only for visitors:
Route::middleware('auth')->group(function () {
Route::get('/dashboard', DashboardController::class);
Route::resource('projects', ProjectController::class);
});
Route::middleware('guest')->group(function () {
Route::get('/login', [LoginController::class, 'create'])->name('login');
Route::post('/login', [LoginController::class, 'store']);
});
Inside a controller or view you can read the current user several ways:
| Code | Returns |
|---|---|
Auth::user() / auth()->user() / $request->user() |
The User model, or null for guests |
Auth::id() |
Just the id, without loading the model |
Auth::check() / @auth ... @endauth |
Whether someone is logged in |
Who may log in: this site's admin gate
Being logged in is one question. Being allowed in is another. Filament asks the user model through canAccessPanel(). On this site only the single account named in .env passes:
app/Models/User.php
/**
* Only the account configured as ADMIN_EMAIL may use the admin panel.
*/
public function canAccessPanel(Panel $panel): bool
{
$adminEmail = config('admin.email');
return filled($adminEmail) && strcasecmp($this->email, $adminEmail) === 0;
}
config/admin.php
return [
'name' => env('ADMIN_NAME', 'Admin'),
'email' => env('ADMIN_EMAIL'),
'password' => env('ADMIN_PASSWORD'),
];
The tutorial's demo users (Ava, Ben, Chloe and Dan) exist only as task assignees. They have random 64-character passwords and are refused by this check even if somebody guessed one. That rule is pinned down by a feature test, where owner@example.test is the admin email set for the test run:
tests/Feature/AdminAccessTest.php (excerpt)
public function test_other_users_are_refused_by_the_admin_panel(): void
{
$this->actingAs(User::factory()->create())
->get('/admin')
->assertForbidden();
$this->actingAs(User::factory()->create(['email' => 'owner@example.test']))
->get('/admin')
->assertOk();
}
actingAs() is the testing shortcut for "log this user in". You never need to post a login form in a test. For finer-grained rules ("may this user edit this task?") use policies, covered in Middleware, Auth & Policies.
Remember me and password resets
Remember me. Pass remember: true to Auth::attempt(). Laravel then stores a long-lived token in the remember_token column and in a cookie, so the user stays logged in after the session expires.
Password resets. The password_reset_tokens table and the Password facade cover the whole flow. Illustrative:
// 1. Email a reset link
$status = Password::sendResetLink($request->only('email'));
// 2. Later, reset with the token from the link
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function (User $user, string $password) {
$user->forceFill(['password' => $password])->setRememberToken(Str::random(60));
$user->save();
event(new PasswordReset($user));
},
);
Starter kits and API tokens
You rarely write all of this by hand. laravel new offers starter kits (React, Vue or Livewire) that generate login, registration, password reset, email verification and profile pages. You can then edit them like your own code.
For APIs and mobile apps, sessions don't fit. Laravel Sanctum issues API tokens and also authenticates your own SPA through its session cookie. Illustrative — Sanctum isn't installed on this site; its demo API is public and read-only:
$token = $user->createToken('mobile-app', ['tasks:read'])->plainTextToken;
Route::middleware('auth:sanctum')->get('/api/me', fn (Request $request) => $request->user());
Clients send the token as Authorization: Bearer <token>.
Key takeaways
- A guard identifies the user on each request (session for web pages, tokens for APIs). A provider loads them, usually through Eloquent.
- Store passwords only as hashes. The
hashedcast plus$hiddenhandle that on theUsermodel. - Logging in means
Auth::attempt(), thensession()->regenerate(), thenredirect()->intended(). Logging out also invalidates the session and regenerates the CSRF token. - Protect routes with the
authmiddleware. Decide who may enter separately, as this site'scanAccessPanel()does. - Use
actingAs()in tests, starter kits for full auth UIs, and Sanctum for API tokens.