0

Session-based authentication — how login() and Auth::check() work

Intermediate5 min read·lv-16-002
laravel-srcsecurity

Concept

Session-based authentication is Laravel's default mechanism for stateful web applications. The entry point is Illuminate\Auth\SessionGuard::attempt(array $credentials, bool $remember = false). When called with an email/password pair, attempt() invokes $this->provider->retrieveByCredentials($credentials) to fetch a user whose non-password fields match (an SQL WHERE email = ? in the Eloquent provider). It then calls $this->provider->validateCredentials($user, $credentials), which runs Hash::check($credentials['password'], $user->getAuthPassword()). If the hash check passes, attempt() calls login($user, $remember).

login($user, $remember) does three things: it calls updateSession($user->getAuthIdentifier()) to write the user's id into the session under the key returned by getName() (which is login_web_<sha1 of guard name>), it fires the Illuminate\Auth\Events\Login event, and — if $remember is true — it calls queueRecallerCookie() to queue a long-lived remember_me cookie. This cookie contains the user's id, a random token, and a SHA-256 hash of both, stored in the remember_token column of the users table.

On subsequent requests where the session has expired or been cleared but the remember cookie is present, SessionGuard reads the cookie, splits out the id and token, queries the database to find the matching remember token, validates the hash, and re-logs the user automatically via loginUsingId(). This is how the "remember me" feature survives across browser sessions.

Auth::check() internally calls $this->user() !== null. Auth::id() calls $this->user()?->getAuthIdentifier(). Both are thin wrappers that trigger the lazy user resolution described above. The Authenticate middleware (Illuminate\Auth\Middleware\Authenticate) calls Auth::check() and redirects to the login route if the user is not authenticated — or returns a 401 JSON response for requests that expect JSON.

MethodRole
Auth::attempt($credentials, $remember)Validate credentials and start session
Auth::login($user, $remember)Log in a pre-fetched user model
Auth::loginUsingId($id, $remember)Log in by primary key (skips credential check)
Auth::once($credentials)Log in for current request only (no session)
Auth::logout()Invalidate session, delete remember cookie, regenerate session id
Auth::logoutOtherDevices($password)Rotate remember tokens on all other devices

Code Example

php
<?php

namespace App\Http\Controllers\Auth;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;

class LoginController extends Controller
{
    public function store(Request $request): \Illuminate\Http\RedirectResponse
    {
        $credentials = $request->validate([
            'email'    => ['required', 'email'],
            'password' => ['required'],
        ]);

        $remember = $request->boolean('remember');

        if (Auth::attempt($credentials, $remember)) {
            // Regenerate session id to prevent session fixation attacks
            $request->session()->regenerate();

            return redirect()->intended('/dashboard');
        }

        // Return back with a generic error — do not reveal whether
        // the email or password specifically was wrong.
        return back()->withErrors([
            'email' => __('auth.failed'),
        ])->onlyInput('email');
    }

    public function destroy(Request $request): \Illuminate\Http\RedirectResponse
    {
        Auth::logout();

        // Invalidate the session entirely and regenerate the CSRF token
        $request->session()->invalidate();
        $request->session()->regenerateToken();

        return redirect('/');
    }
}

// Logging out all other devices (requires password re-confirmation)
Auth::logoutOtherDevices($request->validated('current_password'));

Interview Q&A

Q: Walk through exactly what happens inside Auth::attempt() from the credentials array to a session write.

attempt() first calls $this->fireAttemptEvent($credentials), then delegates to $this->provider->retrieveByCredentials($credentials) which strips the password key and builds a WHERE email = ? query via Eloquent, returning the user model or null. If a user is found, it calls $this->provider->validateCredentials($user, $credentials) which runs Hash::check($plainPassword, $user->getAuthPassword()) using bcrypt or Argon2. On success, attempt() calls $this->login($user, $remember) which writes the user's primary key into the session via $this->session->put($this->getName(), $id) and fires the Login event. The method returns true on success and false on any failure.


Q: Why must you call $request->session()->regenerate() after a successful login?

Session fixation is an attack where a malicious actor plants a known session ID into the victim's browser (e.g., via a URL parameter on a poorly configured server) before the victim logs in. If the application keeps the same session ID after login, the attacker — who knows the ID — can now make authenticated requests as the victim. regenerate() issues a new session ID and migrates the existing session data to it, invalidating the old ID. Laravel's built-in auth scaffolding always calls regenerate() on login and invalidate() on logout for this reason.


Q: How does the "remember me" cookie work and where is the token stored?

When $remember is true, SessionGuard::login() calls createRememberTokenIfDoesntExist($user) to ensure the remember_token column is populated, then queues a cookie named remember_web_<hash> containing base64_encode($id . '|' . $token . '|' . $passwordHash). On subsequent requests, SessionGuard::user() checks for this cookie when no session identifier is found, unpacks it, queries for the user by id, verifies the token matches remember_token in the database, and then verifies the password hash has not changed (which invalidates all remember tokens on password change). The token is single-use by design in more recent Laravel versions — it is rotated after each use to prevent token theft from replay attacks.