Implementing OpenID Using PHP

Our first practical OpenID implementation example will use PHP. Our intention is to build out an end-to-end implementation that will allow a user to input the OpenID provider that she wants to use, after which the program will allow her to log in with that provider service and deliver information about her at the end of the authentication process.

In addition to obtaining a pass/fail state for whether the user authenticated, we will acquire additional information and levels of security by implementing the previously discussed OpenID extensions:

  • Simple Registration for acquiring basic user information

  • Attribute Exchange for acquiring more extensive user information

  • PAPE for providing additional security levels

At the end, we will have a solid understanding of how OpenID functions from a programmatic perspective.

The discovery form

Let’s start off the process by building out the form that will allow the user to input the provider OpenID URL she wants to use and select some of the PAPE policies that she would like to send along as well.

Note

In a real-world implementation, you would not provide the user with a form field to have her input the OpenID provider URL or the policies that she would like to use. As mentioned earlier, you would add icons (or some other identifying marker) for each provider option in order to allow the user to initiate the login process by choosing one. When the user clicks an icon, you would then determine the corresponding OpenID URL for the selected provider and add in the policies that you need, without requiring further user interaction.

For the sake of this example, the following file will be named index.html:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
   <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
   <title>OpenID Sample Application</title>
</head>
<body>
<style type="text/css">
   form{ font:12px arial,helvetica,sans-serif; }
   #openid_url { background:#FFFFFF url(http://wiki.openid.net/f/openid-16x16.gif)
                          no-repeat scroll 5px 50%;
                          padding-left:25px; }
</style>

<form action="auth.php" method="GET">
   <input type="hidden" value="login" name="actionType">
   <h2>Sign in using OpenID</h2>
   <input type="text" style="font-size: 12px;" size="40" id="openid_url"
      name="openid_url"> &nbsp;
   <input type="submit" value="Sign in"><br />
   <small>(e.g. http://username.myopenid.com)</small>
   <br /><br />

   <b>PAPE Policies (Optional)</b><br />
   <input type="checkbox" name="policies[]" value="PAPE_AUTH_MULTI_FACTOR_PHYSICAL" />
   PAPE_AUTH_MULTI_FACTOR_PHYSICAL<br />
   <input type="checkbox" name="policies[]" value="PAPE_AUTH_MULTI_FACTOR" />
   PAPE_AUTH_MULTI_FACTOR<br />
   <input type="checkbox" name="policies[]" value="PAPE_AUTH_PHISHING_RESISTANT" />
   PAPE_AUTH_PHISHING_RESISTANT<br />
</form>
</body>
</html>

The beginning of our file is quite standard and includes the styles that we will be using for the form, including an OpenID logo image.

The real piece that we will focus on is the form itself. First, when the user clicks the submit button to process the form, she will be forwarded to our auth.php file to generate the authentication requests for her to sign in to the provider.

Next, we have an input box to allow the user to enter the OpenID discovery URL for the provider that she would like to sign in to. In practice, this step usually includes a series of provider images (e.g., Yahoo!, Google, etc.) from which the user can select so that she does not have to know the discovery endpoint herself.

Last, we have a block of inputs to allow the user to select the different PAPE policies that she would like to use for the request.

Once the user fills out the form and submits it, she will be forwarded to our auth.php file.

The common includes, functions, and globals

All files involved in the discovery and processing of the OpenID functions and functionality in this example use a common set of includes, functions, and global definitions, which are stored in a file named includes.php.

Let’s take a brief look at the common elements that we will use throughout this example:

<?php
require_once "Auth/OpenID/Consumer.php";   //openid consumer code
require_once "Auth/OpenID/FileStore.php";  //file storage
require_once "Auth/OpenID/SReg.php";       //simple registration
require_once "Auth/OpenID/PAPE.php";       //pape policy
require_once "Auth/OpenID/AX.php";         //attribute exchange

define('FILE_COMPLETE', 'complete.php'),
define('STORAGE_PATH', 'php_consumer'),

/******************************************************************
 * Function: Get Consumer
 * Description: Creates consumer file storage and OpenID consumer
 ******************************************************************/
function get_consumer() {
   //ensure file storage path can be created
   if (!file_exists(STORAGE_PATH) && !mkdir(STORAGE_PATH)){
      print "Could not create FileStore directory '". STORAGE_PATH ."'.
         Please check permissions.";
      exit(0);
   }

   //create consumer file store
   $store = new Auth_OpenID_FileStore(STORAGE_PATH);

   //create and return consumer
   $consumer =& new Auth_OpenID_Consumer($store);
   return $consumer;
}
?>

There are three distinct blocks of functionality in our common includes file that we need to go over.

First, the required file includes at the top introduce the OpenID files that we must have to process the OpenID example. These are:

Consumer.php

OpenID consumer code

FileStore.php

The functionality to store

SReg.php

The Simple Registration extension that enables us to obtain simple profile information about the user

PAPE.php

The PAPE policy definition file that enables us to use the associated functionality

AX.php

The Attribute Exchange file that enables us to obtain extended public profile information about the user

The next block contains our global path definitions:

FILE_COMPLETE

The filename (under the APP_ROOT folder) where the provider should forward the user once she has logged in to the provider

STORAGE_PATH

The relative path to store the OpenID consumer objects

Finally, we have our get_consumer function, which allows us to obtain a new OpenID consumer object and a consumer file storage mechanism that we will use later in the program.

Now that we have an overview of the common file that we’ll use throughout our program flow, let’s jump into the authentication request file that the initial form forwards the user to.

The authentication request

Warning

Support for extensions such as Attribute Exchange or Simple Registration fully depends on the provider that you are attempting to use. Each provider supports its own set of extensions and defines its own data sets that can be obtained. Be sure to check for support prior to using extensions.

The auth.php file contains a series of functions to initiate the authentication process and attach the three extensions that we are exploring in this example.

We’ll first start a new PHP session and integrate our includes.php file that we just went over.

We can then jump into the make_request function, which will be the controller for this section of the authentication process:

<?php
require_once "includes.php";  //configurations and common functions

/******************************************************************
 * Function: Make Request
 * Description: Builds out the OpenID request using the defined
 *           request extensions
 ******************************************************************/
function make_request(){
   //get openid identifier URL
   if (empty($_GET['openid_url'])) {
      $error = "Expected an OpenID URL.";
      print $error;
      exit(0);
   }

   $openid = $_GET['openid_url'];
   $consumer = get_consumer();

   //begin openid authentication
   $auth_request = $consumer->begin($openid);

   //no authentication available
   if (!$auth_request) {
      echo "Authentication error; not a valid OpenID.";
   }

   //add openid extensions to the request
   $auth_request->addExtension(attach_ax());    //attribute exchange
   $auth_request->addExtension(attach_sreg());  //simple registration
   $auth_request->addExtension(attach_pape());  //pape policies

   $return_url = sprintf("http://%s%s/%s", $_SERVER['SERVER_NAME'],
                          dirname($_SERVER['PHP_SELF']),
                          FILE_COMPLETE);
   $trust_root = sprintf("http://%s%s/", $_SERVER['SERVER_NAME'],
                          dirname($_SERVER['PHP_SELF']));

   //openid v1 - send through redirect
   if ($auth_request->shouldSendRedirect()){
      $redirect_url = $auth_request->redirectURL($trust_root, $return_url);

      //if no redirect available display error message, else redirect
      if (Auth_OpenID::isFailure($redirect_url)) {
         print "Could not redirect to server: " . $redirect_url->message;
      } else {
         header("Location: " . $redirect_url);
      }
   //openid v2 - use javascript form to send POST to server
   } else {
      //build form markup
      $form_id = 'openid_message';
      $form_html = $auth_request->htmlMarkup($trust_root, $return_url, false,
                                             array('id' => $form_id));

      //if markup cannot be built display error, else render form
      if (Auth_OpenID::isFailure($form_html)){
         print "Could not redirect to server: " . $form_html->message;
      } else {
         print $form_html;
      }
   }
}

At the top of the function, we first check to make sure that the user (or our program, for that matter) has defined an OpenID provider URL for us to initiate the authentication request against. Once we confirm that, we obtain the URL and create a new OpenID consumer object, as well as an OpenID consumer file storage mechanism.

We then call the authentication begin function against our OpenID consumer, passing along the OpenID URL. This step performs the URI discovery to validate that the specified URL is indeed a valid OpenID endpoint. If that succeeds, we can start attaching our extensions.

Calling the addExtension(...) method against our authentication request object for each extension, we pass in the return value of our extension generation functions as the attribute. These will simply be objects that define the type of data that we want or the process that we want to use. We’ll look at these functions in more detail shortly.

Now we need to define a few URLs for the remainder of the process. The return_url variable is the absolute URL to the complete file that will be called once the user has logged in through the authentication process. The trust_root parameter is used to define a trusted location to validate that the authentication process is going through the expected channels.

Now, depending on the version of OpenID being employed, we will handle the request for authentication in different ways. We use the shouldSendRedirect() method against our authentication object to determine whether we should redirect the user (OpenID 1) or use a form POST (OpenID 2).

To redirect the user, we build a redirect URL with our trust_root and redirect_url, and then call Auth_OpenID::isFailure(...) to ensure that the redirect URL is valid. If so, we redirect the user.

To send a form POST request, we create a form ID and the form HTML markup using the htmlMarkup(...) method. We then call the Auth_OpenID::isFailure(...) method to ensure that the form markup can be displayed. If it can, we print it out for the user to authenticate with a login.

Now that you understand this process, let’s take a closer look at the functions that generate the OpenID extension objects that we are sending along with our authentication request. We’ll start by looking at the Attribute Exchange function:

/******************************************************************
 * Function: Attach Attribute Exchange
 * Description: Creates attribute exchange OpenID extension request
 *           to allow capturing of extended profile attributes
 ******************************************************************/
function attach_ax(){
   //build attribute request list
   $attribute[] = Auth_OpenID_AX_AttrInfo::make(
                  'http://axschema.org/contact/email', 1, 1, 'email'),
   $attribute[] = Auth_OpenID_AX_AttrInfo::make(
                  'http://axschema.org/namePerson', 1, 1, 'fullname'),
   $attribute[] = Auth_OpenID_AX_AttrInfo::make(
                  'http://axschema.org/person/gender', 1, 1, 'gender'),
   $attribute[] = Auth_OpenID_AX_AttrInfo::make(
                  'http://axschema.org/media/image/default', 1, 1, 'picture'),

   //create attribute exchange request
   $ax = new Auth_OpenID_AX_FetchRequest;

   //add attributes to ax request
   foreach($attribute as $attr){
      $ax->add($attr);
   }

   //return ax request
   return $ax;
}

The AX function contains a fairly simple process for defining the user profile values that we want to obtain from the user once she has logged in to the provider site.

We first create an array of Attribute Exchange attribute information objects. We do so by making requests to Auth_OpenID_AX_AttrInfo::make(...) with several parameters to denote the piece of information that we are trying to obtain. These include:

type_uri (string)

The URI for the OpenID type that defines the attribute.

count (integer)

The number of values to request for the type. You might have a count greater than 1 if, for example, you are trying to obtain employment information from a user’s profile when multiple jobs may be defined.

required (Boolean)

Whether the type should be marked as required in the OpenID request, and is required to complete the request.

alias (string)

The name alias to be attributed to the type in the request.

Now that we have defined the attributes we want to obtain, we create a new attribute exchange request object by calling the constructor for Auth_OpenID_AX_FetchRequest. We then loop through the array of attributes that we just created and add them to the new attribute exchange request object. Once this is complete, we return the object.

Next, let’s look at attaching the functionality for the Simple Registration extension:

/******************************************************************
 * Function: Attach Simple Registration
 * Description: Creates simple registration OpenID extension request
 *           to allow capturing of simple profile attributes
 ******************************************************************/
function attach_sreg(){
   //create simple registration request
   $sreg_request = Auth_OpenID_SRegRequest::build(
      array('nickname'),
      array('fullname', 'email'));

   //return sreg request
   return $sreg_request;
}

The Simple Registration extension process is even simpler than the Attribute Exchange process. We create the Simple Registration object at the same time that we define which user profile fields we’d like to obtain. We make a request to Auth_OpenID_SRegRequest::build(...), passing in the fields that we would like to obtain as arrays of strings. Attributes that are passed in as the first array of strings are marked as required for the completion of the process, while attributes in the second array of strings are optional and may not be returned.

Note

If you are unsure whether the provider you are working with makes available a certain user profile attribute that you are trying to obtain, then it is best to set its return requirement as optional and then be prepared to catch the case where the data may not be returned.

Now that we have set up our Simple Registration flow, let’s define our PAPE policies for the request:

/******************************************************************
 * Function: Attach PAPE
 * Description: Creates PAPE policy OpenID extension request to
 *           inform server of policy standards
 ******************************************************************/
function attach_pape(){
   //capture pape policies passed in via openid form
   $policy_uris = $_GET['policies'];

   //create pape policy request
   $pape_request = new Auth_OpenID_PAPE_Request($policy_uris);

   //return pape request
   return $pape_request;
}

//initiate the OpenID request
make_request();
?>

Our attach_pape() function follows the same type of flow as the SREG and AX extensions. We first obtain all selected PAPE policies from the query string that the user selected in the original form. These will be the authentication policies that we will use for the request.

We can then simply call the constructor for Auth_OpenID_PAPE_Request(), passing in the policies obtained from the form and return the object back. It’s that simple.

Now that all of our functions are defined, we call make_request() to begin the authentication process.

The authentication callback

No matter which method we’re using to authenticate the user (either forwarding the user on to the provider domain or printing out the authentication process as a form), the user will be presented with a login screen that allows her to log in to the service provider of her choice. Once she has entered in her username and password and has clicked to log in, she will be sent to the authentication callback location that is associated with the process. For our example, we have this file saved as complete.php. This file will allow us to complete the authentication process and pull out all of the data that we are requesting from our extensions.

Let’s break down the callback into the logical blocks that we set up in the initial request, our primary OpenID authentication, and the extensions that we requested.

Checking the OpenID authentication state

The first thing that we are going to work with now is the OpenID response. We need to ensure that the user did not cancel the process and that there wasn’t a failure at some point in the request:

<?php
require_once("includes.php");

//get new OpenID consumer object
$consumer = get_consumer();

//complete openid process using current app root
$return_url = sprintf("http://%s%s/complete.php", $_SERVER['SERVER_NAME'],
                       dirname($_SERVER['PHP_SELF']));
$response = $consumer->complete($return_url);

//response state - authentication cancelled
if ($response->status == Auth_OpenID_CANCEL) {
   $response_state = 'OpenID authentication was cancelled';
//response state - authentication failed
} else if ($response->status == Auth_OpenID_FAILURE) {
   $response_state = "OpenID authentication failed: " . $response->message;
//response state - authentication succeeded
} else if ($response->status == Auth_OpenID_SUCCESS) {
   //get the identity url and capture success message
   $openid = htmlentities($response->getDisplayIdentifier());
   $response_state = sprintf('OpenID authentication succeeded:
      <a href="%s">%s</a>', $openid, $openid);

   if ($response->endpoint->canonicalID){
      $response_state .= '<br />XRI CanonicalID Included: '
                       . htmlentities($response->endpoint->canonicalID);
   }

We start the process by including our includes.php file that we detailed earlier. From this set of includes, we create a new OpenID consumer object that we can use to complete the OpenID process.

To complete the OpenID process, we need to do two things. We first construct the absolute URL to the complete.php file (where we currently are), which we will use to verify the complete state location. We then call the complete(...) method of our OpenID consumer, passing in the current URL. This method will interpret the server’s response to our OpenID request. The absolute URL that we specified will be compared against the openid.current_url variable to confirm a match. If a match cannot be made, the OpenID complete(...) method will return a response of FAILURE. In any event, the response object returned from this method will provide us with all of the information that we need to process the OpenID server response.

We start that process by checking the string status of the OpenID complete(...) response object, $response->status. Depending on the response from this parameter, we will proceed in different ways:

Auth_OpenID_CANCEL

The authentication process was cancelled. There is no information to obtain from the response.

Auth_OpenID_FAILURE

The authentication process failed at some point. The message parameter in the response object will have more information about the failure, so we display the “Something went wrong” string with that message.

Auth_OpenID_SUCCESS

The process completed successfully. We call getDisplayIdentifier() in the response object to obtain the profile URL of the user who authenticated, and then display that in a success message to the user.

The case that we will explore for the callback is the SUCCESS response. If there is a CANCEL or FAILURE instance, we’ll need to handle those appropriately, but for the scope of this example we’ll see how to pull our user information from an OpenID SUCCESS case.

After we have displayed the OpenID user identifier for the user in our SUCCESS case, we check the endpoint to see whether there is a CanonicalID field available. This field will be available if the verified identifier is an XRI (Extensible Resource Identifier). If available, the CanonicalID field that is discovered from the XRD (Extensible Resource Descriptor) should be used as the key lookup field when we’re storing information about the end user.

Now that we have the simple OpenID information for the user, let’s look at how we can extract further information from the extensions that we defined. We’ll take a look at the Simple Registration extension first.

Capturing values returned by Simple Registration

Using the Simple Registration extension from our OpenID request, we can capture some profile, contact, and geographical information about a user through our existing OpenID process.

Within the SUCCESS instance of the OpenID response in our sample, we can display the information that the provider has returned from the Simple Registration extension:

//display sreg return data if available
$response_sreg =
   Auth_OpenID_SRegResponse::fromSuccessResponse($response)->contents();
foreach ($response_sreg as $item => $value){
   $response_state .= "<br />SReg returned <b>$item</b> with the value:
                       <b>$value</b>";
}

Using the Auth_OpenID_SRegResponse::fromSuccessResponse(...) method, we can capture the Simple Registration object from the OpenID response. Against that object, we can call the contents() helper method to return only the Simple Registration data. (This method is really just returning the “data” structure inside the Simple Registration return object.)

The object that you are working with might look something like the following:

array(7) {
  ["openid.sreg.email"]=> array(1) {
   [0]=> string(17) "[email protected]"
  }
  ["openid.sreg.nickname"]=> array(1) {
   [0]=> string(3) "Jon"
  }
  ["openid.sreg.gender"]=> array(1) {
   [0]=> string(1) "M"
  }
  ["openid.sreg.dob"]=> array(1) {
   [0]=> string(10) "1980-12-06"
  }
  ["openid.sreg.country"]=> array(1) {
   [0]=> string(2) "US"
  }
  ["openid.sreg.language"]=> array(1) {
   [0]=> string(2) "en"
  }
  ["openid.sreg.timezone"]=> array(1) {
   [0]=> string(18) "America/Los_Angeles"
  }
}

Once we have obtained that object, we then loop over each key and display the content from the process so that we can see what the provider has returned.

Now that we have processed the content from the Simple Registration extension, we can begin to look at the PAPE policy extension values.

Checking the PAPE policy states

Depending on the support the provider offers for PAPE policies and what we designated at the beginning of our OpenID example, we can display the PAPE policy responses from the provider to see how they affected the OpenID process:

//display pape policy return data if available
$response_pape = Auth_OpenID_PAPE_Response::fromSuccessResponse($response);
if ($response_pape){
   //pape policies affected by authentication
   if ($response_pape->auth_policies){
      $response_state .= "<br />PAPE returned policies which affected
                          the authentication:";

      foreach ($response_pape->auth_policies as $uri){
         $response_state .= '- ' . htmlentities($uri);
      }
   }

   //server authentication age
   if ($response_pape->auth_age){
      $response_state .= "<br />PAPE returned server authentication age with
                          the value: " . htmlentities($response_pape->auth_age);
   }

   //nist authentication level
   if ($response_pape->nist_auth_level) {
      $response_state .= "<br />PAPE returned server NIST auth level with the
                          value: " . htmlentities($response_pape->nist_auth_level);
   }
}

We first call the Auth_OpenID_PAPE_Response::fromSuccessResponse(...) method against our OpenID response object to return our PAPE data. If a PAPE response object exists, we can display the processing information.

We start by checking the policies that affected the authentication process. For each policy found, we display the URI.

Next, we tackle server authentication age. We display the age, if available, that was returned from the provider.

Last, we check the NIST authentication level that was used for the OpenID request. We return back the level that was used, if available.

The final extension that we will process is Attribute Exchange.

Capturing values returned by Attribute Exchange

If we specified that we wanted to use the Attribute Exchange extension in our request, we can easily process the data that is returned from the provider:

   //get attribute exchange return values
   $response_ax = new Auth_OpenID_AX_FetchResponse();
   $ax_return = $response_ax->fromSuccessResponse($response);
   foreach ($ax_return->data as $item => $value){
      $response_state .= "<br />AX returned <b>$item</b> with the value:
                          <b>{$value[0]}</b>";
   }
}

print $response_state;
?>

We fetch the Attribute Exchange structure from the OpenID response object by creating a new instance of Auth_OpenID_AX_FetchResponse and then calling the fromSuccessResponse(...) method against the new instance, passing in the OpenID response object. We should now have an object that contains the Attribute Exchange information that we requested at the beginning of the OpenID process. This object should look something like the following:

array(4) {
  ["http://axschema.org/contact/email"]=> array(1) {
   [0]=> string(17) "[email protected]"
  }
  ["http://axschema.org/namePerson"]=> array(1) {
   [0]=> string(16) "Jonathan LeBlanc"
  }
  ["http://axschema.org/person/gender"]=> array(1) {
   [0]=> string(1) "M"
  }
  ["http://axschema.org/media/image/default"]=> array(1) {
   [0]=> string(111) "https://a323.yahoofs.com/coreid/4ca0e24
             cibc9zws131sp2/VXtMnow7dKiKol09_NI9bAeW
             Ig--/7/tn48.jpeg?ciAgZ3NBvexVYA_D"
  }
}

Once we’ve obtained this object, we loop through each returned element and add it to our response object to be displayed.

Once all OpenID elements and extension structures have been processed for the SUCCESS state, we print out the information to complete the example.

We should now have a functional example that will authenticate the user and capture some general profile information about her.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.75.165