DALT.PHP
Framework Deep Dive5. Authentication

The Authenticator Class

How the Authenticator manages login and logout operations

The Gatekeeper

Authentication is about proving who you are:

The Authenticator class handles this verification and manages logged-in state.


The Authenticator Class

class Authenticator
{
    public function attempt($email, $password)
    {
        $user = App::resolve(Database::class)
            ->query('select * from users where email = :email', [
                'email' => $email
            ])->find();

        if ($user) {
            if (password_verify($password, $user['password'])) {
                $this->login($user);  // use the DB row, not raw input
                return true;
            }
        }
        return false;
    }

    public function login($user)
    {
        $_SESSION['user'] = [
            'email' => $user['email'],
        ];
        session_regenerate_id(true);
    }

    public function logout()
    {
        Session::destroy();
    }
}

Just three methods:

  • attempt() - Try to log in
  • login() - Mark as logged in
  • logout() - Log out

The attempt() Method

This is the heart of authentication:

public function attempt($email, $password)
{
    $user = App::resolve(Database::class)
        ->query('select * from users where email = :email', [
            'email' => $email
        ])->find();

    if ($user) {
        if (password_verify($password, $user['password'])) {
            $this->login($user);  // use the DB row, not raw input
            return true;
        }
    }
    return false;
}

Step-by-Step Breakdown

1. Find the user by email

$user = App::resolve(Database::class)
    ->query('select * from users where email = :email', [
        'email' => $email
    ])->find();

What this returns:

// If found:
[
    'id' => 1,
    'email' => 'user@example.com',
    'password' => '$2y$10$abc...xyz',  // Hashed
    'created_at' => '2024-01-15 10:30:00'
]

// If not found:
false

2. Check if user exists

if ($user) {

If find() returns false, user doesn't exist.

3. Verify the password

if (password_verify($password, $user['password'])) {

What password_verify() does:

  • Takes plain password: 'password123'
  • Takes hashed password: '$2y$10$abc...xyz'
  • Returns true if they match, false if not

Why not just compare?

// WRONG - This will never work
if ($password === $user['password']) {
    // 'password123' === '$2y$10$abc...xyz'
    // Always false!
}

Passwords are hashed (scrambled) in the database. You can't compare them directly.

4. Log the user in using Database Data

$this->login($user);

If the password is correct, we mark them as logged in by passing the verified database row ($user) down to the login() method. This ensures that the session stores the exact canonical email from the database, bypassing issues where users might submit differently-cased emails at login (like Alice@Example.COM).

5. Return success

return true;

6. Return failure if anything fails

return false;

Returns false if:

  • User doesn't exist
  • Password is wrong

Why Return True/False?

The controller needs to know if login succeeded:

// In a login controller
$auth = new Authenticator();

if ($auth->attempt($email, $password)) {
    // Success - redirect to dashboard
    redirect('/dashboard');
} else {
    // Failure - show error
    $errors = ['email' => 'Invalid credentials'];
    view('login.view.php', ['errors' => $errors]);
}

The login() Method

public function login($user)
{
    $_SESSION['user'] = [
        'email' => $user['email'],
    ];
    session_regenerate_id(true);
}

What It Does

1. Store user in session

$_SESSION['user'] = [
    'email' => $user['email'],
];

This marks the user as logged in. Now:

  • Auth middleware will pass
  • Controllers can access user data
  • User stays logged in across requests

Why only store email?

  • Minimal data in session
  • Can query database for full user data when needed
  • Session stays small

Could store more:

$_SESSION['user'] = [
    'id' => $user['id'],
    'email' => $user['email'],
    'name' => $user['name'],
];

2. Regenerate session ID

session_regenerate_id(true);

What this does:

  • Creates a new session ID
  • Copies session data to new ID
  • Deletes old session file (the true parameter)

Why regenerate?

Security. Prevents "session fixation" attacks:

The attack:

1. Attacker gets a session ID: abc123
2. Attacker tricks victim into using that ID
3. Victim logs in with ID abc123
4. Attacker now has access to victim's logged-in session

The defense:

1. Victim has session ID: abc123
2. Victim logs in
3. Session ID changes to: xyz789
4. Attacker's abc123 is now useless

By regenerating on login, the old session ID becomes invalid.


The logout() Method

public function logout()
{
    Session::destroy();
}

Simple! Just destroys the entire session.

Remember from Part 4.1, Session::destroy():

  1. Clears $_SESSION
  2. Destroys session file
  3. Deletes session cookie

User is completely logged out.


How Auth Middleware Uses This

Remember the Auth middleware from Part 2.4:

class Auth
{
    public function handle()
    {
        if(!($_SESSION['user'] ?? false)){
            header('location: /' );
            exit();
        }
    }
}

It checks:

$_SESSION['user'] ?? false

After login:

$_SESSION['user'] = ['email' => 'user@example.com'];
// Auth middleware: User exists, allow access

After logout:

Session::destroy();  // $_SESSION is empty
// Auth middleware: User doesn't exist, redirect

Usage in Controllers

Registration Controller

// app/Http/controllers/registration/store.php

// ... validation ...

// Create user
$db = App::resolve(Database::class);
$hashedPassword = password_hash($password, PASSWORD_BCRYPT);

$db->query('INSERT INTO users (email, password) VALUES (:email, :password)', [
    'email' => $email,
    'password' => $hashedPassword
]);

// Log them in automatically
$auth = new Authenticator();
$auth->login(['email' => $email]);

redirect('/');

Why log in after registration?

  • Better UX (don't make them log in separately)
  • They just proved they have the email/password
  • Common pattern in web apps

Login Controller

// app/Http/controllers/session/store.php

$email = $_POST['email'];
$password = $_POST['password'];

$auth = new Authenticator();

if ($auth->attempt($email, $password)) {
    redirect('/');
} else {
    $errors = ['email' => 'Invalid credentials'];
    view('session/create.view.php', ['errors' => $errors]);
}

Logout Controller

// app/Http/controllers/session/destroy.php

$auth = new Authenticator();
$auth->logout();

redirect('/');

What Gets Stored in the Session

After login:

$_SESSION = [
    'user' => [
        'email' => 'user@example.com'
    ],
    '_csrf' => 'abc123...',
    // ... other session data
];

Accessing the logged-in user:

$user = $_SESSION['user'];
echo "Welcome, " . $user['email'];

Or with Session helper:

$user = Session::get('user');

Security Considerations

1. Password Verification

password_verify($password, $user['password'])

Never do:

$password === $user['password']  // Won't work with hashes
hash('sha256', $password) === $user['password']  // Insecure
md5($password) === $user['password']  // Very insecure

Always use password_verify() with password_hash().

2. Session Regeneration

session_regenerate_id(true);

Always regenerate on:

  • Login
  • Privilege escalation (becoming admin)
  • Any authentication state change

3. Timing Attacks

The current code is vulnerable:

if ($user) {
    if (password_verify($password, $user['password'])) {
        // ...
    }
}
return false;

The problem:

  • If user doesn't exist: returns immediately (fast)
  • If user exists but password wrong: runs password_verify() (slow)
  • Attacker can measure timing to enumerate valid emails

Better approach:

$user = $this->findUser($email);
$validPassword = $user && password_verify($password, $user['password']);

if ($validPassword) {
    $this->login(['email' => $email]);
    return true;
}

return false;

Always run password_verify() even if user doesn't exist (use a dummy hash).

4. Rate Limiting

The current code has no rate limiting:

  • Attacker can try unlimited passwords
  • Brute force is possible

Production would need:

if ($this->tooManyAttempts($email)) {
    throw new TooManyAttemptsException();
}

$this->recordAttempt($email);

Key Takeaways

  1. attempt() verifies credentials - Finds user, checks password
  2. login() marks as logged in - Stores in session, regenerates ID
  3. logout() destroys session - Complete cleanup
  4. Returns boolean - Controllers know if login succeeded
  5. Session regeneration is important - Prevents fixation attacks

What's Good Here

✅ Simple, understandable implementation
✅ Uses password_verify() correctly
✅ Regenerates session ID on login
✅ Clean separation (auth logic in one class)

Design Note

DALT keeps the authenticator intentionally small:

  • It uses sessions (simple and visible).
  • It focuses on correct password hashing + session fixation protection.
  • Advanced production features (rate limiting, remember-me, MFA) are intentionally not included in the learning core.

On this page