0

Session-based auth — login, logout, check, user()

Intermediate5 min read·fw-09-002
security

Concept

Session-based auth is the traditional web authentication model. After login, the user's ID is stored in the session. On subsequent requests, the session is read to identify the user.

Session flow:

  1. User submits credentials (email + password).
  2. Server validates credentials against the database.
  3. If valid: store $_SESSION['user_id'] = $user->id. PHP session ID is stored in a cookie (PHPSESSID) in the browser.
  4. On each subsequent request: PHP reads the session from the session store, the auth system fetches the user by session('user_id').
  5. Logout: destroy the session.

PHP session API:

  • session_start(): Initialize or resume a session. Must be called before headers are sent.
  • $_SESSION['key'] = $value: Store in session.
  • session_destroy(): Destroy the session. Also clear the session cookie.
  • session_regenerate_id(true): Generate a new session ID (prevent session fixation attacks on login).

Session fixation prevention: Call session_regenerate_id(true) after successful login. The true parameter deletes the old session.

Remember me: Store a long-lived cookie with a token. On future requests without an active session, look up the token and re-authenticate. Token stored in DB, hashed.

CSRF protection: Session-based auth is vulnerable to CSRF. Every form must include a CSRF token stored in the session and verified on submission.

Code Example

php
<?php
namespace Framework\Auth;

class SessionGuard implements GuardInterface
{
    private ?Authenticatable $resolvedUser = null;
    private string $sessionKey = 'auth_user_id';

    public function __construct(
        private readonly UserProviderInterface $provider,
        private readonly \Framework\Http\Request $request,
    ) {
        // Ensure session is started
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
    }

    public function check(): bool { return $this->user() !== null; }
    public function guest(): bool { return !$this->check(); }
    public function id(): int|string|null { return $this->user()?->getAuthIdentifier(); }

    public function user(): ?Authenticatable
    {
        if ($this->resolvedUser !== null) return $this->resolvedUser;

        $id = $_SESSION[$this->sessionKey] ?? null;
        if ($id === null) return null;

        return $this->resolvedUser = $this->provider->findById($id);
    }

    public function attempt(array $credentials): bool
    {
        $user = $this->provider->findByCredentials($credentials);
        if (!$user || !$this->provider->validateCredentials($user, $credentials)) {
            return false;
        }
        $this->login($user);
        return true;
    }

    public function login(Authenticatable $user, bool $remember = false): void
    {
        // Prevent session fixation — ALWAYS regenerate after login
        session_regenerate_id(true);
        $_SESSION[$this->sessionKey] = $user->getAuthIdentifier();
        $this->resolvedUser = $user;

        if ($remember) {
            $this->queueRememberTokenCookie($user);
        }
    }

    public function logout(): void
    {
        unset($_SESSION[$this->sessionKey]);
        $this->resolvedUser = null;
        session_regenerate_id(true); // prevent session replay
    }

    public function validate(array $credentials): bool
    {
        $user = $this->provider->findByCredentials($credentials);
        return $user && $this->provider->validateCredentials($user, $credentials);
    }

    private function queueRememberTokenCookie(Authenticatable $user): void
    {
        $token = bin2hex(random_bytes(32));
        // Store hash($token) in users.remember_token, set cookie 'remember_me' = $token
        setcookie('remember_me', $token, time() + (86400 * 30), '/', '', true, true);
    }
}