Logging in

Where are we?

This chapter is about restricting access to pages on a Web site.

You know how sessions let PHP pages share information. You’ve seen how we can store data about users and their permissions in a database table.

Now let’s create the log in system that uses that data.

This lesson’s goals

You will learn that:

  • The log in form gets a user name and password from the user.
  • It sends the data to a page that checks whether the user name and password is in the database table users.
  • If the user name and password are found, permission information is stored in the session.

Overview

We want the user to see a log in form, like this:

Louise logs in

Figure 1. Louise logs in

If the user name and password match a record in the database, then the user would see the admin menu.

Admin menu

Figure 2. Admin menu

Here are the pages we’ll create:

Pages involved in log in

Figure 3. Pages involved in log in

log-in.php shows the form. The user completes it, and clicks the submit button. log-in.php passes the data to check-log-in.php. check-log-in.php checks the database for a matching record in the users table. If it finds one, it stores some data in the session, and sends the browser to the admin menu page, admin/index.php. If there is no matching record, the browser is sent back to the log in form.

Let’s look at the code.

log-in.php

Here’s the log in form again:

Louise logs in

Figure 1 (again). Louise logs in

The form does some error checking. There are client-side checks for empty fields:

Empty field errors

Figure 4. Empty field errors

But what if the user fills in both fields with an unknown user name and password? This check cannot be done on the client-side. A server-side program has to look up the user name and password before finding that the user name/password combination is unknown.

Here’s the error reporting for a unknown user name/password combination:

Unknown user name/password error

Figure 5. Unknown user name/password error

So the page needs to:

  • Check for missing data on the client side.
  • Report errors found on the server side.

The page will also have to:

  • Skip the log in process if the user is already logged in.
  • Implement the template system.

That’s quite a lot of stuff for log-in.php to do!

Here’s the complete code. We’ll go over all the pieces.

<?php
//Show the admin menu
//Input: order(optional). Sort order for article list. GET.

//Path from this page to the site root.
$path_to_root = '..';
//Already logged in?
session_start();
if ( $_SESSION['logged in'] == 'y' ) {
  //Yes - jump to admin menu.
  header("location:$path_to_root/admin/index.php");
  exit();
}
//Title of this page.
$page_title = 'DogToys Log In';
require $path_to_root . '/library/start_page.inc';
require $path_to_root . '/library/header.inc';
require $path_to_root . '/library/left_nav.inc';
?>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
<script type="text/javascript" src="<?php print $path_to_root;?>/library/show-error-messages.js"></script>
<script type="text/javascript">
  $(document).ready(function() {
    //Initialization.
    $("#global_error_message_container").hide();
    $("#user_name").focus();
    <?php
      if ( $_GET['err'] == 'auth' ) {
        print("show_global_error_message('Sorry, user name or password not found.');");
      }
    ?>
    //Form submit event.
    $("#log_in_form").submit(function() {
      //Set flag showing everything is OK.
      var data_ok = true;

      //Check the user name field.
      var user_name = $("#user_name").val();
      if ( user_name == '' ) {
        data_ok = false;
        show_field_error_message('Sorry, you must enter a user name.', 'user_name');
        show_global_error_message("Sorry, I can't log you in.");
      }
      else {
        hide_error_message('user_name');
      }

      //Check the password field.
      var user_password = $("#user_password").val();
      if ( user_password == '' ) {
        data_ok = false;
        show_field_error_message('Sorry, you must enter a password.', 'user_password');
        show_global_error_message("Sorry, I can't log you in.");
      }
      else {
        hide_error_message('user_password');
      }

      return data_ok;
    });
  });
</script>

<div id="center_region">
  <h1><?php print $page_title; ?></h1>
  <p id="global_error_message_container" class="message_container">
    <img src="<?php print $path_to_root; ?>/library/error.png" alt="Error">
    <span id="global_error_message"/>
  </p>
  <form id="log_in_form" method="post" action="check-log-in.php">
    <p>
      User name<br>
      <input type="text" id="user_name" name="user_name" size="20"><br>
      <span id="user_name_message_container" class="message_container">
        <img src="<?php print $path_to_root; ?>/library/error.png" alt="Error">
        <span id="user_name_message"/>
      </span>
    </p>
    <p>
      Password<br>
      <input type="password" id="user_password" name="user_password" size="20"><br>
      <span id="user_password_message_container" class="message_container">
        <img src="<?php print $path_to_root; ?>/library/error.png" alt="Error">
        <span id="user_password_message"/>
      </span>
    </p>
    <p>
      <button type="submit">Log in</button>
    </p>
  </form>
</div>
<?php
require $path_to_root . '/library/footer.inc';
require $path_to_root . '/library/end_page.inc';
?>

Figure 6. log-in.php

Line 8 starts the session.

Lines 9 to 13 check whether the user has already logged in. This…

$_SESSION['logged in']

...looks for the session variable logged in. If it has the value y, the user is already logged in. The browser is sent to the admin menu immediately, skipping the log in process.

Lines 14 to 18 are part of the templating system.

Lines 20 to 62 mostly implement client-side error checking. But lines 27 to 31 are new. They show errors from the server.

Here are the pages again:

Pages involved in log in

Figure 3 (again). Pages involved in log in

check-log-in.php is the page that finds whether the user name and password are valid. If they aren’t, is sends something to log-in.php, to tell it to show an error message. But what should it send, and how?

Here’s one way to do it. When check-log-in.php finds an error, it tells the browser to jump to log-in.php. It attaches something to the URL. Here is some code from check-log-in.php :

header("location:$path_to_root/admin/log-in.php?err=auth");

See the err=auth bit? This tells log-in.php to show an authentication error. Here’s how the URL looks in a browser’s address bar:

Error flag in URL

Figure 7. Error flag in URL

When log-in.php receives this, it can show the error message.

We already have some JavaScript code in log-in.php for showing errors. It uses colors, shows an icon, and other Good Stuff. Let’s use that existing JavaScript code to report the server error.

Here’s some code from the JavaScript part of log-in.php.

$(document).ready(function() {
  //Initialization.
  $("#global_error_message_container").hide();
  $("#user_name").focus();
  <?php
    if ( $_GET['err'] == 'auth' ) {
      print("show_global_error_message('Sorry, user name or password not found.');");
    }
  ?>

Figure 8. Part of log-in.php

The JavaScript function show_global_error_message() shows an error message at the top of the page. Line 29 outputs JavaScript that would call that function:

show_global_error_message('Sorry, user name or password not found.');

The PHP in line 28 adds this JavaScript if log-in.php has err passed to it, with the value auth. Recall that GET attaches stuff to the URL.

So, if the server program check-log-in.php can’t find the user name and password in the users database table, it jumps back to log-in.php, with err set to auth.

If log-in.php gets err set to auth, it calls some JavaScript to show an error message. This is PHP writing JavaScript. Again.

The rest of log-in.php should be familiar. But there’s a new thing in line 81:

<input type="password" id="user_password" name="user_password" size="20">

An <input> field with a type of password is almost the same as one with a type of text. The only difference is that the browser doesn’t show what the user types into a password field:

Password field

Figure 9. Password field

check-log-in.php

Here are the pages again:

Pages involved in log in

Figure 3 (again). Pages involved in log in

check-log-in.php gets a user name and password from log-in.php. It looks them up in the database. If they’re found, it sets some session data, and jumps to the admin menu. If the user name and password are not found, check-log-in.php jumps back to log-in.php.

Here is the code:

<?php
//Path from this page to the site root.
$path_to_root = '..';
//Get the values the user typed.
$user_name = $_POST['user_name'];
$user_password = $_POST['user_password'];
//Validate
if ( $user_name == '' || $user_password == '') {
  header("location:$path_to_root/admin/log-in.php");
  exit();
}
//Connect to DB.
require $path_to_root . '/library/db-connect.php';
$db = new mysqli($db_host, $db_user_name, $db_password, $db_name);
if ( mysqli_connect_error() ) {
  print '<p>Error! Could not connect to the database. ';
  print 'Error message: '.mysqli_connect_error().'</p>';
  exit();
}
//Fetch article data.
$query = "select permission_add, permission_edit, permission_delete
    from users
    where binary user_name = '$user_name'
        and binary user_password = '$user_password'";
$record_set = $db->query($query);
if ( $record_set->num_rows == 1 ) {
  session_start();
  $_SESSION['logged in'] = 'y';
  $_SESSION['user name'] = $user_name;
  //Store permissions in session.
  $row = $record_set->fetch_assoc();
  $_SESSION['permission add'] = $row['permission_add'];
  $_SESSION['permission edit'] = $row['permission_edit'];
  $_SESSION['permission delete'] = $row['permission_delete'];
  //Jump to admin menu.
  header("location:$path_to_root/admin/index.php");
  exit();
}
//Authentication problem.
header("location:$path_to_root/admin/log-in.php?err=auth");
exit();
?>

Figure 10. check-log-in.php

Lines 5 and 6 get the data sent to the page:

$user_name = $_POST['user_name'];
$user_password = $_POST['user_password'];

Lines 8 to 11 make sure that the data is there:

if ( $user_name '' || $user_password '') {
   header("location:$path_to_root/admin/log-in.php");
   exit();
}

If it isn’t, the browser is sent back to log-in.php. This should never happen, of course, because of the client-side validation in log-in.php. These lines are an extra security check.

Now we need to check the database, and see if the user name and password are valid. Lines 13 to 19 connect to the database.

Lines 21 to 24 create the SQL query that will look up the user name and password:

select permission_add, permission_edit, permission_delete
from users
where binary user_name = '$user_name'
and binary user_password = '$user_password'

Two things to notice. The first is the and. The select will only return records where both conditions are true.

The second thing is the binary keyword. MySQL tests are usually not case-sensitive. So louise, Louise, and lOUise are all the same.

But we don’t want that here. Applications are more secure if user names and passwords as case-sensitive, so louise and Louise are not the same. Adding the binary keyword makes the tests case-sensitive.

Line 25 is:

$record_set = $db->query($query);

It tells MySQL to run the query, returning a record set.

If the user name and password are valid, the record set should return exactly one record. That gets tested in the next piece of code.

if ( $record_set->num_rows == 1 ) {
  session_start();
  $_SESSION['logged in'] = 'y';
  $_SESSION['user name'] = $user_name;
  //Store permissions in session.
  $row = $record_set->fetch_assoc();
  $_SESSION['permission add'] = $row['permission_add'];
  $_SESSION['permission edit'] = $row['permission_edit'];
  $_SESSION['permission delete'] = $row['permission_delete'];
  //Jump to admin menu.
  header("location:$path_to_root/admin/index.php");
  exit();
}

Figure 11. Part of check-log-in.php

The code…

$record_set->num_rows

...returns the number of rows in the record set. If one record was returned, the user name and password is valid. In that case, we want to store stuff in the session.

This line…

$_SESSION['logged in'] = 'y';

...sets logged in to y. This lets other pages easily test whether there is a logged in user. We’ll see that later.

This line…

$_SESSION['user name'] = $user_name;

...stores the user name passed into check-log-in.php in the session.

Line 31 fetches the row in the record set. Remember that we already know there is only one row.

Lines 32 to 34 get the values of the three permissions fields. They are stored in the session.

Line 36 jumps to the admin menu.

Here’s what happens if the user name/password combination is not found:

if ( $record_set->num_rows == 1 ) {
  ...
  exit();
}
//Authentication problem.
header("location:$path_to_root/admin/log-in.php?err=auth");

Figure 12. Another part of check-log-in.php

The browser is sent back to log-in.php, with a flag showing that log-in.php should show an error message.

Summary

  • The log in form gets a user name and password from the user.
  • It sends the data to a page that checks whether the user name and password is in the database table users.
  • If the user name and password are found, permission information is stored in the session.

What now?

Now we have a log in system that leaves data in the session. Let’s see how other pages use that data for security.


How to...

Lessons

User login

Log in problems? Try here


Dogs