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:
- Create an
Authenticatorinstance - Call
attempt()with the email and password - If it returns
true, login succeeded → redirect to homepage - 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 emailELI5: The Login Flow
Imagine a nightclub with a bouncer:
- You show your ID (submit email + password)
- Bouncer checks format (is this even a real ID format?)
- Bouncer checks the list (is this person in our database?)
- Bouncer verifies photo (does the password match?)
- You get a wristband (session is created with your info)
- Bouncer changes your wristband color (session ID regenerated for security)
- 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 /:
- The session now contains
$_SESSION['user'] = ['email' => 'user@example.com'] - Any page protected by the
Authmiddleware will now allow access - The session cookie is sent with every request, so the server remembers who you are
- 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:
- Try different emails until they find one that exists
- 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'] === $passwordAlways 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.