Creating an OpenID client for PHP

Open ID logo

Many people are registered for many services with different usernames, passwords etc.. There have been a few attempts to make a central id system, the one that is probably the most famous is Microsofts Passport service (which then changed names to .NET accounts, which changed names to Live account). The Passport service had a lot of trouble taking off and failed miserably in its goals.

Recently there have been attempts to create an open decentralized identity system. The most well-known one is OpenID. With OpenID you identify yourself with an URL you own. The webapplication then fetches information to identify you and sends you to a page where you can login and allow the webapplication to log you in. This means as long as the application supports OpenID, you only need to use 1 identification for those applications. 1 centralized place to change your password and 1 ID provider you have to trust.

The biggest player currently using this is LiveJournal. They also developed the initial spec of openid. Verisign also recently jumped on the bandwagon..

In this article I will show how to create an OpenId client. Before you start its recommended to create an account somewhere that supports OpenID.. else you will have nothing to test with. You can do this at for example LiveJournal.com or claimid.com

After you did that, you might also want to try logging in somewhere, just so you know how the procedure works from a client perspective.

The basics

Three parties are involved in OpenID; the client (user or browser), the consumer site (the place where a user wants to log in), and the identity provider (the server thats hosts your identity).

These are the steps for the logging in process.

  1. User fills in his identity url
  2. Consumer site fetches the url and tries to find the identity provider address.
  3. Consumer site associates itself with the identity provider and retrieves a key.
  4. Consumer site (http-)redirects the user to the identity provider.
  5. If the user is not logged into the identity provider, it will do this now.
  6. User allows (or disallows) the consumer site to have access to the identity.
  7. Identity provider redirects the user back to the consumer site.
  8. Consumer site matches the key in the url with the key he received from the identity provider and logs in the user.

The html

I will just go through the process step by step, and the first thing is the login box.

The box only has one field, which is the (url) identifier. There are 2 things you have to consider. First, there’s a small logo in the input field, which allows people to reconize this loginbox as an openid one, and a naming convention for the name=”” attribute, which should be: “openid_url”. This allows browsers to autocomplete the id url (as long as people stick to the convention).

Here’s an example of how it should look like:

And the code:

<form action="/openidlogin1.php" method="post">
  <input type="text" class="openid_url" name="openid_url" />
  <input type="submit" value="login" />
</form>

The css:

.openid_url {
    padding-left: 18px;
    background-image: url('http://openid.net/login-bg.gif');
    background-repeat: no-repeat
 }

Finding the provider url

Once the user has entered their id, and pressed ‘login’ we enter the PHP realm, and there’s a bunch of things we need to detect at this point.

First, we fetch the html page, then we check out the <head> section and look for <link&ht; tags, which, in the case of livejournal, could look like this:

<link rel="openid.server" href="http://www.livejournal.com/openid/server.bml">
<link rel="openid.delegate" href="http://exampleuser.livejournal.com/">

openid.server tells us the url where we should submit our API calls to. If opeid.delegate is in this page, the authentication is delegated to a different identity and we should fetch that page instead. You could see ‘delegate’ as an alias for your account.

I am not going to show you how you can exactly find and parse html elements, instead we are going to use the MetaDetector library. This is a lightweight library to find this kind of meta data from pages. If you want an example of how this library works, check out /blogdetect and fill in your openid identity.

After you downloaded and unpacked the MetaDetector library, place the ‘Sabre’ directory in the same directory as your script, or some directory on the server that is included by the PHP.INI setting include_path.

First, a code snippet that will show how to find the openid meta data in the identity url.

<?php

// We need the metaparser
require_once 'Sabre/Web/MetaParser.php';

// The url from the form
$url = $_REQUEST['openid_url'];

// We create a metaparser
$mp = new Sabre_Web_MetaParser($url);

// And execute
$metaData = $mp->exec();

?>

We now have the information we need in the $metaData variable. But, in some cases we will find openid.delegate information, and we have to accommodate for that. This code replaces the first code snippet.

<?php

// We need the metaparser
require_once 'Sabre/Web/MetaParser.php';

// The identity
$idUrl = $_REQUEST['openid_url'];

// The url we're gonna authenticate with
// (if there was delegation this url will be different)
$authUrl = $url;

// We need to add a slash after the url where there is just a domain..
// This is not in the spec, but without it, some identity providers seem to require it (livejournal for example).
$url = parse_url($authUrl);
$authUrl = $url['scheme'] . '://' . $url['host'];
if (isset($url['port'])) $authUrl.=':' . $url['port'];
if (isset($url['path'])) $authUrl.=$path; else $id.='/';
if (isset($url['query'])) $authUrl.='?' . $url['query'];

function findProviderURL(&$id_url) {

  // Create the metaparser
  $mp = new Sabre_Web_MetaParser($id_url);

  // Execute
  $metaData = $mp->exec();

  // Do we have a delegation?
  if (isset($metaData['openid.delegate'])) {

    $id_url = $metaData['openid.delegate'];
    return findProviderURL($metaData['openid.delegate']);

  }

  if (!isset($metaData['openid.server'])) {

     // Error handling here, we didnt find openid information

  } else return $metaData['openid.server'];

}

// Find the provider url
$providerURL = findProviderURL($authUrl);
?>

Getting the provider secret

Now we need to find the provider secret by doing a post request to the server. Along with the MetaDetector library there was also a small class to do http requests, so we’re gonna use that. (append this to the previous code snippet)

<?php

function doPostRequest($vars) {

    $r = new Sabre_HTTP_Request($this->serverUrl);
    $r->setRequestType('POST');
    $r->setRequestContentType('application/x-www-form-urlencoded');

    $vars2 = array();
    foreach($vars as $k=>$v) $vars2[] = $k.'=' . urlencode($v);

    $r->setRequestData(implode('&',$vars2));
    $data = $r->exec();
    $data = explode("\n",$data);
    $rvars = array();
    foreach($data as $l) {

        if (!$l) break;
        $yo = explode(':',$l,2);
        $rvars[$yo[0]] = $yo[1];

    }

    return $rvars;

}

function getProviderData() {

    // This sends an associate requests, which should return the provider secret
    return doPostRequest(array(
        'openid.mode' => 'associate'
    ));

}

$providerData = getProviderData();

?>

The next thing we need to do, is send off the user to the provider url, and there are a few things we have to pass along.

openid.modeThis has to be 'checkid_setup'.
openid.identityThis is the identification url we want to authenticate.
openid.return_toThis is the url we want the user to return to after authentication
openid.trust_rootThis is our domain, it means we will authenticate the user for this domain and keep it valid for the entire domain.

To redirect our user here, we need the following code-snippet. Append this to the previous code snippet.

<?php

  // Append this to the end of the previous script
  // The domain we want to authenticate (change this)
  $domain = 'http://www.rooftopsolutions.nl';

  // Where the user should land after authentication
  //(should be in the same domain as $domain)

  $returnto = $domain . '/openidlogin2.php';
  /* We want to make sure the user that tried to
     login is the same as the user that will go to the landing page.
     This is why we save some of the information in session variables
     to check later on.

     You are required to check yourself if cookies are supported on the client!
  */

  $_SESSION['openid.authUrl']  = $authUrl;
  $_SESSION['openid.idUrl'] = $idUrl;
  $_SESSION['openid.confirmed'] = false;

  $location = $providerURL . '?openid.mode=checkid_setup';
  $location .= '&openid.identity=' . urlencode($authUrl);
  $location .= '&openid.return_to=' . urlencode($returnto);
  $location .= '&openid.trust_root=' . urlencode($domain);

  header('Location: ' . $location);

?>

When a user authenticated itself (or not) it will be redirected to openidlogin2.php, with a bunch of _GET variables. We now need to check those variables to see if things worked out or not.

<?php


?>