DALT.PHP

Authentication System

Learn how user login and session management works

Lesson 4: Authentication System

Authentication verifies user identity and maintains login state across requests.

The Authentication Flow

Registration

User creates an account:

// Hash the password
$hashedPassword = password_hash($_POST['password'], PASSWORD_BCRYPT);

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

Login

User submits credentials:

$auth = new Authenticator();

if ($auth->attempt($_POST['email'], $_POST['password'])) {
    redirect('/dashboard');
} else {
    $errors['email'] = 'Invalid credentials';
}

Session Storage

User data stored in session:

$_SESSION['user'] = [
    'email' => $user['email']
];
session_regenerate_id(true); // Security!

Protected Routes

Middleware checks session:

if (!isset($_SESSION['user'])) {
    redirect('/login');
}

Password Security

Never Store Plain Text!

// ❌ WRONG - Plain text
$db->query('INSERT INTO users (password) VALUES (:password)', [
    'password' => $_POST['password']
]);

// ✅ CORRECT - Hashed
$db->query('INSERT INTO users (password) VALUES (:password)', [
    'password' => password_hash($_POST['password'], PASSWORD_BCRYPT)
]);

How password_hash() Works

$password = 'secret123';
$hash = password_hash($password, PASSWORD_BCRYPT);
// Result: $2y$10$abcdefghijklmnopqrstuvwxyz...

// Same password, different hash each time (random salt)
$hash2 = password_hash($password, PASSWORD_BCRYPT);
// Result: $2y$10$zyxwvutsrqponmlkjihgfedcba...

Why different hashes? Each hash includes a random salt, making rainbow table attacks impossible.

How password_verify() Works

$password = 'secret123';
$hash = '$2y$10$abcdefghijklmnopqrstuvwxyz...';

password_verify($password, $hash);  // true
password_verify('wrong', $hash);    // false

Never use == for password comparison!

// ❌ WRONG
if ($password == $user['password']) { }

// ✅ CORRECT
if (password_verify($password, $user['password'])) { }

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 && password_verify($password, $user['password'])) {
            $this->login($user);
            return true;
        }
        return false;
    }
    
    public function login($user) {
        $_SESSION['user'] = [
            'email' => $user['email']
        ];
        session_regenerate_id(true);
    }
    
    public function logout() {
        Session::destroy();
    }
}

Session Management

Starting Sessions

// In public/index.php
session_name('DALT_SESSION');
session_start();

Storing User Data

$_SESSION['user'] = [
    'email' => 'user@example.com',
    'name' => 'John Doe'
];

Checking Authentication

if (isset($_SESSION['user'])) {
    // User is logged in
    $email = $_SESSION['user']['email'];
}

Session Regeneration

Always regenerate session ID after login:

session_regenerate_id(true);

Why? Prevents session fixation attacks.

Complete Login Example

Registration Form

<form method="POST" action="/register">
    <?= csrf_field() ?>
    <input type="email" name="email" required>
    <input type="password" name="password" required>
    <button type="submit">Register</button>
</form>

Registration Controller

// Validate
$errors = [];
if (!Validator::email($_POST['email'])) {
    $errors['email'] = 'Invalid email';
}
if (!Validator::string($_POST['password'], 8, 255)) {
    $errors['password'] = 'Password must be at least 8 characters';
}

if (!empty($errors)) {
    return view('auth/register.view.php', ['errors' => $errors]);
}

// Hash password
$hashedPassword = password_hash($_POST['password'], PASSWORD_BCRYPT);

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

// Auto-login
$auth = new Authenticator();
$auth->login(['email' => $_POST['email']]);

redirect('/dashboard');

Login Controller

$auth = new Authenticator();

if ($auth->attempt($_POST['email'], $_POST['password'])) {
    redirect('/dashboard');
}

$errors['email'] = 'Invalid credentials';
return view('auth/login.view.php', ['errors' => $errors]);

Logout Controller

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

redirect('/');

Protecting Routes

Use auth middleware:

// Require login
$router->get('/dashboard', 'dashboard.php')->only('auth');
$router->get('/profile', 'profile.php')->only('auth');

// Guest only (redirect if logged in)
$router->get('/login', 'auth/login.php')->only('guest');
$router->get('/register', 'auth/register.php')->only('guest');

Common Security Issues

Timing Attacks

// ❌ VULNERABLE
if ($token === $sessionToken) { }

// ✅ SECURE
if (hash_equals($sessionToken, $token)) { }

hash_equals() takes constant time, preventing timing attacks.

Session Fixation

// ❌ VULNERABLE
$_SESSION['user'] = $user;

// ✅ SECURE
$_SESSION['user'] = $user;
session_regenerate_id(true);

SQL Injection

// ❌ VULNERABLE
$query = "SELECT * FROM users WHERE email = '{$email}'";

// ✅ SECURE
$db->query('SELECT * FROM users WHERE email = :email', ['email' => $email]);

Always use prepared statements!

Debugging Authentication

Check Session

dd($_SESSION);

Verify Password Hash

$hash = password_hash('password123', PASSWORD_BCRYPT);
dd([
    'hash' => $hash,
    'verify' => password_verify('password123', $hash)
]);

Test Login Flow

// In Authenticator::attempt()
dd([
    'email' => $email,
    'user_found' => $user !== null,
    'password_match' => password_verify($password, $user['password'] ?? '')
]);

Security Tip: Never reveal whether the email or password was wrong. Always say "Invalid credentials".

Ready for the Challenge?

On this page