Authentication in Laravel Without Reinventing the Wheel
· 5 min read
Lesson 13 used `auth` middleware and `auth()->id()` without explaining how a user actually gets logged in, in the first place.
Lesson 13 used auth middleware and auth()->id() without explaining how a user actually gets logged in, in the first place, or why that auth middleware trusts the request at all. This lesson covers Laravel's authentication system properly, and the title is deliberately specific: the entire point of this lesson is that you should almost never write this yourself from scratch.
What Authentication Means
Authentication, confirming who a user is, sounds simple but is genuinely difficult to get fully right: password hashing has to use a slow, salted algorithm resistant to brute-force and rainbow-table attacks, session tokens need to be generated unpredictably and invalidated correctly on logout, "remember me" cookies need their own careful handling so they don't become a permanent security hole, and every one of these pieces needs to work together correctly, every time, with no gaps. Rolling your own authentication system means becoming personally responsible for every one of those details being correct, forever, across every change anyone ever makes to the codebase. Laravel's built-in authentication, used by an enormous number of production applications and scrutinized accordingly, has already solved this.
Logging A User In
At its core, logging a user in is one call:
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AuthController extends Controller
{
public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (! Auth::attempt($credentials)) {
return back()->withErrors(['email' => 'Those credentials do not match our records.']);
}
$request->session()->regenerate();
return redirect()->intended('/dashboard');
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/login');
}
}
Auth::attempt($credentials) takes the email and password, finds the matching user record, and compares the submitted password against the stored hash, using PHP's password_verify() underneath (never comparing plain-text passwords directly, which is exactly why you never see the raw password being compared with === anywhere in this flow). If it matches, Laravel logs the user in, storing their identity in the session, and returns true.
$request->session()->regenerate() right after a successful login deserves specific attention, because skipping it is a real, named vulnerability called session fixation. Without regenerating the session ID after login, an attacker who somehow got a victim to use a session ID the attacker already knows (through a crafted link, for instance) before the victim logs in, could then use that same session ID themselves afterward, now authenticated as the victim. Regenerating the session ID the moment authentication status changes closes this off completely, and it is exactly the kind of one-line detail that is trivial to forget if you were writing login logic entirely from scratch, and exactly why this lesson's framing matters: Laravel's own authentication scaffolding already includes this, correctly, every time.
redirect()->intended('/dashboard') is a small but genuinely useful detail: if the user originally tried to visit a protected page before being redirected to log in (because auth middleware, from lesson 13, caught them), Laravel remembers that original destination and sends them there after a successful login instead of always to a generic dashboard, which is the behavior users actually expect.
Hashing Passwords Safely
Password hashing on the way in, when a user registers or changes their password, uses the same underlying mechanism in reverse:
use Illuminate\Support\Facades\Hash;
$user = User::create([
'name' => $request->validated('name'),
'email' => $request->validated('email'),
'password' => Hash::make($request->validated('password')),
]);
Hash::make() runs the password through bcrypt (Laravel's default, configurable to other algorithms like Argon2) before it ever touches the database. The raw password is never stored anywhere, not even temporarily; only this one-way hash is. This is why a genuine password reset always issues a new password rather than "recovering" the old one; there is nothing to recover, by design, because recovering it would mean it was never truly hashed at all.
Checking The Current User
Once a user is logged in, checking their identity anywhere in the application is consistent and simple, which is exactly the payoff for using the built-in system instead of something custom:
if (Auth::check()) {
$user = Auth::user();
echo $user->name;
}
if ($trade->user_id !== Auth::id()) {
abort(403);
}
Auth::check() answers "is anyone logged in at all." Auth::user() returns the currently authenticated user's model, or null if nobody is logged in, exactly the same model instance you would get from User::find($id), which means every relationship and method from lessons 9 and 11 works on it identically. Auth::id() is a convenience shortcut for Auth::user()?->id, used constantly for authorization checks like the one above, confirming the currently logged-in user actually owns the record they are trying to act on, not just any record.
A Common Authorization Mistake
A real mistake worth naming because it is easy to miss during a quick build: checking only that a user is logged in (auth middleware) without also checking that they own or are permitted to act on the specific record they're requesting. Route::get('/trades/{trade}/edit', ...) protected by auth middleware stops anonymous visitors, but it does nothing to stop a logged-in user from editing someone else's trade simply by guessing or incrementing an ID in the URL, unless the controller itself explicitly checks $trade->user_id === Auth::id() (or uses Laravel's Policy/Gate authorization system, a more structured version of exactly this check). Authentication answers "who are you"; authorization answers "are you allowed to do this specific thing," and a real application needs both, deliberately, not just the first one.
Try it yourself: build a minimal login form posting to the login method above, log in with a seeded test user, then add an authorization check to TradeController::show() that calls abort(403) if $trade->user_id !== Auth::id(), and confirm by logging in as two different users that each can only view their own trades.
KEEP READING