The full code for this sample is available at https://github.com/jcleblanc/programming-social-applications/tree/master/chapter_11/openid-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.
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.
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"> <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.
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:
OpenID consumer code
The functionality to store
The Simple Registration extension that enables us to obtain simple profile information about the user
The PAPE policy definition file that enables us to use the associated functionality
The Attribute Exchange file that enables us to obtain extended public profile information about the user
The next block contains our global path definitions:
The filename (under the APP_ROOT folder) where the provider should forward the user once she has logged in to the provider
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.
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.
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.
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.
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.
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.
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.
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.
3.16.75.165