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

Authentication and access control are critical to keep 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.

Introduction

IBM Security Identity and Access Management

Learn more about managing access control at Internet scale in the IBM white paper, "When Millions Need Access."

In the first part of this article, I introduced you to Sentry 2, walked you through installing and configuring it, and provided a few examples of using it to implement authentication workflows for a PHP application. Among other things, I demonstrated setting up a registration form with email activation; implementing a password reset workflow; and creating a user account manager with support for creating, editing, and deleting user accounts.

In this second and final article, I'll dive into the Sentry 2 permission model, explaining how to create groups, assign users and permissions to them, and use permission checks to selectively enable application functions. I'll also show how you can harden your application with login throttling and temporary user deactivation, and explain how you can use Sentry 2 with third-party authentication services like Google and Twitter. Come on in, and let's get started!


Assigning user permissions

First off the starting block: permissions. Sentry 2 provides a reasonably-sophisticated permissions model out of the box, supporting both direct permissions (permissions assigned directly to a user) and indirect permissions (permissions assigned to a group and inherited by users who are members of that group).

To illustrate how this works, look at Listing 1, which has a simple example of creating a user account and assigning it permissions.

Listing 1. User permission grant
<?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,
      'permissions' => array('read' => 1, 'write' => 1)
  ));
  
} catch (Exception $e) {
  echo $e->getMessage();
}
?>

As Listing 1 illustrates, it's quite simple to assign permissions to a user account: simply add a 'permissions' key to the array passed to the createUser() method and specify a collection of permissions. To assign or change permissions on existing user accounts, retrieve the corresponding User object, assign new permissions and save() the object back to the database. The permission names are completely customizable, and up to you to define as you wish. The value assigned to a permission name indicates whether it is allowed (1), denied (-1), or inherited (0).

Listing 2 updates the user account creation script you saw in the first part of this article by allowing you to additionally specify whether the user has permission to "view," "add," "edit," or "delete" other user accounts.

Listing 2. Interactive user permission grant
<?php if (isset($_POST['submit'])): ?>
<?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));
  
  // validate input and create user record
  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,
        'permissions' => $_POST['perms'],
        'activated' => true,
    ));

    echo 'User successfully created.';
    
  } catch (Exception $e) {
    echo 'User could not be created. ' . $e->getMessage();
  }
?>
<?php else: ?>
<html>
<head></head>
<body> 
  <h1>Add User</h2>
  <form action="<?php echo htmlentities($_SERVER['PHP_SELF']); ?>" 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/>
    Permissions: <br/>
    <input type="checkbox" name="perms[view]" value="1" />View
    <input type="checkbox" name="perms[add]" value="1" />Add
    <input type="checkbox" name="perms[edit]" value="1" />Edit
    <input type="checkbox" name="perms[delete]" value="1" />Delete
    <input type="submit" name="submit" value="Create" />
  </form>
</body>
</html>
<?php endif; ?>

Figure 1 illustrates what the revised user account creation form looks like. On form submission, the checked permissions are assigned to the newly-created account using the 'permissions' key.

Figure 1. Account creation form with permission grants
Fields to enter email, password, name and permissions

Validating user permissions

Assigning permissions is only the first part of the puzzle. At different points in the application, you will usually want to check that the user has the permissions needed to perform specific tasks, and deny access to users lacking sufficient privileges. Sentry 2 makes this easy with the hasAccess() method, which lets you test whether a user has access to a required permission. Consider Listing 3, which revises the user editing tool seen in the first part of this article to only display add, edit, and delete links if the logged-in user has the corresponding permission.

Listing 3. User permission check
<?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));

try {
  $currentUser = Cartalyst\Sentry\Facades\Native\Sentry::getUser();  
  if (!$currentUser->hasAccess('view')) {
    throw new Exception ('You don\'t have permission to view this page.');
  }
  
  $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>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 isset($userArr['last_login']) ? 
          $userArr['last_login'] : '-'; ?></td>
        <?php if ($currentUser->hasAccess('edit')): ?>
        <td><a href="edit.php?id=<?php echo $userArr['id']; ?>">
          Edit</a></td>
        <?php endif; ?>
        <?php if ($currentUser->hasAccess('delete')): ?>
        <td><a href="delete.php?id=<?php echo $userArr['id']; ?>">
          Delete</a></td>
        <?php endif; ?>
      </tr>
    <?php endforeach; ?>
    </table>
    <?php if ($currentUser->hasAccess('add')): ?>
    <a href="create.php">Add new user</a>
    <?php endif; ?>
  <body>
</html>
<?php
} catch (Exception $e) {
  echo $e->getMessage();
}
?>

Listing 3 sets up the Sentry 2 database connection and then uses the getUser() method to retrieve the User object corresponding to the currently logged-in user. It then uses the object's hasAccess() method at various points to decide whether the user has permission to view the list of user accounts ("view") and which editing links to display next to each account in the list ("edit," "delete," or "add").

Listing 4 offers another example, this one first checking that the logged-in user has "delete" permission before allowing account deletion.

Listing 4. User permission check
<?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));

  try {
    $currentUser = Cartalyst\Sentry\Facades\Native\Sentry::getUser();  
    if (!$currentUser->hasAccess('delete')) {
      throw new Exception ('You don\'t have permission to view this page.');
    }
  
    // find user by id and delete
    $id = strip_tags($_GET['id']);    
    $user = Cartalyst\Sentry\Facades\Native\Sentry::findUserById($id);
    $user->delete();
    echo 'User successfully deleted.';
  } catch (Exception $e) {
    echo $e->getMessage();
  }
}    
?>

Creating groups

Sentry 2 also lets you organize users into groups. This is particularly useful if you want to permit different function levels to different groups of users. Groups are created with the createGroup() method, as illustrated in Listing 5.

Listing 5. Group 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 group record
try {

  $group1 = Cartalyst\Sentry\Facades\Native\Sentry::createGroup(array(
      'name'    => 'staff',
      'permissions' => array(
        'view' => 1,
        'add' => 0,
        'edit' => 1,
        'delete' => 0
      )
  ));

  $group2 = Cartalyst\Sentry\Facades\Native\Sentry::createGroup(array(
      'name'    => 'managers',
      'permissions' => array(
        'view' => 1,
        'add' => 1,
        'edit' => 1,
        'delete' => 1
      )
  ));
  
} catch (Exception $e) {
  echo $e->getMessage();
}
?>

Listing 5 creates two new groups, named "staff" and "managers." The "staff" group has permission to only "view" and "edit," while the "managers" group has permissions to also "edit" and "delete."

The group records are stored in the 'groups' table. Use the DESC command to see the structure of this table.

mysql> DESC groups;

Figure 2 illustrates the structure of this table.

Figure 2. Structure of Sentry 2 groups table
Command window showing the structure of Sentry 2 groups table

Listing 6 demonstrates the process of adding a user to a group.

Listing 6. User assignment to group
<?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
  ));
  $group = Cartalyst\Sentry\Facades\Native\Sentry::findGroupByName('managers');
  $user->addGroup($group);    
  
} catch (Exception $e) {
  echo $e->getMessage();
}
?>

Adding a user to a group is a two-step process. First, use the findGroupByName() method to retrieve a Group object and then, invoke the User object's addGroup() method to attach the group to the user account.


Merging user and group permissions

Groups provide a convenient way to implicitly assign permissions to users; simply assign a user to a group, and the user will automatically inherit the group's permissions. Following the example in Listing 5, users belonging to the "staff" group automatically inherit the group's permissions to "view" and "edit," and users belonging to the manager's group will automatically inherit the group's permissions to also "edit" and "delete."

You can override a user's default group permissions by explicitly specifying different values when creating or updating the user account. Consider Listing 7, which illustrates this.

Listing 7. Listing 7: Group permissions override
<?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'    => 'john@example.com',
      'password' => 'guessme',
      'first_name' => 'John',
      'last_name' => 'User',
      'activated' => true,
      'permissions' => array('edit' => '-1')
  ));
  $group = Cartalyst\Sentry\Facades\Native\Sentry::findGroupByName('managers');
  $user->addGroup($group);    
  
} catch (Exception $e) {
  echo $e->getMessage();
}
?>

In Listing 7, user "john@example.com" belongs to the "managers" group and would, therefore, have all permissions by default. However, at the time of account creation, the "edit" permission has been explicitly denied to the account. As a result, the user will end up with "view," "add," and "delete" permissions only.

Listing 8 demonstrates using the getMergedPermissions() method to retrieve a user's merged direct and indirect permissions.

Listing 8. Merged user and group permissions
<?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> <br/>
  Permissions: <?php echo implode(', ', 
    array_keys($currentUser->getMergedPermissions(), '1')); ?>
  <?php else: ?>
  <h1>Log In</h1>
  <div><?php echo (isset($failMessage)) ? 
    $failMessage : null; ?></div> 
  <form 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>

Figure 3 shows the calculated permissions of "john@example.com" from Listing 8.

Figure 3. User permission display
User permission display

Sentry 2 also provides the special "superuser" permission, which works as a shortcut to assign a user "all available permissions". Listing 9 shows it in action.

Listing 9. Superuser permission grant
<?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'    => 'root@example.com',
      'password' => 'guessme',
      'first_name' => 'Root',
      'last_name' => 'User',
      'activated' => true,
      'permissions' => array('superuser' => '1')
  ));
  
} catch (Exception $e) {
  echo $e->getMessage();
}
?>

Throttling user logins

Sentry 2 also comes with an interesting feature to improve application security: automatic login throttling in the event of too many failed login attempts. There are some interesting methods to know about in this context:

  • The setAttemptLimit() method sets the maximum number of attempts to allow before throttling the login.
  • The setSuspensionTime() method sets the number of minutes the login should be throttled for.
  • The getLoginAttempts() method retrieves the number of login attempts.
  • The clearLoginAttempts() method clears the count of failed login attempts and unsuspends the login account.
  • The check() method retrieves the current status of the login, whether throttled or not.
  • The suspend() method suspends the login account, and the unsuspend() method restores it to active status.
  • The ban() method bans the login account, and the unBan() method restores it to active status.

Listing 10 shows how easy it is to add throttling to your login workflow.

Listing 10. Login throttling
<?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));

// enable throttling
$throttleProvider = Cartalyst\Sentry\Facades\Native\Sentry::getThrottleProvider();
$throttleProvider->enable();

// if form submitted
if (isset($_POST['submit'])) {
  try {
    // validate input
    $username = filter_var($_POST['username'], FILTER_SANITIZE_EMAIL);
    $password = strip_tags(trim($_POST['password']));
    
    // configure throttling
    $throttle = $throttleProvider->findByUserLogin($username);
    $throttle->setAttemptLimit(3);
    $throttle->setSuspensionTime(5);
    
    $credentials = array(
      'email'    => $username,
      'password' => $password,
    );
    Cartalyst\Sentry\Facades\Native\Sentry::authenticate($credentials, false); 
    
  } catch (Exception $e) {
    // all other authentication failures
    $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 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 10 implements the standard Sentry 2 login workflow you've seen in previous examples, with two notable differences:

  • The getThrottleProvider() method is used to retrieve an instance of the global throttle provider. This provider object serves as the primary control point for login throttling. Invoking the object's enable() method activates Sentry 2's built-in throttling feature.
  • The throttle object's findByUserLogin() method is used to retrieve an instance of the user throttle object. The object's setAttemptLimit() and setSuspensionTime() methods are used to configure the key parameters for login throttling—how many attempts, and how long to suspend.

If you now try logging in to the application with an incorrect password, your account is automatically suspended for five minutes following three failed attempts. This provides a robust response to bots and automated scripts that try to log in by attempting to guess passwords using a dictionary.


Integrating with third-party networks (Twitter)

As you've seen, Sentry 2 provides everything you need to authenticate users stored in its own database. But these days, you'll usually want your application to also support login through third-party OAuth providers, such as Google, Facebook, or Twitter.

Sentry Social 3 is an add-on component to Sentry 2 designed for integration with social networks. However, this component is only available under a commercial license and, as such, might not be the best fit for your project. And so, an alternative solution is to use open-source OAuth libraries provided (in most cases) by each third-party provider.

Relax, it's really not as complicated as it sounds. To illustrate, I'll show you a few examples of different authentication and account creation workflows by integrating Sentry 2 with two of today's most popular social networks: Twitter and Google+.

If you've ever tried logging into an application using your Twitter, Google, or Facebook credentials, you might have noticed that the application is able to access your account details on that network and use that information to pre-fill its own registration form. Listing 11 demonstrates this workflow, using two components from the Zend Framework 1.x—specifically, Zend_Oauth_Consumer and Zend_Json—to connect to the Twitter API and retrieve the authenticating user's screen name. This information is then used to populate the application's local account creation form which, when submitted, creates a new user account in the Sentry 2 database.

Listing 11 assumes that you have a working copy of the Zend Framework in your PHP include path. Note that before you can use the code in Listing 11, you must first register your application with Twitter, obtain a consumer key and secret (Figure 4 has an example), and insert this information into Listing 11 at the appropriate place. See Resources for links to the Zend Framework and to the Twitter API developer console for managing applications.

Figure 4. Twitter app console settings
Twitter app console settings
Listing 11. User authentication and profile retrieval with Twitter
<?php
session_start();
if (isset($_POST['submit'])) {
  // if form submitted
  // use input to create user account
  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));

  try {
    $email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
    $fname = strip_tags($_POST['name']);
    $password = strip_tags($_POST['password']);
    
    $user = Cartalyst\Sentry\Facades\Native\Sentry::createUser(array(
        'email'    => $email,
        'password' => $password,
        'first_name' => $fname,
    ));

    echo 'User successfully created.';    
  } catch (Exception $e) {
    echo 'User could not be created. ' . $e->getMessage();
  }

} else {
  // if form not submitted
  // connect to Twitter via Oauth
  // ask for user authorization and retrieve user's name via Twitter API
  // pre-populate account creation form
  require_once 'Zend/Loader.php';
  Zend_Loader::loadClass('Zend_Oauth_Consumer');
  Zend_Loader::loadClass('Zend_Json');

  $config = array(
    'callbackUrl' => 'http://yourhost/path/to/script.php',
    'siteUrl' => 'https://api.twitter.com/oauth',
    'consumerKey' => 'YOUR-CONSUMER-KEY',
    'consumerSecret' => 'YOUR-CONSUMER-SECRET'
  );
  $consumer = new Zend_Oauth_Consumer($config);
   
  // get access token
  if (!empty($_GET) && isset($_SESSION['TWITTER_REQUEST_TOKEN'])) {
      $token = $consumer->getAccessToken(
                   $_GET,
                   unserialize($_SESSION['TWITTER_REQUEST_TOKEN'])
               );
      $_SESSION['TWITTER_ACCESS_TOKEN'] = serialize($token);
  } 

  // fetch a request token
  if (!isset($_SESSION['TWITTER_REQUEST_TOKEN'])) {
    $token = $consumer->getRequestToken();
    $_SESSION['TWITTER_REQUEST_TOKEN'] = serialize($token);
    $consumer->redirect();
  }

  // use access token to connect to Twitter API
  // decode response and retrieve user's screen name
  // generate form with name pre-filled
  if (isset($_SESSION['TWITTER_ACCESS_TOKEN'])) {
    $token = unserialize($_SESSION['TWITTER_ACCESS_TOKEN']);
    $client = $token->getHttpClient($config);
    $client->setUri('https://api.twitter.com/1.1/account/verify_credentials.json');
    $client->setMethod(Zend_Http_Client::GET);
    $response = $client->request();     
    $data = Zend_Json::decode($response->getBody());
?>
  <html>
  <head></head>
  <body> 
    <form method="post">
    Name: <input type="text" name="name" 
      value="<?php echo $data['name']; ?>" /> <br/>
    Email address: <input type="text" name="email" value="" /> <br/>
    Password: <input type="password" name="password" value="" /> <br/>
    <input type="submit" name="submit" value="Create Account" />
    </form>
  </body>
  </html>
<?php
  }
}
?>

Listing 11 begins by starting a new session, which will be used as the storage container for the OAuth access and request tokens received during the authentication process. It then uses the Zend Framework auto-loader to load the required classes and initialize a new Zend_Oauth_Consumer object. The code then implements the standard OAuth workflow, first obtaining a request token and then redirecting the user to Twitter to log in, authenticate, and obtain an access token.

With the access token in hand, Listing 11 is now able to make an authenticated request to the /verify_credentials endpoint of the Twitter API. The response to this request is a JSON document containing a brief user profile, including the user's screen name. It's now easy to decode the JSON data, extract the user's screen name from it, and use this information to pre-populate the account creation form. The user now needs to simply add in his or her email address and submit the form, which will launch Sentry 2 and create a user account using the createUser() method you've seen previously.

Figure 5 displays examples of the Twitter authentication in progress and the resulting account creation form with the user's screen name already populated from the Twitter API.

Figure 5. Twitter authentication process
Twitter authentication process

In case you're wondering why Listing 11 can't also pre-populate the account creation form with the user's email address, the reason is simple: Twitter doesn't expose email addresses through its API. Because the email address is the primary account identifier used by Sentry 2, this information must be manually requested and entered by the user to complete the account creation process. Don't worry, though, this is a Twitter-specific constraint and Listing 12, which uses Google+, offers a different take on this.


Integrating with third-party networks (Google)

Google+ is becoming increasingly popular as a social network, and Google also provides a robust implementation of a PHP OAuth client. This client, which is freely downloadable, makes it possible to connect to all of the Google APIs, including the Google+ API. However, before you can use the code in the following listings, you must first register your application with Google, obtain a client ID and secret, and insert this information at the appropriate places in each listing (Figure 6 has an example). See Resources for links to the Google PHP OAuth client and to the Google API console.

Figure 6. Google app console settings
Google app console settings with client secret greyed out

Listing 12 provides an example of authenticating against the Google OAuth service, retrieving the authenticated user's name and email address from the Google+ API, and automatically creating a new Sentry 2 user account using this information.

Listing 12. User authentication and account creation with Google
<?php
// load required classes
require_once 'vendor/google-api-php-client/src/Google_Client.php';
require_once 'vendor/google-api-php-client/src/contrib/Google_Oauth2Service.php';
require_once 'vendor/google-api-php-client/src/contrib/Google_PlusService.php';
require_once '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));

// start session
session_start();

// initialize OAuth 2.0 client
// set scopes
$client = new Google_Client();
$client->setApplicationName('Project X');
$client->setClientId('YOUR-CLIENT-ID');
$client->setClientSecret('YOUR-CLIENT-SECRET');
$client->setRedirectUri('http://yourhost/path/to/script.php');
$client->setScopes(array(
  'https://www.googleapis.com/auth/userinfo.email', 
  'https://www.googleapis.com/auth/userinfo.profile',
  'https://www.googleapis.com/auth/plus.login'
));
$oauth2Service = new Google_Oauth2Service($client);
$plusService = new Google_PlusService($client);

// if code received, authenticate and store token in session
if (isset($_GET['code'])) {
  $client->authenticate();
  $_SESSION['token'] = $client->getAccessToken();
  $redirect = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'];
  header('Location: ' . filter_var($redirect, FILTER_SANITIZE_URL));
  exit;
}

// if token available in session, set token in client
if (isset($_SESSION['token'])) {
  $client->setAccessToken($_SESSION['token']);
}

// if token available in client, access API
// get user name and email address
// generate random password
// create user in Sentry2 database and notify user with email
if ($client->getAccessToken()) {
  $userinfo = $oauth2Service->userinfo->get();
  $profile = $plusService->people->get('me');
  $email = filter_var($userinfo['email'], FILTER_SANITIZE_EMAIL);
  $fname = $profile['name']['givenName'];
  $lname = $profile['name']['familyName'];
  $password = substr(md5(rand()),0,7);
  
  try {    
    $user = Cartalyst\Sentry\Facades\Native\Sentry::createUser(array(
      'email'    => $email,
      'password' => $password,
      'first_name' => $fname,
      'last_name' => $lname,
      'activated' => true,
    ));
    
    $body = <<<EOM
Your account was successfully created. Please log in with the credentials below:
Email: $email
Password: $password
EOM;
    @mail($email, 'Account created successfully', $body);    
  } catch (Exception $e) {
    $failMessage = $e->getMessage();
  }
  
  $_SESSION['token'] = $client->getAccessToken();
  
} else {
  $authUrl = $client->createAuthUrl();
}
?>
<html>
<head></head>
<body> 
  <?php if (isset($authUrl)): ?>
    <a href='<?php echo $authUrl; ?>'>Register with Google+</a>
  <?php else: ?>
    <div><?php echo (isset($failMessage)) ? 
      $failMessage : "Account created for '$fname $lname'. 
      Credentials: $email / $password"; ?></div>  
  <?php endif; ?>
</body>
</html>

Listing 12 begins by loading Google's PHP OAuth library, setting up the Sentry 2 database connection and starting a new PHP session. It then initializes the Google OAuth client, sets the client ID and secret obtained from the Google API console, and, importantly, defines the scopes for the client. Finally, it initializes two service objects, one for the OAuth service and one for the Google+ service, and creates an authentication URL using the client's createAuthUrl() method.

Following the standard OAuth workflow, clicking the link to the authentication URL takes the user to a Google authentication page (Figure 7). After the user authenticates the application and confirms the data it has access to, an access token is generated and stored in the session. This access token gives the client access to selected Google APIs.

Figure 7. Google authentication process
Google authentication process

With the access token, the service objects can obtain profile information for the authenticating user, such as the first name, last name, and email address. This information is then combined with an auto-generated random password to create and activate a new user account in the Sentry 2 database. The account information, including the auto-generated password, is then emailed to the user using PHP's mail() function.

A variant of this approach is to create the user account from third-party network information and then manually log the user in to the application. Listing 13 is a revised version of Listing 12 that follows this approach.

Listing 13. User authentication and login with Google
<?php
// load required classes
require_once 'vendor/google-api-php-client/src/Google_Client.php';
require_once 'vendor/google-api-php-client/src/contrib/Google_Oauth2Service.php';
require_once 'vendor/google-api-php-client/src/contrib/Google_PlusService.php';
require_once '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));

// start session
session_start();

// initialize OAuth 2.0 client
// set scopes
$client = new Google_Client();
$client->setApplicationName('Project X');
$client->setClientId('YOUR-CLIENT-ID');
$client->setClientSecret('YOUR-CLIENT-SECRET');
$client->setRedirectUri('http://yourhost/path/to/script.php');
$client->setScopes(array(
  'https://www.googleapis.com/auth/userinfo.email', 
  'https://www.googleapis.com/auth/userinfo.profile',
  'https://www.googleapis.com/auth/plus.login'
));
$oauth2Service = new Google_Oauth2Service($client);
$plusService = new Google_PlusService($client);

// if code received, authenticate and store token in session
if (isset($_GET['code'])) {
  $client->authenticate();
  $_SESSION['token'] = $client->getAccessToken();
  $redirect = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'];
  header('Location: ' . filter_var($redirect, FILTER_SANITIZE_URL));
  exit;
}

// if token available in session, set token in client
if (isset($_SESSION['token'])) {
  $client->setAccessToken($_SESSION['token']);
}

// if logout request, destroy session and revoke token
if (isset($_REQUEST['logout'])) {
  unset($_SESSION['token']);    
  $client->revokeToken();
  Cartalyst\Sentry\Facades\Native\Sentry::logout();
}

// if token available in client, access API
// get user name and email address
// check if user present in Sentry2 database and auto-login
// or create new user account and log user in
if ($client->getAccessToken()) {
  $userinfo = $oauth2Service->userinfo->get();
  $profile = $plusService->people->get('me');
  $email = $userinfo['email'];
  $fname = $profile['name']['givenName'];
  $lname = $profile['name']['familyName'];
  
  try {
    try {    
      $user = Cartalyst\Sentry\Facades\Native\Sentry::findUserByLogin($email);
    } catch (Cartalyst\Sentry\Users\UserNotFoundException $e) {
      $password = substr(md5(rand()),0,7);
      $user = Cartalyst\Sentry\Facades\Native\Sentry::createUser(array(
          'email'    => $email,
          'password' => $password,
          'first_name' => $fname,
          'last_name' => $lname,
          'activated' => true,
      ));
      $createdMessage = "Account created for '$fname $lname'. 
        Credentials: $email / $password";
    }

    Cartalyst\Sentry\Facades\Native\Sentry::login($user, false); 
    
    if (Cartalyst\Sentry\Facades\Native\Sentry::check()) {
      $currentUser = Cartalyst\Sentry\Facades\Native\Sentry::getUser();
    }
  } catch (Exception $e) {
    $failMessage = $e->getMessage();
  }

  $_SESSION['token'] = $client->getAccessToken();
    
} else {
  $authUrl = $client->createAuthUrl();
}
?>
<html>
<head></head>
<body> 
  <?php if (isset($authUrl)): ?>
    <a href='<?php echo $authUrl; ?>'>Log in with Google+</a>
  <?php else: ?>
    <div><?php echo (isset($failMessage)) ? 
      $failMessage : null; ?></div>  
    <div><?php echo (isset($createdMessage)) ? 
      $createdMessage : null; ?></div>  
    Logged in as <?php echo $currentUser->getLogin(); ?>. 
      [<a href='?logout'>Log out</a>]
  <?php endif; ?>
</body>
</html>

Most of the code in Listing 13 is the same as that in Listing 12. The primary difference is that after the Google authentication process is complete, the script checks if there already exists a Sentry 2 account with the same email address and creates one if it doesn't exist. After the user account is present on the system, the Sentry 2 login() method is used to manually authenticate and log the user in to the application. There's also support for a logout workflow, which logs the user out of the application and also revokes the Google API access token.


Summary

As these examples illustrate, Sentry 2 provides a full-featured framework for authentication and granular access control within a PHP web application. By adding easily available open-source libraries, it's possible to integrate it with third-party social networks and authentication providers, thereby satisfying a wide variety of common use cases with minimal development effort. Try it out for yourself the next time you have a web application to build, and see what you think!


Download

DescriptionNameSize
Sample PHP scripts for this tutorialcode-2.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


  • Bluemix Developers Community

    Get samples, articles, product docs, and community resources to help build, deploy, and manage your cloud apps.

  • Security

    Pragmatic, intelligent, risk-based IT Security practices.

  • DevOps Services

    Software development in the cloud. Register today to create a project.

  • IBM evaluation software

    Evaluate IBM software and solutions, and transform challenges into opportunities.

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