Authentication & Login System
Learn the general flow behind login systems and build a simplified registration, login, and page-protection example in PHP.
Introduction
Almost every real web application needs some way to know who is using it. Authentication is the process of confirming that a visitor is who they claim to be, usually by checking a username or email against a password. Once that check succeeds, the application needs a way to "remember" the visitor as they move from page to page, which is where sessions come in.
In this lesson you will walk through the general shape of a login system: how a user registers, how the stored credentials get checked during login, and how a session keeps that user recognized across requests. You have already learned PDO and prepared statements, so the database side of these examples should feel familiar.
- The overall flow connecting registration, login, and sessions.
- How a registration form stores a user with a hashed password.
- How a login script checks submitted credentials against that stored hash.
- How to set $_SESSION['user_id'] on a successful login.
- How to protect a page by checking whether a session exists.
How a Login System Works
A typical login system is built from three cooperating pieces: a registration form that creates accounts, a login form that checks credentials, and a session mechanism that keeps the user "logged in" across multiple page loads. HTTP itself is stateless — every request is independent — so without a session, the server would forget who you are the instant you clicked a new link.
Passwords are never stored as plain text. Instead, a one-way hash of the password is stored, so that even if the database is ever exposed, the original passwords are not directly readable. The next lesson covers hashing in depth — here, we simply use it as part of the flow.
The Registration Flow
Registration is the step where an account is created. The form collects a username (or email) and a password, the server hashes that password, and both the username and the hash get saved as a new row in a users table.
<?php
// Assume $pdo is an existing PDO connection (see the PDO lesson)
$username = $_POST['username'];
$plainPassword = $_POST['password'];
// password_hash() is covered in detail in the next lesson.
// For now, just know it turns a plain password into a secure, one-way hash.
$hashedPassword = password_hash($plainPassword, PASSWORD_DEFAULT);
$stmt = $pdo->prepare('INSERT INTO users (username, password) VALUES (:username, :password)');
$stmt->execute([
'username' => $username,
'password' => $hashedPassword,
]);
echo 'Account created. You can now log in.';The database only ever receives $hashedPassword — the raw $plainPassword is used for one calculation and then discarded. It is never written to the database, a log file, or anywhere else.
The Login Flow
Login reverses the lookup: given a username and a submitted password, the script fetches the stored hash for that username and checks whether the submitted password matches it. It never re-hashes and compares hashes directly — instead it uses a dedicated verification function (password_verify(), covered next lesson) that understands how the hash was built.
Example: A Simplified Login Script
Here is a simplified version of a login script that ties the pieces together. It fetches the user by username, verifies the password, and — on success — starts a session and stores the logged-in user's id.
<?php
session_start();
// Assume $pdo is an existing PDO connection
$username = $_POST['username'];
$submittedPassword = $_POST['password'];
$stmt = $pdo->prepare('SELECT id, username, password FROM users WHERE username = :username');
$stmt->execute(['username' => $username]);
$user = $stmt->fetch();
if ($user && password_verify($submittedPassword, $user['password'])) {
// Credentials are correct — start tracking this user in the session
$_SESSION['user_id'] = $user['id'];
header('Location: dashboard.php');
exit;
} else {
echo 'Invalid username or password.';
}Location: dashboard.php
(The browser is redirected, and $_SESSION['user_id'] now holds the logged-in user's id for every later request.)session_start() must run before any output is sent and before $_SESSION is read or written. It is usually the very first line of any script that needs session data, right after the opening <?php tag.
Protecting Pages with Sessions
Once a user is logged in, other pages need to check that a valid session exists before showing protected content. The pattern is simple: start the session, check whether the expected session value is set, and redirect away (usually back to the login page) if it is not.
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
// No logged-in user in this session — send them to log in first
header('Location: login.php');
exit;
}
// From here on, we know a logged-in user is viewing the page
echo 'Welcome back, user #' . $_SESSION['user_id'];Because this check is just a few lines, it is common to place it at the top of every page that requires a login, or to pull it into its own file and require it at the top of each protected script.
<?php
session_start();
$_SESSION = [];
session_destroy();
header('Location: login.php');
exit;A Note on Production Security
The scripts above show the core flow, but a real system needs a lot more: input validation, rate limiting on login attempts, CSRF protection on forms, secure session cookie settings (HttpOnly, Secure, SameSite), password reset flows, and often two-factor authentication. Lesson 68 (Security Best Practices) expands on hardening an application like this. For real projects, frameworks like Laravel or Symfony provide battle-tested authentication out of the box — reimplementing it from scratch in production is rarely a good idea.
Common Mistakes
- Forgetting session_start() at the top of every script that reads or writes $_SESSION.
- Storing the plain-text password anywhere, even temporarily, in logs or session data.
- Checking isset($_SESSION['user_id']) after output has already been sent, or in the wrong order relative to session_start().
- Trusting a login form without validating and sanitizing the submitted username.
- Assuming this simplified example is safe to deploy as-is.
Best Practices
- Always call session_start() before touching $_SESSION or producing any output.
- Store only the minimum needed in a session, such as a user id — not the full user record or password.
- Redirect immediately (and call exit) after header('Location: ...') so the rest of the script never runs.
- Use prepared statements (PDO) for every query involving user input, as covered in earlier lessons.
- Treat this lesson as a foundation, and layer in real security practices before using authentication in production.
Frequently Asked Questions
Why do I need to call session_start() on every protected page?
PHP sessions are per-request. session_start() tells PHP to resume the existing session (based on the visitor's cookie) so $_SESSION is populated with whatever was stored earlier, such as user_id.
Is checking $_SESSION['user_id'] enough to secure a page?
It is enough to confirm someone is logged in, but a complete application also needs authorization checks (is this user allowed to see this specific data), CSRF protection, and other safeguards.
What happens if I skip exit after a header('Location: ...') redirect?
The rest of the script keeps running even though a redirect was sent, which can leak content or perform actions that should only happen for authenticated users. Always follow a redirect with exit.
Can I store more than the user id in the session?
Technically yes, but it is best practice to store as little as possible (usually just the id) and re-fetch other user details from the database when needed, so the session data never goes stale.
Key Takeaways
- A login system connects three steps: registration, login verification, and session tracking.
- Registration stores a hashed password, never the plain-text password.
- Login compares a submitted password against the stored hash and, on success, starts a session.
- Storing $_SESSION['user_id'] lets later pages recognize the same logged-in user.
- Protecting a page means checking for a valid session and redirecting unauthenticated visitors away.
- This lesson's scripts are simplified for learning — production systems need far more hardening.
Summary
You now understand the overall anatomy of a login system: a registration form that stores a hashed password, a login script that verifies credentials and starts a session, and protected pages that check for that session before rendering.
The next lesson zooms into the hashing functions used here — password_hash() and password_verify() — and explains exactly how they keep stored passwords safe.
- You understand the registration → login → session flow.
- You can write a simplified login script that checks a hashed password.
- You can protect a page by checking for an active session.
- You know this example is a teaching foundation, not a production-ready system.