DALT.PHP
Framework Deep Dive5. Authentication

Login Flow

Complete login flow from form submission to session creation

The Complete Login Journey

Let's follow what happens when a user tries to log in, from the moment they click "Login" to when they're authenticated.

The Login Controller

When a user submits the login form, it goes to the login controller:

// .dalt/stubs/auth/Http/controllers/session/store.php

// 1. Get the form data
$email = $_POST['email'];
$password = $_POST['password'];

// 2. Validate the inputs
$errors = [];

if (!Validator::email($email)) {
    $errors['email'] = 'Please provide a valid email address.';
}

if (!Validator::string($password)) {
    $errors['password'] = 'Please provide a valid password.';
}

// 3. If validation fails, throw exception
if (!empty($errors)) {
    ValidationException::throw($errors, $_POST);
}

// 4. Try to log in
$auth = new Authenticator();

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

// 5. If login fails, show error
ValidationException::throw([
    'email' => 'No matching account found for that email address and password.'
], $_POST);

Step-by-Step Breakdown

Step 1: Get Form Data

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

When the user submits the form, their data comes in through $_POST. This is a PHP superglobal that contains all form data sent via POST method.

Step 2: Basic Validation

if (!Validator::email($email)) {
    $errors['email'] = 'Please provide a valid email address.';
}

if (!Validator::string($password)) {
    $errors['password'] = 'Please provide a valid password.';
}

Before we even check the database, we validate:

  • Is the email in a valid format?
  • Is the password not empty?

This catches obvious mistakes early (like typing "user@" without the domain).

Step 3: Handle Validation Errors

if (!empty($errors)) {
    ValidationException::throw($errors, $_POST);
}

If validation fails, we throw a ValidationException. Remember from the previous page:

  • This exception gets caught in public/index.php
  • The errors and old input get stored in flash
  • The user gets redirected back to the login form
  • The form shows the errors and remembers what they typed

Step 4: Attempt Login

$auth = new Authenticator();

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

Now we try to actually log in:

  1. Create an Authenticator instance
  2. Call attempt() with the email and password
  3. If it returns true, login succeeded → redirect to homepage
  4. If it returns false, continue to step 5

The attempt() method (from the previous page) does:

  • Find the user in the database by email
  • Check if the password matches using password_verify()
  • If both succeed, store the user in the session

Step 5: Handle Login Failure

ValidationException::throw([
    'email' => 'No matching account found for that email address and password.'
], $_POST);

If attempt() returned false, we throw another ValidationException with a generic error message.

Why a generic message?

  • Security: Don't tell attackers whether the email exists or the password was wrong
  • Just say "email and password don't match"

The Login Form View

Here's how the login form displays errors and remembers input:

<!-- .dalt/stubs/auth/resources/views/auth/login.view.php -->

<form method="POST" action="/login">
    <input type="hidden" name="csrf_token" value="<?= csrf_token() ?>">
    
    <div>
        <label>Email</label>
        <input 
            name="email" 
            type="email" 
            value="<?= htmlspecialchars(old('email')) ?>"
        />
        
        <?php if (isset($errors['email'])) : ?>
            <p class="error"><?= $errors['email'] ?></p>
        <?php endif; ?>
    </div>
    
    <div>
        <label>Password</label>
        <input 
            name="password" 
            type="password"
        />
        
        <?php if (isset($errors['password'])) : ?>
            <p class="error"><?= $errors['password'] ?></p>
        <?php endif; ?>
    </div>
    
    <button type="submit">Log In</button>
</form>

Key Parts Explained

CSRF Token:

<input type="hidden" name="csrf_token" value="<?= csrf_token() ?>">

This protects against CSRF attacks (covered in the middleware section).

Remembering Old Input:

value="<?= htmlspecialchars(old('email')) ?>"

The old() helper retrieves the email from flash data, so if login fails, the user doesn't have to retype their email.

Showing Errors:

<?php if (isset($errors['email'])) : ?>
    <p class="error"><?= $errors['email'] ?></p>
<?php endif; ?>

If there's an error for this field, display it below the input.

The Complete Flow Diagram

User submits form

POST /login

Login controller receives data

Validate email format ──→ Invalid? ──→ Throw ValidationException
    ↓                                        ↓
Validate password      ──→ Empty?   ──→ Throw ValidationException
    ↓                                        ↓
Authenticator::attempt()                     ↓
    ↓                                        ↓
Find user by email                           ↓
    ↓                                        ↓
User exists? ──→ No ──→ Return false ──→ Throw ValidationException
    ↓                                        ↓
    Yes                                      ↓
    ↓                                        ↓
password_verify() ──→ No ──→ Return false ──→ Throw ValidationException
    ↓                                        ↓
    Yes                                      ↓
    ↓                                        ↓
Store user in session                        ↓
    ↓                                        ↓
Regenerate session ID                        ↓
    ↓                                        ↓
Return true                                  ↓
    ↓                                        ↓
Redirect to /                                ↓

                            ValidationException caught in index.php

                            Errors + old input stored in flash

                            Redirect back to login form

                            Form shows errors + remembered email

ELI5: The Login Flow

Imagine a nightclub with a bouncer:

  1. You show your ID (submit email + password)
  2. Bouncer checks format (is this even a real ID format?)
  3. Bouncer checks the list (is this person in our database?)
  4. Bouncer verifies photo (does the password match?)
  5. You get a wristband (session is created with your info)
  6. Bouncer changes your wristband color (session ID regenerated for security)
  7. You're in! (redirected to homepage)

If any step fails, you're sent back to the entrance (login form) with a note about what went wrong.

What Happens After Successful Login?

Once attempt() returns true and we redirect to /:

  1. The session now contains $_SESSION['user'] = ['email' => 'user@example.com']
  2. Any page protected by the Auth middleware will now allow access
  3. The session cookie is sent with every request, so the server remembers who you are
  4. This continues until:
    • You log out (session destroyed)
    • Session expires (based on PHP's session settings)
    • You close the browser (if session cookie is not persistent)

Security Notes

Why Generic Error Messages?

'No matching account found for that email address and password.'

This message doesn't tell you whether:

  • The email doesn't exist
  • The email exists but password is wrong

Why? If we said "Email not found", an attacker could:

  1. Try different emails until they find one that exists
  2. Then focus on brute-forcing that password

Generic messages make enumeration harder.

Why Regenerate Session ID?

session_regenerate_id(true);

This protects against "session fixation" attacks:

  • Attacker tricks you into using a session ID they know
  • You log in with that session ID
  • Attacker now has access to your logged-in session

By regenerating the ID after login, even if an attacker knew the old ID, it's now useless.

What's Good Here

  • Simple, easy-to-understand flow
  • Proper password verification with password_verify()
  • Session regeneration after login (security best practice)
  • Flash data makes error handling smooth
  • Generic error messages prevent user enumeration
  • CSRF protection on the form

Production Notes (Optional)

For learning, DALT keeps login small and visible. In a production app you typically also add:

  • Rate limiting (slow down brute-force attempts)
  • “Remember me” (long-lived login cookie)
  • Store a user id in session (not just email)
  • Redirect back to the page the user originally wanted

Common Mistakes to Avoid

Storing Passwords in Plain Text

// ❌ NEVER DO THIS
$user['password'] === $password

Always use password_hash() to store and password_verify() to check.

Comparing Passwords with ==

// ❌ DON'T DO THIS
password_hash($password) == $user['password']

You can't hash the input and compare hashes. Each call to password_hash() produces a different hash (because of the salt). Use password_verify().

Revealing Too Much in Errors

// ❌ BAD
if (!$user) {
    throw new Exception('Email not found');
}
if (!password_verify($password, $user['password'])) {
    throw new Exception('Wrong password');
}

This tells attackers which emails exist in your database.

Not Regenerating Session ID

// ❌ RISKY
$_SESSION['user'] = $user;
// Missing: session_regenerate_id(true);

This leaves you vulnerable to session fixation attacks.

On this page