1. 程式人生 > >Add role-based access and password recovery to your PHP application

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

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 IBM Cloud. 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!

The Passport service on IBM Cloud makes adding full-featured user management, authentication, and role-based access to your application quick and easy.

What you need for this tutorial

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 IBM Cloud terms of use.

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

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

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

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

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
  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
  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

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

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

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

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 IBM Cloud’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!

相關推薦

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

In Part 1, I explained the basics of the Passport API and walked you through the process of integrating Passport with a PHP applicati

Azure ARM (16) 基於角色的訪問控制 (Role Based Access Control, RBAC) - 使用默認的Role

not 問控制 https 所有 嘗試 介紹 admin ima 服務管理   《Windows Azure Platform 系列文章目錄》   今天上午剛剛和客戶溝通過,趁熱打鐵寫一篇Blog。   熟悉Microsoft Azure平臺的讀者都知道,在

Azure ARM (17) 基於角色的訪問控制 (Role Based Access Control, RBAC) - 自定義Role

結果 del role environ db4 lis sele logs ini   《Windows Azure Platform 系列文章目錄》      在上面一篇博客中,筆者介紹了如何在RBAC裏面,設置默認的Role。   這裏筆者將介紹如何使用自定的Ro

Integrate Touch ID and Face ID to your React Native App

Integrate Touch ID and Face ID to your React Native AppAdding authentication using the user’s Touch ID or the new Face ID is easier than ever in your React

【sqli-labs】 less26 GET- Error based -All you SPACES and COMMENTS belong to us(GET型基於錯誤的去除了空格和註釋的註入)

src blog sci 字符 XML 包含 col concat image 看了下源碼 所有的註釋形式和反斜線,and,or都被了過濾掉了 單引號沒有過濾 空格也被過濾了 http://localhost/sqli-labs-master/Less-26/?id=

【sqli-labs】 less26a GET- Blind based -All you SPACES and COMMENTS belong to us -String-single quotes-Parenthesis(GET型基於盲註的去除了空格和註釋的單引號括號註入)

.com tables ng- username spa str ase space pan 這個和less26差不多,空格還是用%a0代替,26過了這個也就簡單了 ;%00 可以代替註釋,嘗試一下 order by 3 http://192.168.136.128/s

How To Add Google Apps and ARM Support to Genymotion v2.0+

How To Add Google Apps and ARM Support to Genymotion v2.0+ Original Source: [GUIDE] Genymotion | Installing ARM Translation and GApps - XDA-Develop

You are trying to add a non-nullable field 'password' to userinfo without a default問題

當向models.py對應類新增一個新欄位 password = models.CharField(max_length=20) 之後,執行python3 manage.py makemigrations命令提示以下資訊: You are trying to add a n

How to create role based accounts for your Saas App using FEAN? (Part 1)

Setup firebase in your angular app and express js// Front-endng new exampleAppcd exampleApp && cd exampleApp// For adding firebase to angular appng

Use reflector to access and modify private parameter

Just one important setting: fields[i].setAccessible(true); 關注微信公眾號: 回覆語言名稱,比如java,python,go,C, C++.有海量資源免費贈送! package com.kado; import java.

Add and Verify Domains to Use With Workmail

Amazon Web Services is Hiring. Amazon Web Services (AWS) is a dynamic, growing business unit within Amazon.com. We are currently hiring So

Add menubar and search function to hexo blog

Add Menubar In the theme folder, we can find the following scripts in the _config.yml file. # ---------------------------------

Azure RBAC(Roles Based Access Control)正式上線了

如果 ext 授權 開發 fonts style clas 應用程序 tex 期盼已久的Azure RBAC(Roles Based Access Control)正式上線了。在非常多情況下。客戶須要對各種類型的用戶加以區分,以便做出適當的授權決定。基於角色的訪問控制

Two Wrongs Can Make a Right (and Are Difficult to Fix)

write work visible fun mar pop sig per cati Two Wrongs Can Make a Right (and Are Difficult to Fix) Allan Kelly CODE NEVER LIE

使用命令:ssh-add 時,出現 “Could not open a connection to your authentication agent.”

col cti ash agent str cati authent b- then 為 GitHub 賬號設置 SSH Key時, 使用命令:ssh-add,出現“Could not open a connection to your authentication age

eclipse啟動報錯the catalog could not be loaded please ensure that you have network access and if needed have configured your network proxy

實例 等待 ces .cn access 分享圖片 clas 安裝包 nan 搜索關鍵詞不對在網上查了一圈沒找到合適的解決辦法 去看報錯的日誌文件 然並卵。不過下面有個config.ini,想著以前能用現在不能用,肯定是配置問題,打開該文件 轉載請註明出處http

關於RBAC(Role-Base Access Control)的理解(轉)

topic div 今天 就是 對象 配置文件 需要 需求 重新 基於角色的訪問控制(Role-Base Access Control) 有兩種正在實踐中使用的RBAC訪問控制方式:隱式(模糊)的方式和顯示(明確)的方式。 今天依舊有大量的軟件應用是使用隱式的訪問控制方式。

mysql更新字段值提示You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column To disable safe mode

error without 使用 using ble mod code span set 1 引言 當更新字段缺少where語句時,mysql會提示一下錯誤代碼: Error Code: 1175. You are using safe update mode and yo

Add Languages to Your Xamarin Apps with Multilingual App Toolkit

stand efi working geb ray running strong snippet apply With Xamarin, you can extend your cross-platform apps with support for native spea

http://localhost:8080 is requesting your username and password

ifg cxf sep eat iuc wpf srm UC rtg after you startup your tomcat, you type a concrete request url in broswer, the tomcat probably will