Contents


Manage and authenticate users easily in IBM Bluemix applications with PHP and the Passport service, Part 2

Add role-based access and password recovery to your PHP application

Use the Passport API to enhance user management for your PHP app

Comments

Content series:

This content is part # of # in the series: Manage and authenticate users easily in IBM Bluemix applications with PHP and the Passport service, Part 2

Stay tuned for additional content in this series.

This content is part of the series:Manage and authenticate users easily in IBM Bluemix applications with PHP and the Passport service, Part 2

Stay tuned for additional content in this series.

In Part 1, I explained the basics of the Passport API and walked you through the process of integrating Passport with a PHP application that runs on Bluemix. By outsourcing your application's user management and authentication to Passport, you can quickly and efficiently add user registration, login/logout, and activation/deactivation workflows to a PHP application.

But as I said then, that is just the tip of the iceberg. With the Passport API, you can enhance user management within your PHP application by adding features such as modifying and deleting user accounts, storing custom user profile attributes, restricting access to features based on user roles, and providing a recovery system for forgotten passwords. As I promised at the end of Part 1, I'll cover all of those features here. Let's go!

Bluemix's Passport service makes adding full-featured user management, authentication, and role-based access to your application quick and easy.

What you will need

See "What you will need" in Part 1 for everything you will need to follow along in this tutorial. Be sure to note the requirements regarding the Inversoft License Agreement and the Bluemix terms of use.

Try the demoGet the code on GitHub

Step 1: Support custom attributes in user records

The user registration form you saw in Part 1 was fairly basic: All it asked for was the user's name, email address, and password. In reality, you're probably going to need a few more pieces of information from your users when they register—perhaps their address, phone number, and even payment information.

The good news is that Passport supports custom attributes for user records, allowing you to request (and save) whatever information you deem necessary to complete your user records. This information is stored in the Passport service together with the other mandatory user information, and can be accessed using the Passport API.

To illustrate how this works, go back to the previous $APP_ROOT/views/users-save.phtml file and update the registration form to include three additional fields—for city, occupation, and mobile phone:

...
<form method="post" 
  action="<?php echo $data['router']->pathFor('admin-users-save'); ?>">
  <div class="form-group">
    <label for="fname">First name</label>
    <input type="text" class="form-control" id="fname" name="fname">
  </div>
  <div class="form-group">
    <label for="lname">Last name</label>
    <input type="text" class="form-control" id="lname" name="lname">
  </div>
  <div class="form-group">
    <label for="email">Email address</label>
    <input type="text" class="form-control" id="email" name="email">
  </div>
  <div class="form-group">
    <label for="password">Password</label>
    <input type="password" class="form-control" id="password" name="password">
  </div> 
  <div class="form-group">
    <label for="city">City</label>
    <input type="text" class="form-control" id="city" name="city">
  </div>
  <div class="form-group">
    <label for="occupation">Occupation</label>
    <input type="text" class="form-control" id="occupation" name="occupation">
  </div>
  <div class="form-group">
    <label for="mobilePhone">Mobile phone (with country code)</label>
    <input type="text" class="form-control" id="mobilePhone" name="mobilePhone">
  </div>
  <div class="form-group">
    <button type="submit" name="submit" class="btn btn-default">Save</button>
  </div>
</form>  
...

Here's what the revised user registration form looks like:

Figure 1. User registration form
User registration form
User registration form

Next, update the corresponding Slim callback to validate these new inputs and add them to the JSON document that's sent to the /api/user/registration API method.

<?php
// Slim application initialization - snipped

// user form processor
$app->post('/admin/users/save', function (Request $request, Response $response) {
  // get configuration
  $config = $this->get('settings');

  // get input values
  $params = $request->getParams();
  
  // validate input
  if (!($fname = filter_var($params['fname'], FILTER_SANITIZE_STRING))) {
    throw new Exception('ERROR: First name is not a valid string');
  }
  
  if (!($lname = filter_var($params['lname'], FILTER_SANITIZE_STRING))) {
    throw new Exception('ERROR: Last name is not a valid string');
  }
  
  $password = trim(strip_tags($params['password']));
  if (strlen($password) < 8) {
    throw new Exception('ERROR: Password should be at least 8 characters long');      
  }
      
  $email = filter_var($params['email'], FILTER_SANITIZE_EMAIL);
  if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
    throw new Exception('ERROR: Email address should be in a valid format');
  }
  
  if (!($city = filter_var($params['city'], FILTER_SANITIZE_STRING))) {
    throw new Exception('ERROR: City is not a valid string');
  }
  
  if (!($occupation = filter_var($params['occupation'], FILTER_SANITIZE_STRING))) {
    throw new Exception('ERROR: Occupation is not a valid string');
  }

  $mobilePhone = filter_var($params['mobilePhone'], FILTER_SANITIZE_NUMBER_INT);
  if (filter_var($mobilePhone, FILTER_VALIDATE_FLOAT) === false) {
    throw new Exception('ERROR: Mobile phone is not a valid number');
  }

  // generate array of user data
  $user = [
    'registration' => [
      'applicationId' => $config['passport_app_id'],
    ],
    'skipVerification' => true,
    'user'  => [
      'email' => $email,
      'firstName' => $fname,
      'lastName' => $lname,
      'password' => $password,
      'mobilePhone' => $mobilePhone,
      'data' => [
        'attributes' => [
          'city' => $city,
          'occupation' => $occupation,
        ]
      ]
    ]
  ];
  
  // encode user data as JSON
  // POST to Passport API for user registration and creation
  $apiResponse = $this->passport->post('/api/user/registration', [
    'body' => json_encode($user),
    'headers' => ['Content-Type' => 'application/json'],
  ]);

  // if successful, display success message
  // with user id
  if ($apiResponse->getStatusCode() == 200) {
    $json = (string)$apiResponse->getBody();
    $body = json_decode($json);
    $response = $this->view->render($response, 'users-save.phtml', [
      'router' => $this->router, 'user' => $body->user
    ]);
    return $response;
 }
});

// other callbacks

Notice the additional user.data.attributes key in the JSON document that's sent to the Passport API; this key is designed to hold all the custom attributes you wish to store for your application users. In this example, it stores the user's city and occupation, but you can add other attributes as well, depending on your requirements. The user's mobile phone number is stored separately in the user.mobilePhone key, which is a pre-defined key already supported by the Passport API.

To see the additional requests in action, create a new user account using the registration form, remembering to enter the additional information requested. Then, browse to the Passport front-end URL, sign in with your administration credentials, and view the new user record in the Passport service dashboard to verify that the information was successfully saved. Here's an example of what you should see:

Figure 2. User record with custom attributes
User record with custom attributes
User record with custom attributes

Step 2: Enable user profile modification

Typically, an application will also allow registered users the ability to modify the information stored in their user profiles. Although in this case user information is stored with Passport and not in the application database, it's still quite easy to retrieve and modify it using the Passport API.

The simplest way to do this is by reusing the existing user registration form and updating it to also work as a user profile modification form. Begin with the callback handler for the /admin/users/save route. This route displays the registration form and should be updated to accept an optional user identifier as a route parameter, as shown below:

<?php
// Slim application initialization - snipped

// user form handler
$app->get('/admin/users/save[/{id}]', function (Request $request, 
  Response $response, $args) {
  $user = [];
  
  if (isset($args['id'])) {
    // sanitize input
    if (!($id = filter_var($args['id'], FILTER_SANITIZE_STRING))) {
      throw new Exception('ERROR: User identifier is not a valid string');
    }
    
    $apiResponse = $this->passport->get('/api/user/' . $id);
    
    if ($apiResponse->getStatusCode() == 200) {
      $json = (string)$apiResponse->getBody();
      $body = json_decode($json);
      $user = $body->user;
    } 
  }

  $response = $this->view->render($response, 'users-save.phtml', [
    'router' => $this->router, 'user' => $user
  ]);
  return $response;
})->setName('admin-users-save');

// other callbacks

When this route is requested with the optional user identifier attached, the callback performs a request to the /api/user/USER_ID endpoint. This endpoint then returns a JSON document containing the corresponding user record (including custom attributes), and this information is passed to the view script as an array.

The next step is to update the registration form at $APP_ROOT/views/users-save.phtml to take note of this additional information and pre-populate the form fields with the user's existing data from the passed array. Here are the necessary changes to the form:

...              
<?php if (!isset($_POST['submit'])): ?>
  <form method="post" 
    action="<?php echo $data['router']->pathFor('admin-users-save'); ?>">
    <input name="id" type="hidden" 
      value="<?php echo (isset($data['user']->id)) ? 
      $data['user']->id : ''; ?>" />
    <div class="form-group">
      <label for="fname">First name</label>
      <input type="text" class="form-control" id="fname" 
        name="fname" value="<?php echo (isset($data['user']->firstName)) ? 
        $data['user']->firstName : ''; ?>">
    </div>
    <div class="form-group">
      <label for="lname">Last name</label>
      <input type="text" class="form-control" id="lname" 
        name="lname" value="<?php echo (isset($data['user']->lastName)) ? 
        $data['user']->lastName : ''; ?>">
    </div>
    <div class="form-group">
      <label for="email">Email address</label>
      <input type="text" class="form-control" id="email" 
        name="email" value="<?php echo (isset($data['user']->email)) ? 
        $data['user']->email : ''; ?>">
    </div>
    <div class="form-group">
      <label for="password">Password</label>
      <input type="password" class="form-control" id="password" name="password">
    </div>
    <div class="form-group">
      <label for="city">City</label>
      <input type="text" class="form-control" id="city" name="city" 
        value="<?php echo (isset($data['user']->data->attributes->city)) ? 
        $data['user']->data->attributes->city : ''; ?>">
    </div>
    <div class="form-group">
      <label for="occupation">Occupation</label>
      <input type="text" class="form-control" id="occupation" name="occupation" 
        value="<?php echo (isset($data['user']->data->attributes->occupation)) ? 
        $data['user']->data->attributes->occupation : ''; ?>">
    </div>
    <div class="form-group">
      <label for="mobilePhone">Mobile phone (with country code)</label>
      <input type="text" class="form-control" id="mobilePhone" name="mobilePhone" 
        value="<?php echo (isset($data['user']->mobilePhone)) ? 
        $data['user']->mobilePhone : ''; ?>">
    </div>
    <div class="form-group">
      <button type="submit" name="submit" class="btn btn-default">Save
      </button>
    </div>
  </form>  
<?php else: ?>
  <div class="alert alert-success">
    <strong>Success!</strong> The user with identifier 
      <strong><?php echo $data['user']->id; ?></strong> 
      was successfully created or updated. <a role="button" 
      class="btn btn-primary" 
      href="<?php echo $data['router']->pathFor('admin-users-save'); ?>">
      Add another?</a>
  </div>
<?php endif; ?>
...

Notice that each form field now includes a value attribute, which automatically sets the field value to the corresponding value from the user record (if it exists). The result is that the form is pre-populated with the user's existing data.

Now, when the user modifies some or all of this information and submits it, the form processor needs to validate the submission and update the existing user record in the Passport service. This is accomplished by sending a PUT request to the /api/user/USER_ID endpoint, including the user identifier as part of the endpoint signature.

Since the input validation to be performed for a modification request is almost the same as that for a new registration request, it makes sense to reuse the existing callback handler. You merely need to update it to distinguish between creation and modification operations on the basis of the absence or presence of the user identifier in the URL being requested. Here's the revised callback, which should be updated in $APP_ROOT/public/index.php:

<?php
// Slim application initialization - snipped

// user form processor
$app->post('/admin/users/save', function (Request $request, Response $response) {
  // get configuration
  $config = $this->get('settings');

  // get input values
  $params = $request->getParams();
  
  // check for user id
  // if present, this is update modification
  // if absent, this is user creation
  if ($params['id']) {
    if (!($id = filter_var($params['id'], FILTER_SANITIZE_STRING))) {
      throw new Exception('ERROR: User identifier is not a valid string');
    }
  }    
  
  if (!($fname = filter_var($params['fname'], FILTER_SANITIZE_STRING))) {
    throw new Exception('ERROR: First name is not a valid string');
  }
  
  if (!($lname = filter_var($params['lname'], FILTER_SANITIZE_STRING))) {
    throw new Exception('ERROR: Last name is not a valid string');
  }
  
  $password = trim(strip_tags($params['password']));
  if (empty($id)) {
    if (strlen($password) < 8) {
      throw new Exception('ERROR: Password should be at least 8 characters long');      
    }
  } else {
    if (!empty($password) && (strlen($password) < 8)) {
      throw new Exception('ERROR: Password should be at least 8 characters long');      
    }
  }
      
  $email = filter_var($params['email'], FILTER_SANITIZE_EMAIL);
  if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
    throw new Exception('ERROR: Email address should be in a valid format');
  }
  
  if (!($city = filter_var($params['city'], FILTER_SANITIZE_STRING))) {
    throw new Exception('ERROR: City is not a valid string');
  }
  
  if (!($occupation = filter_var($params['occupation'], FILTER_SANITIZE_STRING))) {
    throw new Exception('ERROR: Occupation is not a valid string');
  }

  $mobilePhone = filter_var($params['mobilePhone'], FILTER_SANITIZE_NUMBER_INT);
  if (filter_var($mobilePhone, FILTER_VALIDATE_FLOAT) === false) {
    throw new Exception('ERROR: Mobile phone is not a valid number');
  }
  
  // generate array of user data
  $user = [
    'registration' => [
      'applicationId' => $config['passport_app_id'],
    ],
    'skipVerification' => true,
    'user'  => [
      'email' => $email,
      'firstName' => $fname,
      'lastName' => $lname,
      'mobilePhone' => $mobilePhone,
      'data' => [
        'attributes' => [
          'city' => $city,
          'occupation' => $occupation,
        ]
      ]
    ]
  ];
  
  // add password if exists
  // can be empty for user modification operations
  if (!empty($password)) {
    $user['user']['password'] = $password;
  }
  
  if (empty($id)) {
    // encode user data as JSON
    // POST to Passport API for user registration and creation
    $apiResponse = $this->passport->post('/api/user/registration', [
      'body' => json_encode($user),
      'headers' => ['Content-Type' => 'application/json'],
    ]);
  } else {
    // encode user data as JSON
    // PUT to Passport API for user modification
    $apiResponse = $this->passport->put('/api/user/' . $id, [
      'body' => json_encode($user),
      'headers' => ['Content-Type' => 'application/json'],
    ]);      
  }

  // if successful, display success message
  // with user id
  if ($apiResponse->getStatusCode() == 200) {
    $json = (string)$apiResponse->getBody();
    $body = json_decode($json);
    $response = $this->view->render($response, 'users-save.phtml', [
      'router' => $this->router, 'user' => $body->user
    ]);
    return $response;
 }
});

// other callbacks

Compare this revised handler with the simpler version seen in Part 1, and you'll notice the following key differences:

  • The callback begins by checking for the presence of a user identifier in the request parameters. If this user identifier is present, the callback assumes this is an update operation and not a new user creation operation. This information affects how some of the subsequent input validation (most notably, password validation) is performed.
  • Previously, the callback tested the password supplied in the form to ensure that it was at least 8 characters long, and raised an error if it wasn't. However, for update operations, the user might well choose not to update the existing password. Therefore, the password validation code in the handler has been updated so that it doesn't raise an error if a password is not supplied during update operations. Similarly, the user key of the JSON document generated after input validation is configured so that the password key is included only for new user creation operations, or for update operations where a new password is supplied in the form.
  • Previously, the client would send a POST request to the Passport API's /api/user/registration endpoint with the JSON-encoded document. This segment of the code has now been updated so that the POST request is now sent only for new user creation operations, and a PUT request is generated instead to the /api/user/USER_ID endpoint for update operations.

The result of all these changes is that the same form can now be used both to process new user registrations and to allow existing users to modify their profiles. All that's left is to add an Edit command button next to each record in the user list page and link it to the route above (you can see the code for this in the source code repository).

Here's an example of what the final result looks like:

Figure 3. User registration form populated for edit operation
User registration form populated for edit operation
User registration form populated for edit operation

Step 3: Enable user account deletion

Just as you can offer application users an interface to edit their profiles by integrating with the Passport API, you can also enable application administrators to delete users from the system. This is as simple as sending a DELETE request to the Passport API's /api/user/USER_ID endpoint and including the additional hardDelete argument as a query parameter. Here's the code necessary to add this feature to your application:

<?php
// Slim application initialization – snipped

// user deletion handler
$app->get('/admin/users/delete/{id}', function (Request $request, 
  Response $response, $args) {
  // sanitize and validate input
  if (!($id = filter_var($args['id'], FILTER_SANITIZE_STRING))) {
    throw new Exception('ERROR: User identifier is not a valid string');
  }

  $apiResponse = $this->passport->delete('/api/user/' . $id , [
    'query' => ['hardDelete' => 'true']
  ]);

  return $response->withHeader('Location', 
    $this->router->pathFor('admin-users-index'));
})->setName('admin-users-delete');

// other callbacks

As before, you will need to add a Delete command button next to each record in the user list page and link it to the route above. Here's what you should end up with:

Figure 4. User dashboard with edit/delete buttons
User dashboard with edit/delete buttons
User dashboard with edit/delete buttons

Step 4: Implement role-based access

In Part 1, I showed you how to protect access to certain pages of your application by requiring the user to authenticate himself or herself via login. This was implemented as Slim middleware, which could be added to specific route handlers to protect access to the corresponding application functions.

Very often, however, you need more than just authentication. For example, it's common for each application user to be assigned one or more roles, and this role (or combination of roles) then defines the features or functions that are available to the user. So, users with an "employee" role might have access to a limited number of functions, while those with an "administrator" role might have access to all functions.

Implementing this type of role-based authentication in your PHP application is fully supported by the Passport API. Each user record includes a roles key, which holds the complete set of roles assigned to the corresponding user, and your application can use this when deciding whether to grant or deny access to a user for specific functions.

Let's see how this works with a simple example. Assume that your application will support two user roles: "gold" members and "silver" members. Assume further that these two roles will have access to different, non-overlapping functions within the application. To implement this role-based access, follow the steps below:

  1. The first step is to tell the Passport API about the roles you wish to support. Browse to the Passport front-end URL, sign in with your administration credentials, navigate to the Manage Roles option for your application, and add two roles: "member-silver" and "member-gold." You should end up with something like this:
    Figure 5. User roles
    User roles
    User roles
  2. The next step is to update the user registration form and handler to support these two roles, so that a user can be assigned the appropriate role during the registration process. Here's the addition to the user registration form at $APP_ROOT/views/users-save.phtml:
    ...
    <div class="form-group">
      <label for="tier">Membership tier</label>
      <select class="form-control" id="tier" name="tier">
        <option value="1" <?php echo !empty($data['user']) &&
          !empty($data['user']->registrations[0]->roles) &&    
          ($data['user']->registrations[0]->roles[0] == 'member-gold') ? 
          'selected="selected"' : ''; ?>>Gold</option>
        <option value="2" <?php echo !empty($data['user']) && 
          !empty($data['user']->registrations[0]->roles) &&    
          ($data['user']->registrations[0]->roles[0] == 'member-silver') ? 
          'selected="selected"' : ''; ?>>Silver</option>
      </select>
    </div>
    ...

    This adds a role selection list to the registration form. Here's what it looks like:
    Figure 6. User profile form with role selector
    User profile form with role selector
    User profile form with role selector
  3. At the same time, update the form processor so that it validates and adds the selected role to the JSON document that's submitted to the Passport API when creating a new user. The selected role is added to the registration.roles key.
    <?php
    // Slim application initialization – snipped
    
    // user form processor
    $app->post('/admin/users/save', function (Request $request, Response $response) {
    
      // ...
      
      if (!($tier = filter_var($params['tier'], FILTER_SANITIZE_NUMBER_INT))) {
        throw new Exception('ERROR: Membership tier is not valid');
      }
      if ($tier == 1) {
        $role = 'member-gold';
      } else if ($tier == 2) {
        $role = 'member-silver';
      }
      
      // generate array of user data
      $user = [
        'registration' => [
          'applicationId' => $config['passport_app_id'],
          'roles' => [
            $role
          ]
        ],
        'skipVerification' => true,
        'user'  => [
          'email' => $email,
          'firstName' => $fname,
          'lastName' => $lname,
          'mobilePhone' => $mobilePhone,
          'data' => [
            'attributes' => [
              'city' => $city,
              'occupation' => $occupation,
            ]
          ]
        ]
      ];
      
      // ...
      
    });
    
    // other callbacks
  4. The next step is to add some code for role-based authorization (implemented as Slim middleware). This code will check the currently logged-in user's role and compare it to the role requirements for the route that is being accessed. If there is a mismatch, access to the route is denied and the user is redirected to the login page. Here's the code, which should be added to $APP_ROOT/public/index.php before the other callback handlers:
    <?php
    // Slim application initialization – snipped
    
    // simple authorization middleware
    $authorize = function ($role) {
      return function($request, $response, $next) use ($role) {
        if ($_SESSION['user']->registrations[0]->roles[0] != $role) {
          return $response->withHeader('Location', 
            $this->router->pathFor('login'));
        } 
        return $next($request, $response);  
      };
    };
    
    // other callbacks
  5. The final step is to attach the authorization middleware to all routes that are to be restricted by role. To demonstrate this, create two routes and corresponding callback handlers in $APP_ROOT/public/index.php, one for "gold" members only and the other for "silver" members only:
    <?php
    // Slim application initialization – snipped
    
    // role-limited page handler
    $app->get('/members/gold', function (Request $request, Response $response) {
      return $this->view->render($response, 'members-gold.phtml', [
        'router' => $this->router, 'user' => $_SESSION['user']
      ]);
    })->setName('members-gold')->add($authenticate)->add($authorize('member-gold'));
    
    // role-limited page handler
    $app->get('/members/silver', function (Request $request, Response $response) {
      return $this->view->render($response, 'members-silver.phtml', [
        'router' => $this->router, 'user' => $_SESSION['user']
      ]);
    })->setName('members-silver')->add($authenticate)->add($authorize('member-silver'));
    
    // other callbacks

    Notice that the /members/gold route has two middleware functions attached to it. The $authenticate middleware ensures that this route is available only to logged-in users, while the $authorize middleware further restricts access to only logged-in users with the "member-gold" role. A similar approach is followed to restrict access to the /members/silver route to only users with the "member-silver" role. The view scripts for these routes can be obtained from the application source code repository.

    To see this role assignment in action, create two users in the application, assigning each the "gold" or "silver" role. Next, attempt to access the two routes above after logging in as each user. The "gold" user should be able to access the /members/gold route but should be denied access to the /members/silver route, and the opposite should be true for the "silver" user. Here's an example of what you should see:
    Figure 7. Role-restricted page
    Role-restricted page
    Role-restricted page

It's worth pointing out that in this implementation, users who are not assigned a role will not be able to access any role-restricted pages (unless you build in a special exception for them). That's why it's always a good idea to define and implement role-based access control rules right at the beginning of your project, so that you don't end up having to code special exceptions for users who don't have any role assignments.

Step 5: Implement password recovery (request stage)

Every application worth its salt needs to have a way to handle forgotten user passwords. The Passport API includes methods that help you quickly implement a recovery system for forgotten passwords, without needing a great deal of code or time. It works like this:

  1. A user initiates the workflow by clicking a link in the application.
  2. The application asks the user for his or her email address. On receipt of this input, the application sends a request to the Passport API endpoint /api/user/forgot-password and passes it the user's email address.
  3. The Passport API sends an email containing a verification link and unique verification ID to the user's email address. The link directs the user to a route within the application.
  4. The user receives the email and clicks the verification link.
  5. The application asks the user for a new password. On receipt of this input, the application sends a request to the Passport API endpoint /api/user/change-password/VERIFICATION_ID, and passes it the verification ID and new password.
  6. The Passport API verifies the request using the verification ID. If it matches, the API resets the user's password to the supplied value.

To implement this process, begin by creating a form for the user to request a password reset, at $APP_ROOT/views/password-request.phtml:

...
<?php if (!isset($_POST['submit'])): ?>
<div>
  <form method="post" 
    action="<?php echo $data['router']->pathFor('password-request'); ?>">
    <div class="form-group">
      <label for="email">Email address</label>
      <input type="text" class="form-control" id="email" name="email">
    </div>
    <div class="form-group">
      <button type="submit" name="submit" 
        class="btn btn-default">Submit</button>
    </div>
  </form>  
</div>
<?php else: ?>
<div>
  <?php if ($data['status'] == 200): ?>
  <div class="alert alert-success">
    <strong>Success!</strong> A verification email has been sent 
      to your email address. Click the link in the email to proceed.
  </div>
  <?php elseif ($data['status'] == 404): ?>
  <div class="alert alert-danger">
    <strong>Failure!</strong> No user matching that identifier 
      could be found.
  </div>
  <?php else: ?>
  <div class="alert alert-danger">
    <strong>Failure!</strong> Something unexpected happened.
  </div>
  <?php endif; ?>
</div>        
<?php endif; ?>
...

Next, create callback handlers to render the form and process the submitted email address:

<?php
// Slim application initialization – snipped

// password reset handlers (request step)
$app->get('/password-request', function (Request $request, Response $response) {
  return $this->view->render($response, 'password-request.phtml', [
    'router' => $this->router
  ]);
})->setName('password-request');

$app->post('/password-request', function (Request $request, Response $response) {
  try {
    // validate input 
    $params = $request->getParams();
    $email = filter_var($params['email'], FILTER_SANITIZE_EMAIL);
    if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
      throw new Exception('ERROR: Email address should be in a valid format');
    }

    // generate array of data
    $data = [
      'loginId' => $email,
      'sendForgotPasswordEmail' => true
    ];

    $apiResponse = $this->passport->post('/api/user/forgot-password', [
      'body' => json_encode($data),
      'headers' => ['Content-Type' => 'application/json'],
    ]);

  } catch (ClientException $e) {
    // in case of a Guzzle exception
    // if 404, user not found error 
    // bypass exception handler and show failure message
    // for other errors, transfer to exception handler as normal
    if ($e->getResponse()->getStatusCode() != 404) {
      throw new Exception($e->getResponse());
    } else {
      $apiResponse = $e->getResponse();
    }
  } 

  return $this->view->render($response, 'password-request.phtml', [
    'router' => $this->router, 
    'status' => $apiResponse->getStatusCode()
  ]);
});

// other callbacks

Here's what the form looks like; it will be accessible at the /password-request application URL.

Figure 8. Password reset request and response
Password reset request and response
Password reset request and response

You can add a link to this URL in the login page of the application, as shown here:

            <a href="<?php echo $data['router']->pathFor('password-request'); ?>" 
              class="btn btn-default">Forgot password?</a>

When the form is submitted, the input email address is validated and, if valid, the handler sends a POST request to the /api/user/forgot-password endpoint. The body of the request contains the submitted email address and a flag that directs the Passport API to send a verification email to the specified address.

If the user's email address cannot be matched in the Passport database, the response to the request will be a 404 error, which is caught by the handler and used to generate an appropriate error message. If a match is found, the Passport service sends the user an email containing a verification link.

The target of this link can be configured in the Passport service dashboard and should point to a URL that's controlled by your application; a unique verification ID will be automatically appended to it by Passport. To define the link, browse to the Passport front-end URL, sign in with your administration credentials, and navigate to the Settings → Email Templates → Forgot Password template. Update the link in the template as shown below, adjusting the domain to reflect your application host. Notice the ${user.verificationId} placeholder in the link URL, which represents the unique verification ID that is used as a security check before installing the new password.

Figure 9. Email template for password reset
Email template for password reset
Email template for password reset

Step 6: Implement password recovery (verification and reset stages)

When the user receives the email and clicks the verification link, the application displays a form for the user to enter a new password.

Here's the callback handler for the application URL /password-reset, which is invoked when the user clicks the verification link in the email:

<?php
// Slim application initialization – snipped

// password reset handlers (reset step)
$app->get('/password-reset[/{id}]', function (Request $request, 
  Response $response, $args) {
  // sanitize and validate input
  if (!($id = filter_var($args['id'], FILTER_SANITIZE_STRING))) {
    throw new Exception('ERROR: Verification string is invalid');
  }
  return $this->view->render($response, 'password-reset.phtml', [
    'router' => $this->router, 'id' => $args['id']
  ]);
})->setName('password-reset');

// other callbacks

This callback handler looks for the verification ID that is passed along with the URL as a route parameter, sanitizes it, and then renders a form containing the verification ID as a hidden field, together with fields for the user to enter a new password. Here's the code for that form, to be created at $APP_ROOT/views/password-reset.phtml:

...
<?php if (!isset($_POST['submit'])): ?>
<div>
  <form method="post" 
    action="<?php echo $data['router']->pathFor('password-reset'); ?>">
    <input name="id" type="hidden" 
      value="<?php echo htmlentities($data['id']); ?>" />
    <div class="form-group">
      <label for="password">Password</label>
      <input type="password" class="form-control" id="password" 
        name="password">
    </div>
    <div class="form-group">
      <label for="password-confirm">Password (again)</label>
      <input type="password" class="form-control" id="password-confirm" 
        name="password-confirm">
    </div>
    <div class="form-group">
      <button type="submit" name="submit" 
        class="btn btn-default">Submit</button>
    </div>
  </form>  
</div>
<?php else: ?>
<div>
  <?php if ($data['status'] == 200): ?>
  <div class="alert alert-success">
    <strong>Success!</strong> Your password was changed.
  </div>
  <?php elseif ($data['status'] == 404): ?>
  <div class="alert alert-danger">
    <strong>Failure!</strong> Your password could not be changed.
  </div>
  <?php else: ?>
  <div class="alert alert-danger">
    <strong>Failure!</strong> Something unexpected happened.
  </div>
  <?php endif; ?>
</div>        
<?php endif; ?>
...

Notice that the form includes a hidden field for the verification ID, which is passed to the form as a template variable by the callback handler. Here's what it looks like:

Figure 10. Password reset form
Password reset form
Password reset form

The final step is to process this form and update the user's password in the Passport database. Here's what the form processor looks like:

<?php
// Slim application initialization – snipped

// password reset handler
$app->post('/password-reset', function (Request $request, Response $response) {
  try {
    // validate input 
    $params = $request->getParams();

    // sanitize and validate input
    if (!($id = filter_var($params['id'], FILTER_SANITIZE_STRING))) {
      throw new Exception('ERROR: Verification string is invalid');
    }

    
    $password = trim(strip_tags($params['password']));
    if (strlen($password) < 8) {
      throw new Exception('ERROR: Password should be at least 8 characters long');      
    }
    $passwordConfirm = trim(strip_tags($params['password-confirm']));
    if ($password != $passwordConfirm) {
      throw new Exception('ERROR: Passwords do not match');      
    }
    
    // generate array of data
    $data = [
      'password' => $password,
    ];

    $apiResponse = $this->passport->post('/api/user/change-password/' . $id, [
      'body' => json_encode($data),
      'headers' => ['Content-Type' => 'application/json'],
    ]);

  } catch (ClientException $e) {
    // in case of a Guzzle exception
    // if 404, user not found error 
    // bypass exception handler and show failure message
    // for other errors, transfer to exception handler as normal
    if ($e->getResponse()->getStatusCode() != 404) {
      throw new Exception($e->getResponse());
    } else {
      $apiResponse = $e->getResponse();
    }
  } 

  return $this->view->render($response, 'password-reset.phtml', [
    'router' => $this->router, 
    'status' => $apiResponse->getStatusCode()
  ]);
});

// other callbacks

When the user submits the form with his or her new password, the form processor verifies that the password matches the requirements and then initiates a POST request to the /api/user/change-password/VERIFICATION_ID endpoint. The body of the POST request contains the user's new password.

At the Passport end of things, the Passport API checks the validity of the verification ID. If valid, the API resets the user's password to the new value. Depending on whether or not the operation is successful, the API returns a 200 or a 4xx error code, which can be intercepted by the callback and used to display an appropriate success or error message. If successful, the user should be able to log in to the application with the new password.

Conclusion

It should be clear from the preceding examples that Bluemix's Passport service makes adding full-featured user management, authentication, and role-based access to your application quick and easy, requiring only an API client and a reasonable understanding of the Passport API. The example application in this tutorial is a PHP application; however, the API and the principles outlined here will work equally well for applications written in any other programming language. The end result of following this approach is a scalable, secure application that meets modern security and SSO requirements while remaining flexible enough to accommodate new requirements.

If you'd like to experiment with the Passport service discussed in this tutorial, start by trying out the demo application. Then, download the code from its GitHub repository and take a closer look to see how it all fits together. You can also refer to the links under "Related topics" below to learn more about the various services and tools used in this tutorial. Have fun!


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Cloud computing, Security
ArticleID=1047930
ArticleTitle=Manage and authenticate users easily in IBM Bluemix applications with PHP and the Passport service, Part 2: Add role-based access and password recovery to your PHP application
publish-date=07242017