DALT.PHP

Broken Authentication

Fix password verification vulnerability

Challenge: Broken Authentication

Difficulty: Easy
Bugs: 1
Time: 45 minutes

The Problem

The authentication system has a critical bug that prevents users from logging in.

Symptom: Login always fails with "Invalid credentials" even when using the correct password.

Setup

Backup Current Files

cp framework/Core/Authenticator.php framework/Core/Authenticator.php.backup

Copy Broken Files

cp challenges/broken-auth/framework/Core/Authenticator.php framework/Core/
cp -r challenges/broken-auth/Http/controllers/auth Http/controllers/

Add Routes

cat challenges/broken-auth/routes/routes.php >> routes/routes.php

Ensure Database Exists

php artisan migrate

Test the Bug

  1. Visit http://localhost:8000/auth/register
  2. Register a new user (this works!)
  3. Try to login with the same credentials (this fails!)

The Bug: Plain Text Password Comparison

What's Happening

// BROKEN - Plain text comparison
public function attempt($email, $password) {
    $user = $this->findUser($email);
    
    if ($user && $password == $user['password']) {
        // This never matches!
        $this->login($user);
        return true;
    }
    return false;
}

Why It's Broken

During registration, passwords are hashed:

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

During login, the code compares plain text against the hash:

'password123' == '$2y$10$abcdefghijklmnopqrstuvwxyz...'
// Always false!

The plain text password will never equal the hashed password.

Security Issue: Even if this worked, using == for password comparison is insecure. Always use password_verify().

Understanding Password Hashing

Why Hash Passwords?

Never store plain text passwords! If your database is compromised, attackers get all passwords.

How password_hash() Works

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

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

How password_verify() Works

$password = 'password123';
$hash = '$2y$10$abcdefghijklmnopqrstuvwxyz123456789...';

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

password_verify() extracts the salt from the hash, recomputes, and compares securely.

The Fix

Replace plain text comparison with password_verify():

// ✅ CORRECT
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([
            'email' => $email,
        ]);
        return true;
    }
    return false;
}

Why This Works: password_verify() correctly compares the plain text password against the hashed password.

Verification

After fixing the bug, run verification:

php artisan verify broken-auth

Expected output:

╔══════════════════════════════════════════════════════════════╗
║           DALT Challenge Verification System                ║
╚══════════════════════════════════════════════════════════════╝

Verifying: broken-auth
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

✓ File contains password_verify function call
✓ File does not contain plain text comparison
✓ Authentication logic is correct

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Results: 3/3 tests passed
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

✅ All tests passed! Challenge complete!

Testing Your Fix

1. Register a New User

Visit: http://localhost:8000/auth/register
Email: test@example.com
Password: password123

2. Verify Password is Hashed

sqlite3 database/app.sqlite
SELECT email, password FROM users;
# Should see hashed password like: $2y$10$...

3. Login with Same Credentials

Visit: http://localhost:8000/auth/login
Email: test@example.com
Password: password123
# Should redirect to dashboard

4. Verify Session

// Add to any controller
dd($_SESSION);
// Should see: ['user' => ['email' => 'test@example.com']]

Success Criteria

When fixed correctly:

  • ✅ Users can register with hashed passwords
  • ✅ Users can login with correct credentials
  • ✅ Login fails with incorrect credentials
  • ✅ Session persists after login
  • ✅ Protected routes are accessible after login

Learning Objectives

After completing this challenge, you understand:

  • ✅ Why passwords must be hashed
  • ✅ How password_hash() and password_verify() work
  • ✅ Why plain text comparison fails with hashed passwords
  • ✅ How authentication flow works from registration to login
  • ✅ Why timing-safe comparison matters

Debugging Tips

Check What's Stored

// In Authenticator::attempt()
dd($user);
// See the hashed password

Compare Values

dd([
    'plain' => $password,
    'hash' => $user['password'],
    'match' => $password == $user['password'] // Always false!
]);

Test password_verify

dd(password_verify($password, $user['password']));
// Should be true for correct password

Files to Investigate

  • framework/Core/Authenticator.php - Login logic (bug is here!)
  • Http/controllers/auth/register-post.php - See how passwords are hashed
  • Http/controllers/auth/login-post.php - See how login is attempted
  • framework/Core/Middleware/Auth.php - See how authentication is checked

Cleanup

After completing the challenge:

# Restore original Authenticator
cp framework/Core/Authenticator.php.backup framework/Core/Authenticator.php

# Remove challenge controllers (optional)
rm -rf Http/controllers/auth

Next Challenge

Continue to Lesson 5: Database or try Challenge: Broken Database.

On this page