Outside of installation and configuration issues, reader questions pertaining to user authentication and session management almost certainly rank among the most common I receive on an ongoing basis. The logic itself is pretty straightforward; however, even a simple implementation involves a number of small but important details which aren't always so easy to figure out the first time around. This tutorial serves to dispel much of the confusion by guiding you through the implementation of a simple user authentication feature which will subsequently keep the user logged in via a session.

The Login Form

Let's start with the easiest part of the feature, creating the login form. There's not much to describe here, as this form consists of just username and password fields, presented here:
<form action="login.php" enctype="application/x-www-form-urlencoded" method="post">

<div class="articlePara">
<label for="username">Your username:</label><br />
<input name="username" size="25" type="text" />
</div>

<div class="articlePara">
<label for="password">Your password:</label><br />
<input name="password" size="25" type="password" />
</div>

<div class="articlePara">
<input name="submit" type="submit" value="Login" />
</div>
</form>
When rendered to the browser the form looks like that found in Figure 1.


Figure 1. The Login Form

The MySQL Accounts Table

Next, we'll create the MySQL table used to manage the user accounts. Of course, one would presume this already exists since some sort of registration mechanism should be in place. In its simplest form this table should consist of an integer-based primary key, unique username field, and a hashed password field. Optionally, the table would also include a field denoting the last time the user logged into the website, which is useful for learning more about usage activity:
CREATE TABLE accounts (
  id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(255) NOT NULL UNIQUE,
  password CHAR(32) NOT NULL,
  last_login DATETIME NOT NULL
);

Authenticating the User

You probably noticed that the login form points to a script named login.php. In a real-world situation the form and the login script would probably be one in the same, but for the purposes of this exercise, I'll treat them separately so as not to muddy the water in terms of explaining the authentication process. A heavily commented login.php script follows; take some time to review this code, after which I'll offer a summary of key lines:
<?php

  // Sanitize incoming username and password
  $username = filter_var($_POST['username'], FILTER_SANITIZE_STRING);
  $password = filter_var($_POST['password'], FILTER_SANITIZE_STRING);

  // Connect to the MySQL server
  $db = new mysqli("localhost", "root", "jason", "phpbuilder");

  // Determine whether an account exists matching this username and password
  $stmt = $db->prepare("SELECT id FROM accounts WHERE username = ? and password = md5(?)");

  // Bind the input parameters to the prepared statement
  $stmt->bind_param('ss', $username, $password); 

  // Execute the query
  $stmt->execute();

  // Store the result so we can determine how many rows have been returned
  $stmt->store_result();

  if ($stmt->num_rows == 1) {

    // Bind the returned user ID to the $id variable
    $stmt->bind_result($id); 
    $stmt->fetch();

    // Update the account's last_login column
    $stmt = $db->prepare("UPDATE accounts SET last_login = NOW() WHERE id = ?");
    $stmt->bind_param('d', $id); 
    $stmt->execute();

    // Redirect the user to the home page
    header('Location: http://www.example.com');
  }

?>
The login.php script begins by sanitizing incoming form input using PHP's filter extension. Although the subsequent prepared statement will help to prevent any potentially malicious input from damaging or circumventing the authentication system, an additional layer of security never hurts.
Following that, we connect to the MySQL server and determine whether a row matching the provided username and password exists. Notice how MySQL's md5() function is used to first hash the incoming password. This is because the password was presumably hashed using the same md5() function at registration time. Storing passwords in plain text is never a good idea, and the md5() function is an easy way to store them in a format which can't be further exploited should an attacker somehow obtain your database.
If a matching row is located, the associated ID is obtained and subsequently used to update the row's last_login column. Notice how another MySQL function is used (NOW()) to accomplish this task.
Finally, the user is redirected to the website home page.

Adding Session Tracking

The above login mechanism works great; however, by the time the user is redirected to the home page, the website has, of course, forgotten all about the successful authentication! This is because HTTP is a stateless protocol, meaning there is no knowledge of what happened previously nor of what is about to happen. As a workaround, developers have devised a great solution known as session management, which can track a user's activity as he navigates from one page to the next. Fortunately for you, PHP excels particularly well at this capability. Therefore, let's revise the relevant part of the login.php script to use PHP's session handling feature to start a new session and then assign the user's username to a session variable. I've bolded the lines added to the revised part of the login script:
if ($stmt->num_rows == 1) {

  // Bind the returned user ID to the $id variable
  $stmt->bind_result($id); 
  $stmt->fetch();

  // Update the account's last_login column
  $stmt = $db->prepare("UPDATE accounts SET last_login = NOW() WHERE id = ?");
  $stmt->bind_param('d', $id); 
  $stmt->execute();

  session_start();

  $_SESSION['username'] = $username;
  
  // Redirect the user to the home page
  header('Location: http://www.example.com');
}
All that remains is to create the home page. The following code determines whether a session variable named username already exists, and if so provides a customized welcome message. If the variable doesn't exist, a registration and login link is provided:
<?php session_start(); ?>

<?php if (isset($_SESSION['username'])) { ?>
<p>Welcome back, <?= $_SESSION['username']; ?>!</p>
<?php } else { ?>

<p>
  <a href="register.php">Create an account</a> | 
  <a href="login.html">Login to your account</a>
</p>

<?php } ?>

Conclusion

Obviously, this solution could use a bit of additional work, notably in terms of validating the login form and properly informing the user should the login attempt fail. However a pretty slick Ajax-driven validation feature could be added to the process in order to perform the validation in real-time without ever leaving the login page. Additionally, a logout feature should be added, preferably one which integrates with the login feature in order to either leave the user logged in for a significant period of time or automatically log the user off as soon as the browser window closes. Either way, the material provided in this tutorial should be enough to help you get started exploring these powerful features!

About the Author

Jason Gilmore is founder of the publishing, training, and consulting firm WJGilmore.com. He is the author of several popular books, including "Easy PHP Websites with the Zend Framework", "Easy PayPal with PHP", and "Beginning PHP and MySQL, Fourth Edition". Follow him on Twitter at @wjgilmore.