The Authenticator Class
How the Authenticator manages login and logout operations
The Gatekeeper
Authentication is about proving who you are:
- "I'm user@example.com"
- "Here's my password"
- "Let me in"
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 inlogin()- Mark as logged inlogout()- 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:
false2. 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
trueif they match,falseif 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
trueparameter)
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 sessionThe defense:
1. Victim has session ID: abc123
2. Victim logs in
3. Session ID changes to: xyz789
4. Attacker's abc123 is now uselessBy 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():
- Clears
$_SESSION - Destroys session file
- 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'] ?? falseAfter login:
$_SESSION['user'] = ['email' => 'user@example.com'];
// Auth middleware: User exists, allow accessAfter logout:
Session::destroy(); // $_SESSION is empty
// Auth middleware: User doesn't exist, redirectUsage 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 insecureAlways 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
- attempt() verifies credentials - Finds user, checks password
- login() marks as logged in - Stores in session, regenerates ID
- logout() destroys session - Complete cleanup
- Returns boolean - Controllers know if login succeeded
- 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.