Remembering Users: Login and User Identification in PHP

A session establishes an anonymous relationship with a particular user. Requiring users to log in to your website lets them tell you who they are. The login process typi­cally requires users to provide you with two pieces of information: one that identifies them (a username or an email address) and one that proves that they are who they say they are (a secret password).

Once a user is logged in, he can access private data, submit message board posts with his name attached, or do anything else that the general public isn’t allowed to do.

Adding user login on top of sessions has five parts:

  1. Displaying a form asking for a username and password
  2. Checking the form submission
  3. Adding the username to the session (if the submitted password is correct)
  4. Looking for the username in the session to do user-specific tasks
  5. Removing the username from the session when the user logs out

The first three steps are handled in the context of regular form processing. The vaLidate_form() function gets the responsibility of checking to make sure that the supplied username and password are acceptable. The process_form() function adds the username to the session. Example 10-15 displays a login form and adds the user­name to the session if the login is successful.

Example 10-15. Displaying a login form

require ‘FormHeLper.php’;

session_start();

if ($_SERVER[‘REQUEST_METHOD’] == ‘POST’)

{

list($errors, $input) = vaLidate_fom();

if ($errors) {

show_form($errors);

} else {

process_form($input);

}

} else {

show_form();

}

function show_form($errors = array()) {

// No defaults of our own, so nothing to pass to the

// FormHelper constructor

$form = new FormHeLperQ;

// Build up the error HTML to use later

if ($errors) {

$errorHtmL = ‘<uL><Li>’;

$errorHtmL .= impLode(‘</Li><Li>’,$errors);

$errorHtmL .= ‘</Li></uL>’;

} else {

$errorHtmL = ;

}

// This form is small, so we’ll just print out its components

// here

print <<<_FORM_

<form method=”POST” action=”{$form->encode($_SERVER[‘PHP_SELF’])}”> $errorHtml

Username: {$form->input(‘text’, [‘name’ => ‘username’])} <br/>

Password: {$form->input(‘password’, [‘name’ => ‘password’])} <br/>

{$form->input(‘submit’, [‘value’ => ‘Log In’])}

</form>

_FORM_;

}

function validate_form() {

$input = array();

$errors = array();

// Some sample usernames and passwords

$users = array(‘alice’ => ‘dog123’,

 ‘bob’ => ‘my^pwd’,

 ‘charlie’ => ‘**fun**’);

// Make sure username is valid

$input[‘username’] = $_POST[‘username’] ?? ;

if (! array_key_exists($input[‘username’], $users)) {

$errors[] = ‘Please enter a valid username and password.’;

}

// The else clause means we avoid checking the password if an invalid

// username is entered

else

{

// See if password is correct

$saved_password = $users[ $input[‘username’] ];

$submitted_password = $_POST[‘password’] ?? ;

if ($saved_password != $submitted_password) {

$errors[] = ‘Please enter a valid username and password.’;

}

}

return array($errors, $input);

} function process_form($input) {

// Add the username to the session

$_SESSION[‘username’] = $input[‘username’];

printWelcome, $_SESSION[username]“;

}

Figure 10-3 shows the form that Example 10-15 displays, Figure 10-4 shows what happens when an incorrect password is entered, and Figure 10-5 shows what happens when a correct password is entered.

Figure 10-3. Login form

Figure 10-4. Unsuccessful login

In Example 10-15, validate_fom() checks two things: whether a valid username is entered and whether the correct password was supplied for that username. Note that the same error message is added to the $errors array in either case. If you use differ­ent error messages for a missing username (such as “Username not found”) and bad passwords (such as “Password doesn’t match”), you provide helpful information for someone trying to guess a valid username and password. Once this attacker stumbles on a valid username, she sees the “Password doesn’t match” error message instead of the “Username not found” message. She then knows that she’s working with a real username and has to guess the password only. When the error messages are the same in both cases, all the attacker knows is that something about the username/password combination she tried is not correct.

If the username is valid and the right password is submitted, validate_fom() returns no errors. When this happens, tge process_form() function is called. This function adds the submitted username ($input[‘username’]) to the session and prints out a welcome message for the user. This makes the username available in the session for other pages to use. Example 10-16 demonstrates how to check for a user­name in the session in another page.

Example 10-16. Doing something special for a logged-in user

<?php

session_start();

if (array_key_exists(‘username’, $_SESSION)) {

print “Hello, $_SESSION[username].”;

} else {

print ‘Howdy, stranger.’;

}

?>

The only way a username element can be added to the $_SESSION array is by your program. So if it’s there, you know that a user has logged in successfully.

The validate_form() function in Example 10-15 uses a sample array of usernames and passwords called $users. Storing passwords without hashing them is a bad idea. If the list of unhashed passwords is compromised, then an attacker can log in as any user. Storing hashed passwords prevents an attacker from getting the actual pass­words even if she gets the list of hashed passwords, because there’s no way to go from the hashed password back to the plain password she’d have to enter to log in. Operat­ing systems that require you to log in with a password use this same technique.

A better validate_form() function is shown in Example 10-17. The $users array in this version of the function contains passwords that have been hashed with PHP’s password_hash() function. Because the passwords are stored as hashed strings, they can’t be compared directly with the plain password that the user enters. Instead, the submitted password in $input[‘password’] is checked by the password_verify() function. This function uses the information in the saved hashed password to pro­duce a hash of the submitted password in the same way. If the two hashes match, then the user has submitted the correct password and password_verify() returns true.

Example 10-17. Using hashed passwords

function validate_form() {

$input = array();

$errors = array();

// Sample users with hashed passwords

$users = array(‘alice’ =>

‘$2y$10$N47IXmT8C.sKUFXs1EBS9uJRuVV8bWxwqubcvNqYP9vcFmlSWEAbq’,

‘bob’ =>

‘$2y$10$qCczYRc7S0llVRESMqUkGeWQT4V4OQ2qkSyhnxO0c.fk.LulKwUwW’, ‘charlie’ =>

‘$2y$10$nKfkdviOBONrzZkRq5pAgOCbaTFiFI6O2xFka9yzXpEBRAXMW5mYi’);

// Make sure username is valid

if (! array_key_exists($_POST[‘username’], $users)) {

$errors[ ] = ‘Please enter a valid username and password.’;

}

else {

// See if password is correct

$saved_password = $users[ $input[‘username’] ];

$submitted_password = $_POST[‘password’] ?? ”;

if (! password_verify($submitted_password, $saved_password)) {

$errors[ ] = ‘Please enter a valid username and password.’;

}

}

return array($errors, $input);

}

Using password_hash() and password_verify() ensures that the passwords are hashed in a sufficiently secure manner and gives you the ability to strengthen that hash in the future if necessary. If you’re interested in more details about how they work, read the password_hash and password_verify pages in the online PHP Manual, or see Recipe 18.7 of PHP Cookbook, by David Sklar and Adam Trachtenberg (O’Reilly).

The password_hash()and password_verify() functions are avail­able in PHP 5.5.0 and later. If you’re using an earlier version of PHP, use the password_compat library, which provides versions of these functions.

Putting an array of users and passwords inside validate_form() makes these exam­ples self-contained. However, more typically, your usernames and passwords are stored in a database table. Example 10-18 is a version of validate_form() that retrieves the username and hashed password from a database. It assumes that a data­base connection has already been set up outside the function and is available in the global variable $db.

Example 10-18. Retrieving a username and password from a database

function validate_form() {

global $db;

$input = array();

$errors = array();

// This gets set to true only if the submitted password matches

$password_ok = false;

$input[‘username’] = $_POST[‘username’] ?? ;

$submitted_password = $_POST[‘password’] ?? ;

$stmt = $db->prepare(‘SELECT password FROM users WHERE username = ?’);

$stmt->execute($input[‘username’]);

$row = $stmt->fetch();

// If there’s no row, then the username didn’t match any rows 

if ($row) {

$password_ok = password_verify($submitted_password, $row[ ]);

}

if (! $password_ok) {

$errors[] = ‘Please enter a valid username and password.’;

}

return array($errors, $input);

}

The query that prepare() and execute() send to the database returns the hashed password for the user identified in $input[‘username’]. If the username supplied doesn’t match any rows in the database, then $row is false. If a row is returned, then password_verify() checks the submitted password against the hashed password retrieved from the database. Only if there is a row returned and the row contains a correct hashed password does $password_ok get set to true. Otherwise, an error message is added to the $errors array.

Just like with any other array, use unset() to remove a key and value from $_SESSION. This is how to log out a user. Example 10-19 shows a logout page.

Example 10-19. Logging out

session_start();

unset($_SESSION[‘username’]);

print ‘Bye-bye.’;

When the $_SESSION array is saved at the end of the request that calls unset(), the username element isn’t included in the saved data. The next time that session’s data is loaded into $_SESSION, there is no username element, and the user is once again anonymous.

Source: Sklar David (2016), Learning PHP: A Gentle Introduction to the Web’s Most Popular Language, O’Reilly Media; 1st edition.

Leave a Reply

Your email address will not be published. Required fields are marked *