Sentry 2 and PHP, Part 1: Authentication and access control for PHP

Authentication and access control are critical to keeping your web application secure. Sentry 2 is a framework-agnostic authentication and authorization system written in PHP. It provides built-in methods for many common authentication and authorization tasks, allowing you to efficiently and securely develop public-facing PHP web applications.

Share:

Vikram Vaswani, Founder, Melonfire

Photo of Vikram VaswaniVikram Vaswani is the founder and CEO of Melonfire, a consulting services firm with special expertise in open source tools and technologies. He is also the author of the books Zend Framework: A Beginners Guide and PHP: A Beginners Guide.



01 October 2013

Introduction

When creating a new web application, there are some bits of code you probably find yourself routinely implementing: a login form, a registration form, a password reset workflow, or a user account manager. Because these workflows are now fairly standard, 99% of the time you're going to be writing the same code with minor modifications. So, why not encapsulate it in a set of classes to save yourself some time?

That's where Sentry 2 comes in. Developed by Cartalyst and licensed under BSD-3, Sentry 2 is a "framework agnostic authentication and authorization system" written in PHP. It provides a set of classes that simplify the task of adding standard authentication and access control flows to a PHP application without compromising on security.

This two-part article series (see Part 2) introduces you to the Sentry 2 API, showing you how to integrate and use it with your PHP web application. It includes examples of setting up a registration form with email activation; implementing password reset workflows; searching for user accounts using various filters; and creating, editing, and deleting user accounts. So come on in, and let's get started!


Installation

I'll assume throughout this article that you're familiar with HTML and SQL, and that you have a working Apache/PHP/MySQL development environment. I'll also assume that you know the basics of working with classes and objects in PHP, as the Sentry 2 package and example code in this article make use of these concepts.

Sentry 2 can be used either as part of a PHP framework (FuelPHP, CodeIgniter or Laravel are supported by default) or as a stand-alone package that you can integrate into your own application, whether framework-based or not. To make the examples in this article easier to understand, I'll use procedural PHP scripts and the Composer auto-loader. However, in a real-world application, you'd probably be better off using a PHP framework and loading the Sentry 2 classes using your framework's class loader.

To use Sentry 2 with a PHP application, you need to first download and install the package and its dependencies. The easiest way to do this is with Composer, which you'll need to download and install if you don't already have it (see Resources for a link). After Composer is in place, create a working directory (which I'll refer to as $PROJECT for convenience), and then create a $PROJECT/composer.json file with the information in Listing 1.

Listing 1. Creating $PROJECT/composer.json file
{
    "name": "myapp/sentry",
    "minimum-stability": "dev",
    "require": {
        "cartalyst/sentry": "2.0.*",
        "illuminate/database": "4.0.*",
        "ircmaxell/password-compat": "1.0.*"
    }
}

Next, run Composer, as shown in Listing 2. It will begin downloading the Sentry 2 package and dependencies and setting up its auto-loader.

Listing 2. Running Composer
shell> php composer.phar self-update
shell> php composer.phar install

At the end of this process, your $PROJECT/vendor/ directory should look something like Figure 1.

Figure 1. Directory structure after Composer update
Directory structure with vendor as top level, then cartalyst, composer, illuminate, ircmaxell, and nesbot as directories.

The next step is to set up the database tables to store user, group, and permission information. Sentry 2 ships with an SQL file containing the necessary commands to set up these tables. You'll find it in $PROJECT/vendor/cartalyst/sentry/schema/mysql.sql. To use it, first create an empty MySQL database:

mysql> CREATE DATABASE appdata;

Then, import the table schema into MySQL using a command like the following command.

shell> mysql -D appdata -u user -p < mysql.sql

Check that the tables were successfully created using a SHOW TABLES command.

mysql> SHOW TABLES;

You should see output like that shown in Figure 2.

Figure 2. Database tables used by Sentry 2
DOS window showing tables in appdata: groups, migration, throttle, users, user-groups.

IBM Security Identity Manager

You can integrate the Sentry 2 user registry with other user registries in your enterprise with an IBM Security Identity Manager adapter. You can find out more about IBM Security Identity Manager and other products from IBM Security Systems in the "When Millions Need Access" white paper.

Most of the examples in this article use the 'users' table. Use the DESC command to see the structure of this table.

mysql> DESC users;

Figure 3 illustrates the structure of this table.

Figure 3. Structure of Sentry 2 users table
DOS window showing field, type, null, key, default, extra values in columns.

To get things rolling and make it easier to work with the examples that follow, you should also insert a single user record, as shown in Listing 3.

Listing 3. Inserting a single user record
INSERT INTO `users` (`id`, `email`, `password`, `permissions`, `activated`, 
 `activation_code`, `activated_at`, `last_login`, `persist_code`, 
 `reset_password_code`, `first_name`, `last_name`, `created_at`, `updated_at`) 
 VALUES
 (1, 'vikram@example.com', '$2y$10$GbX.pVNcGdlDfyDudmE.U.PF7c/
  ZajWhKLI9VZ23Ut.mXbQteDBZG', NULL, 1, NULL, NULL, NULL, NULL, NULL, 
  'Example', 'User', '2013-09-12 11:28:31', '2013-09-12 11:28:31');

Note that the SQL query in Listing 3 creates a user record with an encrypted password. In the Sentry 2 system, each user must have a unique email address.


Getting started with Sentry 2

The quickest way to learn how Sentry 2 works is with an example. So, I'll dive right into the code with something simple: user authentication. Consider Listing 4, which illustrates the basic process.

Listing 4. User authentication
<?php
// set up autoloader
require ('vendor\autoload.php');

// configure database
$dsn = 'mysql:dbname=appdata;host=localhost';
$u = 'sentry';
$p = 'g39ejdl';
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(
  new PDO($dsn, $u, $p));
);
  
// set login credentials
$credentials = array(
  'email'    => 'vikram@example.com',
  'password' => 'guessme',
);

// authenticate
try {
  $currentUser = Cartalyst\Sentry\Facades\Native\Sentry::
    authenticate($credentials, false);
  echo 'Logged in as ' . $currentUser->getLogin();
} catch (Exception $e) {
  echo 'Authentication error: ' . $e->getMessage();
}
?>

Listing 4 begins by loading the Composer auto-loader script, which takes care of pulling in Sentry 2 components as needed. Then, the Sentry facade's setupDatabaseResolver() method is used to connect Sentry 2 with the MySQL database set up earlier by passing it a PDO object with a DSN and database credentials. These preliminary steps are common to every usage of Sentry 2.

After the database connection is initialized, the authenticate() method is used to actually perform authentication. This method accepts an array containing the user's email address and password and checks this information against the information in the database. If the credentials match, a new User object is created to represent the currently logged-in user, and the user information is also stored in the session. User object methods, like getLogin(), getPermissions(), isActivated() and more, can now be used to retrieve specific information about the user.


Logging in and out

With this information at hand, it's a quick job to adapt Listing 4 to create a login/logout framework for your web application. Listing 5 shows a traditional login form.

Listing 5. Login form
<html>
<head></head>
<body> 
  <h1>Login</h2>
  <form action="login.php" method="post">
    Username: <input type="text" name="username" /> <br/>
    Password: <input type="password" name="password" /> <br/>
    <input type="submit" name="submit" value="Log In" />
  </form>
</body>
</html>

Input entered into this form can be submitted to Listing 6, which uses Sentry 2 to validate credentials and authenticate the user.

Listing 6. User authentication and login
<?php
// set up autoloader
require ('vendor\autoload.php');

// configure database
$dsn = 'mysql:dbname=appdata;host=localhost';
$u = 'sentry';
$p = 'g39ejdl';
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(
  new PDO($dsn, $u, $p));
);

// check for form submission
if (isset($_POST['submit'])) {
  try {
    // validate input
    $username = filter_var($_POST['username'], FILTER_SANITIZE_EMAIL);
    $password = strip_tags(trim($_POST['password']));
    
    // set login credentials
    $credentials = array(
      'email'    => $username,
      'password' => $password,
    );

    // authenticate
    $currentUser = Cartalyst\Sentry\Facades\Native\Sentry::
      authenticate($credentials, false);
    echo 'Logged in as ' . $currentUser->getLogin(); 
  } catch (Exception $e) {
    echo 'Authentication error: ' . $e->getMessage();
  }
}
?>

Typically, if a user is already logged in, you'll want to display this information when the user visits any of your application pages, rather than redisplaying the login form. Because Sentry 2 automatically stores the User object in the session after authentication, it's possible to check for its presence and conditionally display either the login form or the user's logged-in status. Consider Listing 7, which merges Listing 5 and Listing 6 and adds the necessary logic.

Listing 7. User authentication with conditional login form display
<?php
// set up autoloader
require ('vendor\autoload.php');

// configure database
$dsn      = 'mysql:dbname=appdata;host=localhost';
$u = 'sentry';
$p = 'g39ejdl';
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(
  new PDO($dsn, $u, $p));

// if form submitted
if (isset($_POST['submit'])) {
  try {
    // validate input
    $username = filter_var($_POST['username'], FILTER_SANITIZE_EMAIL);
    $password = strip_tags(trim($_POST['password']));
    
    // set login credentials
    $credentials = array(
      'email'    => $username,
      'password' => $password,
    );

    // authenticate
    // if authentication fails, capture failure message
    Cartalyst\Sentry\Facades\Native\Sentry::authenticate($credentials, false);    
  } catch (Exception $e) {
    $failMessage = $e->getMessage();
  } 
}

// check if user logged in
if (Cartalyst\Sentry\Facades\Native\Sentry::check()) {
  $currentUser = Cartalyst\Sentry\Facades\Native\Sentry::getUser();
}
?>    
<html>
<head></head>
<body> 
  <?php if (isset($currentUser)): ?>
  Logged in as <?php echo $currentUser->getLogin(); ?>
  <a href="logout.php">[Log out]</a>
  <?php else: ?>
  <h1>Log In</h1>
  <div><?php echo (isset($failMessage)) ? 
    $failMessage : null; ?></div> 
  <form action="login.php" method="post">
    Username: <input type="text" name="username" /> <br/>
    Password: <input type="password" name="password" /> <br/>
    <input type="submit" name="submit" value="Log In" />
  </form>
  <?php endif; ?>
</body>
</html>

Listing 7 uses two additional Sentry 2 methods: check() is a helper method to check if the user is logged in, and getUser() returns the User object representing the currently logged-in user. Using these methods, it's quite easy to display appropriate information to the user based on current authentication status.

The final piece is logging out. This is very simple, just call the logout() method to destroy the User object in the session and log the user out. Listing 8 has example code.

Listing 8. User de-authentication and logout
<?php
// set up autoloader
require ('vendor\autoload.php');

// configure database
$dsn      = 'mysql:dbname=appdata;host=localhost';
$u = 'sentry';
$p = 'g39ejdl';
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(
  new PDO($dsn, $u, $p));

// log user out
Cartalyst\Sentry\Facades\Native\Sentry::logout();
?>

Securing password resets

If a user forgets his or her password, they'll need a way to reset it. The typical process here is to generate and send a unique code to the user's email address. The user then visits the website, passes back the code to confirm the password reset request, and submits a new password.

Sentry 2 comes with various methods that simplify this process:

  • The getResetPasswordCode() method generates a unique password reset code for a user.
  • The checkResetPasswordCode() method validates a password reset code submitted by a user.
  • The attemptResetPassword() attempts to reset a user's password to a new value by validating the accompanying password reset code.

Let's see this in action. To begin, create a reset password form, which asks the user to enter his or her email address (Listing 9).

Listing 9. Password reset request form
<html>
<head></head>
<body> 
  <h1>Reset Password</h2>
  <form action="reset-request.php" method="post">
    Email address: <br/>
    <input type="text" name="email" /> <br/>
    <input type="submit" name="submit" value="Submit Request" />
  </form>
</body>
</html>

The email address submitted by the user goes to a PHP script that initializes the Sentry 2 package, verifies the format of the address supplied, and then attempts to find the corresponding user record (Listing 10).

Listing 10. Password reset code generation
<?php 
if (isset($_POST['submit'])) {
  // set up autoloader
  require ('vendor\autoload.php');

  // configure database
  $dsn      = 'mysql:dbname=appdata;host=localhost';
  $u = 'sentry';
  $p = 'g39ejdl';
  Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(
    new PDO($dsn, $u, $p));
  
  // validate input and find user record
  // send reset code by email to user
  try {
    $email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
    $user = Cartalyst\Sentry\Facades\Native\Sentry::findUserByCredentials(array(
      'email' => $email
    ));
    
    $code = $user->getResetPasswordCode();

    $subject = 'Your password reset code';
    $message = 'Code: ' . $code;
    $headers = 'From: webmaster@example.com';
    if (!mail($email, $subject, $message, $headers)) {
      throw new Exception('Email could not be sent.');
    }    
    
    echo 'Password reset code sent.';   
    exit;
  } catch (Exception $e) {
    echo $e->getMessage();
    exit;
  }
}
?>

Listing 10 introduces the findUserByCredentials() method, which searches the user database for an account matching the supplied credentials and returns the corresponding User object if a match is found. The User object's getResetPasswordCode() method is then used to generate a unique reset code for the user, and the mail() function is then used to send this code to the user's email address.

After the user receives the password reset code, he or she needs to revisit your web application, use the supplied code to confirm the request, and provide a new password. Listing 11 is an example of the form the user might see in this case.

Listing 11. Password reset form
<html>
<head></head>
<body> 
  <h1>Reset Password</h2>
  <form action="reset-password.php" method="post">
    Email address: <br/>
    <input type="text" name="email" /> <br/>
    Reset code: <br/>
    <input type="text" name="code" /> <br/>
    New password: <br/>
    <input type="password" name="password" /> <br/>
    New password (repeat): <br/>
    <input type="password" name="password-repeat" /> <br/>
    <input type="submit" name="submit" value="Change Password" />
  </form>
</body>
</html>

When this form is submitted, the processing script needs to check the reset code supplied by the user against the code recorded in the user database, using the email address as key. Assuming a match, the user's password needs to be updated to the new value supplied in the form. Listing 12 demonstrates the necessary business logic using Sentry 2's built-in methods.

Listing 12. Password reset confirmation
<?php 
if (isset($_POST['code']) && $_POST['email']) {

  // set up autoloader
  require ('vendor\autoload.php');

  // configure database
  $dsn      = 'mysql:dbname=appdata;host=localhost';
  $u = 'sentry';
  $p = 'g39ejdl';
  Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(
    new PDO($dsn, $u, $p));

  // find user by email address
  // attempt password reset
  try {
    $code = strip_tags($_POST['code']);
    $email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
    $password = htmlentities($_POST['password']);
    $password_repeat = htmlentities($_POST['password-repeat']);
    if ($password != $password_repeat) {
      throw new Exception ('Passwords do not match.');
    }
    
    $user = Cartalyst\Sentry\Facades\Native\Sentry::findUserByCredentials(array(
      'email' => $email
    ));
    
    if ($user->checkResetPasswordCode($code)) {
      if ($user->attemptResetPassword($code, $password)) {
        echo 'Password successfully reset.';
        exit;
      } else {
        throw new Exception('User password could not be reset.');  
      }
    } else {
      throw new Exception('User password could not be reset.');  
    }
  } catch (Exception $e) {
    echo $e->getMessage();
    exit;
  }
}
?>

Listing 12 begins by setting up the Sentry 2 object and its database connection. It then checks the input variables submitted with the request, sanitizing them, checking the email address format, and verifying that the two passwords supplied are identical. If all these tests pass, the script uses the findUserByCredentials() method to produce a User object corresponding to the supplied email address, then invokes the object's checkResetPasswordCode() to check the accuracy of the supplied code.

If the code provided by the user matches what is recorded in the database, the password reset request may be considered a genuine one. In this case, the User object's attemptResetPassword() method is passed the reset code and the new password, and this in turn updates the password recorded in the user database.

It's important to note from these code listings that although the mechanics of form generation, input validation, and code transmission do need to be manually coded, the hardest parts—generating the reset code, checking the reset code, and actually resetting the password—are all encapsulated within Sentry 2 methods, which are simply invoked as needed. This built-in function reduces the total amount of effort involved in implementing a standard password reset workflow.


Securing user registrations

The previous examples have demonstrated how to authenticate existing users. However, in most web applications you will typically also need to provide an interface to create new users. Here too, there are two options: you might wants users to self-register their accounts or you might want administrators to create their accounts—or you might want both.

Sentry 2 provides methods that enable secure implementation of these options. The common element here is the createUser() method, which accepts an array of key-value pairs and creates and persists a User object representing this information to the database. Listing 13 provides a quick example of what it looks like.

Listing 13. User account creation
<?php
// set up autoloader
require ('vendor\autoload.php');

// configure database
$dsn      = 'mysql:dbname=appdata;host=localhost';
$u = 'sentry';
$p = 'g39ejdl';
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(
  new PDO($dsn, $u, $p));

// create user record
try {

  $user = Cartalyst\Sentry\Facades\Native\Sentry::createUser(array(
      'email'    => 'test@example.com',
      'password' => 'guessme',
      'first_name' => 'Test',
      'last_name' => 'User',
      'activated' => true,
  ));
  
} catch (Exception $e) {
  echo $e->getMessage();
}
?>

In particular, note the 'activated' key in the array passed to the createUser() method. When set to false, this key saves the user record to the database but disables the user account until it is verified. This adds a much-needed element of security when building public-facing registration forms, as the user needs to additionally verify the account before being permitted to use the application.

Let's see how this works in practice, with an example registration form (Listing 14).

Listing 14. User account self-registration
<?php 
if (isset($_POST['submit'])) {
  // set up autoloader
  require ('vendor\autoload.php');

  // configure database
  $dsn      = 'mysql:dbname=appdata;host=localhost';
  $u = 'sentry';
  $p = 'g39ejdl';
  Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(
    new PDO($dsn, $u, $p));
  
  // validate input and create user record
  // send activation code by email to user
  try {
    $email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
    $fname = strip_tags($_POST['first_name']);
    $lname = strip_tags($_POST['last_name']);
    $password = strip_tags($_POST['password']);
    
    $user = Cartalyst\Sentry\Facades\Native\Sentry::createUser(array(
        'email'    => $email,
        'password' => $password,
        'first_name' => $fname,
        'last_name' => $lname,
        'activated' => false
    ));

    $code = $user->getActivationCode();
    
    $subject = 'Your activation code';
    $message = 'Code: ' . $code;
    $headers = 'From: webmaster@example.com';
    if (!mail($email, $subject, $message, $headers)) {
      throw new Exception('Email could not be sent.');
    }    
    
    echo 'User successfully registered and activation code sent.';   
    exit;
  } catch (Exception $e) {
    echo $e->getMessage();
    exit;
  }
}
?>
<html>
<head></head>
<body> 
  <h1>Register</h2>
  <form action="register.php" method="post">
    Email address: <br/>
    <input type="text" name="email" /> <br/>
    Password: <br/>
    <input type="password" name="password" /> <br/>
    First name: <br/>
    <input type="text" name="first_name" /> <br/>
    Last name: <br/>
    <input type="text" name="last_name" /> <br/>
    <input type="submit" name="submit" value="Create" />
  </form>
</body>
</html>

Listing 14 produces a form with fields for email address, password, and first and last names. A user would self-register by filling out the form and submitting it. On submission, the processing script would validate the user's input (this step is intentionally kept brief in this and other listings) and then invoke the createUser() method to create the user account without activating it.

The next step is to validate the information provided by the user. This is done by sending a unique activation code to the supplied email address and asking the recipient to validate the registration by visiting the website and entering this code. The activation code can be retrieved using the User object's getActivationCode() method. All that's left is to transmit it through email to the supplied email address.

Of course, this is only half of the puzzle. You need one additional script, this one to receive and check the activation code entered by registrants and then switch their accounts to active. Typically, the activation code would be embedded in a clickable link in the email message so that when the user clicks the link he or she is transferred to the confirmation page in your application.

Listing 15 contains the necessary code for this confirmation page, assuming that the link clicked by the user is of the form 'http://your-site.com/confirm.php?code=[code]&email=[email]'.

Listing 15. User account confirmation
<?php 
if (isset($_GET['code']) && $_GET['email']) {

  // set up autoloader
  require ('vendor\autoload.php');

  // configure database
  $dsn      = 'mysql:dbname=appdata;host=localhost';
  $u = 'sentry';
  $p = 'g39ejdl';
  Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(
    new PDO($dsn, $u, $p));

  // find user by email address
  // activate user with activation code
  try {
    $code = strip_tags($_GET['code']);
    $email = filter_var($_GET['email'], FILTER_SANITIZE_EMAIL);
    $user = Cartalyst\Sentry\Facades\Native\Sentry::findUserByCredentials(array(
      'email' => $email
    ));
    if ($user->attemptActivation($code)) {
      echo 'User activated.';
    } else {
      throw new Exception('User could not be activated.');  
    }
  } catch (Exception $e) {
    echo $e->getMessage();
  }
}
?>

There's nothing very complicated about Listing 15. It inspects the incoming request for 'code' and 'email' variables and, if present, uses the findUserByCredentials() method seen earlier to identify the user account being activated. It then calls the attemptActivation() method with the supplied activation code. If the activation code matches what's already in the database, the account is activated; if not, the activation attempt fails and the account remains inactive.

Of course, if you don't want users to go through the additional step of account activation—for example, if you're building an intranet application that is only accessible to known users, or if user accounts will only be created by trusted administrators—then you can set 'activated' to true in the call to the createUser() method and user accounts will be created as active accounts.


Searching for users

You've seen a few examples of the findUserByCredentials() method in previous listings, but this isn't the only game in town when it comes to searching for users.

  • The getUser() method returns the currently logged-in user.
  • The findUserByLogin() method returns the user matching the supplied email address.
  • The findUserByActivationCode() method returns the user matching the supplied activation code.
  • The findUserByResetPasswordCode() method returns the user matching the supplied password reset code.
  • The findAllUsers() method returns a list of all users (whether active or inactive).
  • The findAllUsersWithAccess() method returns a list of all users with a specific permission.
  • The findAllUsersInGroup() method returns a list of all users in a group (groups and permissions are discussed in the second part of this article).

These methods come in handy when you're trying to find a user based on a fragment of user information—something you'd need, for example, if you added a user search engine to your application. You can also use them when constructing administration tools—for example, a user management tool.

Let's now look at how you'd use Sentry 2 to speed up development of such a user management tool. Typically, you'd need to implement four basic functions: adding new users, listing existing users, editing existing users, and deleting users. You've already seen how the createUser() method makes it easy to implement the first one (and there's an example in the code archive as well), so let's focus on the others.

Listing 16 demonstrates how to list all user accounts in the system:

Listing 16. User account listing
<?php
// set up autoloader
require ('vendor\autoload.php');

// configure database
$dsn      = 'mysql:dbname=appdata;host=localhost';
$u = 'sentry';
$p = 'g39ejdl';
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(
  new PDO($dsn, $u, $p));

// find all users
$users = Cartalyst\Sentry\Facades\Native\Sentry::findAllUsers();
?>
<html>
  <head></head>
  <body>
    <h1>Users</h1>
    <table border="1">
      <tr>
        <td>Email address</td>
        <td>First name</td>
        <td>Last name</td>
        <td>Status</td>
        <td>Last login</td>
      </tr>
    <?php foreach ($users as $u): ?>
    <?php $userArr = $u->toArray(); ?>
      <tr>
        <td><?php echo $userArr['email']; ?></td>
        <td><?php echo isset($userArr['first_name']) ? 
          $userArr['first_name'] : '-'; ?></td>
        <td><?php echo isset($userArr['last_name']) ? 
          $userArr['last_name'] : '-'; ?></td>
        <td><?php echo ($userArr['activated'] == 1) ? 
          'Active' : 'Inactive'; ?></td>
        <td><?php echo isset($userArr['last_login']) ? 
          $userArr['last_login'] : '-'; ?></td>
        <td><a href="edit.php?id=<?php echo $userArr['id']; ?>">
          Edit</a></td>
        <td><a href="delete.php?id=<?php echo $userArr['id']; ?>">
          Delete</a></td>
      </tr>
    <?php endforeach; ?>
    </table>
    <a href="create.php">Add new user</a>
  <body>
</html>

Listing 16 uses the findAllUsers() method to return a list of all users from the Sentry 2 database. It's now easy to iterate over this collection, convert each object to an array using the toArray() method, and then display relevant bits of information in a table. Notice that each user record is accompanied with links that point to edit and delete functions and that contain the unique user id. More on these later, but in the meanwhile, Figure 4 illustrates an example of the output from Listing 16.

Figure 4. Figure 4: User account listing
User table showing columns email address, first name, last name, status, and last login filled with variables. An edit and delete link are also provided.

Editing and deleting user accounts

Sentry 2 provides built-in methods to update or delete user accounts. Consider Listing 17, which is the target of the 'delete' links in Listing 16.

Listing 17. User account deletion
<?php
if (isset($_GET['id'])) {
  // set up autoloader
  require ('vendor\autoload.php');

  // configure database
  $dsn      = 'mysql:dbname=appdata;host=localhost';
  $u = 'sentry';
  $p = 'g39ejdl';
  Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(
    new PDO($dsn, $u, $p));

  // find user by id and delete
  try {
    $id = strip_tags($_GET['id']);    
    $user = Cartalyst\Sentry\Facades\Native\Sentry::findUserById($id);
    $user->delete();
    echo 'User successfully deleted.';
  } catch (Exception $e) {
    echo 'User could not be deleted.';
  }
}    
?>

Listing 17 receives the id of the selected user as a GET request parameter, locates the corresponding record using the findUserById() method, and then calls the User object's delete() method to remove the user record from the database.

There's also a save() method, which is useful for making changes to an existing user record. Consider Listing 18, which uses it to build a simple editing tool for users.

Listing 18. User account modification
<?php
// set up autoloader
require ('vendor\autoload.php');

// configure database
$dsn      = 'mysql:dbname=appdata;host=localhost';
$u = 'sentry';
$p = 'g39ejdl';
Cartalyst\Sentry\Facades\Native\Sentry::setupDatabaseResolver(
  new PDO($dsn, $u, $p));

if (isset($_POST['submit'])) {

  try {
    $id = strip_tags($_POST['id']);    
    $user = Cartalyst\Sentry\Facades\Native\Sentry::findUserById($id);    
    $user->email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
    $user->first_name = strip_tags($_POST['first_name']);
    $user->last_name = strip_tags($_POST['last_name']);
    $user->password = strip_tags($_POST['password']);
    
    if ($user->save()) {
      echo 'User successfully updated.';
      exit;
    } else {
      throw new Exception('User could not be updated.');
    }
  } catch (Exception $e) {
    echo 'User could not be created.';
    exit;
  }

} else if (isset($_GET['id'])) {

  try {
    $id = strip_tags($_GET['id']);    
    $user = Cartalyst\Sentry\Facades\Native\Sentry::findUserById($id);
    $userArr = $user->toArray();
  } catch (Exception $e) {
    echo 'User could not be found.';
    exit;
  }
  
?>
<html>
<head></head>
<body> 
  <h1>Edit User</h2>
  <form action="<?php echo htmlentities($_SERVER['PHP_SELF']); ?>" 
    method="post">
    Email address: <br/>
    <input type="text" name="email" 
      value="<?php echo $userArr['email']; ?>" /> <br/>
    Password: <br/>
    <input type="password" name="password" 
      value="<?php echo $userArr['password']; ?>" /> <br/>
    First name: <br/>
    <input type="text" name="first_name" 
      value="<?php echo $userArr['first_name']; ?>" /> <br/>
    Last name: <br/>
    <input type="text" name="last_name" 
      value="<?php echo $userArr['last_name']; ?>" /> <br/>
    <input type="hidden" name="id" 
      value="<?php echo $userArr['id']; ?>" /> <br/>
    <input type="submit" name="submit" value="Update" />
  </form>
</body>
</html>
<?php 
}
?>

Like Listing 17, Listing 18 too receives a user id from Listing 16. It then uses this id to produce the corresponding User object, convert it to an array, and extract the necessary information to pre-fill the fields of the editing form (including a hidden field for the user id). A user can modify these fields and then submit the form back for processing.

After being submitted, the user's input is sanitized and validated, and the hidden user id is used to again retrieve the corresponding User object. The new values submitted through the form are set as properties of the User object, and the object's save() method is finally invoked to save the new values to the database.


Summary

As these examples illustrate, Sentry 2 provides a full-featured framework for managing and authenticating users within a PHP web application. However, what you've seen so far is just the tip of the iceberg: Sentry 2 also includes features for groups, permissions, and additional security features like user bans and login throttles.

Sounds interesting? Come back for the second and final part of this article, which will explain these features and a few others besides. See you then!


Download

DescriptionNameSize
Sample PHP scripts for this tutorialcode-1.zip8KB

Resources

Learn

Get products and technologies

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Security on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Security
ArticleID=946946
ArticleTitle=Sentry 2 and PHP, Part 1: Authentication and access control for PHP
publish-date=10012013