I've assumed you know what OpenID is, you're using your own blog as your identity and now you want to offer a way for your users to log in your sexy new webapp using OpenID, or, as I've done in my code experiment Todged use it exclusively for logging in.

However, in developing the log in system for Todged I found there was a lack of walk throughs on the Internet explaining how to plug OpenID in.

Also, this tutorial aims answer the biggest (and simplest) question I had: how do you test OpenID?

OpenID Communication

There's no point in re-inventing the wheel, so use someone else's library. Since I'm working in PHP, I opted for 'SimpleOpenID' class in PHP, however it did not work entirely out of the box.

To get going, here's my copy of SimpleOpenID.class.php.

Database Setup: Say Goodbye to Password Storage

Again, I'm assuming for simplicity that we're using OpenID as our exclusive way in to the web site. For example (my site) Todged does this, and so does another site: Jyte.

The result is the database table that holds the user details doesn't need to hold a password field. It's a tricky concept to get around, but it's refreshing once you get it.

You need all the usual tables you would have in place, with the following additionals:

alter table user_profile add column identity char(255) not null;

create table user_openids
(
  id int not null auto_increment,
  identity char(255) not null, # this our key field
  openid char(255) not null,
  server char(255) not null,

  primary key (id),
  index openid (openid)
);

The identity field on user_profile (or what ever you name your table) is the key to allowing users to have multiple OpenIDs pointing to one single identity.

The user_openids is important because it will allow the following (types of) relationships:

As you can see, since the OpenID is a URI, it's possible for a range of different OpenID URIs representing one single identity.

The user_openids table will allow us to store the relationships and save having to lookup the identity against the OpenID each time they log in (which is doing by running an HTTP request).

† Note that this does mean if the user changes the service they host their identity with, they may not be able to log in, so you could argue these rows should have an expiry to them.

OpenID Login Steps

If you're familiar with these steps, skip onwards, otherwise it's worth understanding these steps because it will help you debug any future problems you might hit.

  1. User enters their OpenID.
  2. Server checks to see if the OpenID is a delegate, if so, it finds the source OpenID server and redirects the user as appropriate (i.e. to login and to allow access).
  3. The OpenID will redirect the user back to our server††
  4. Our server will now run a callback to the OpenID server which authenticates the whole process.
  5. If the OpenID responds with 'ok', we'll proceed, otherwise, there was some problem with the log in process.

†† It's important that the domain redirects correctly, otherwise this step won't work. For instance, I had the OpenID server redirecting back to www.todged.com rather than todged.com (without the www) which broke the redirect (since I don't support 'www').

OpenID Login Code

Armed with this information, and the knowledge that we'll have to handle two steps of the login process, here's the code.

Note that this is the code (pretty much) directly from the Todged login process.

The Initial Login Request

$openid = new SimpleOpenID;
// $_REQUEST['openid'] is the input field the user submitted
$openid->SetIdentity($_REQUEST['openid']);

// ApprovedURL is the url we want to call back to
$openid->SetApprovedURL('http://todged.com/login');
$openid->SetTrustRoot('http://todged.com');

// I'm also requesting their email address for the creation of their new profile
$openid->SetOptionalFields('email');

// User::GetOpenIDServer checks the database table 'user_openids' for the
// user's openid and the associated identity, which saves having to run
// a separate HTTP request if it's not available (see else case).
if (list($server, $identity) = User::GetOpenIDServer($_REQUEST['openid'])) {
  $openid->SetOpenIDServer($server);
  $openid->SetIdentity($identity);
} else {
  //
  if ($server = $openid->GetOpenIDServer()) {
    // just used to optimise the process
    $identity = $openid->GetIdentity();

    // we're now creating a relationship between the user's OpenID and their
    // *real* identity which can be used in subsequent logins to save time.
    User::SaveOpenIDServer($_REQUEST['openid'], $server, $identity);
  } else {
    // This shouldn't happen - but will if there's something fundamentally wrong
    // with our request.  Examples in Debugging section of this tutorial.
    user_error('Something has gone wrong with OpenID identity request process');
  }
}

// send the user to their OpenID provider for authentication
$openid->Redirect();

Handling the OpenID Server Response

$openid = new SimpleOpenID;
$identity = $_GET['openid_identity'];

$openid->SetIdentity($identity);
$ok = $openid->ValidateWithServer();

if ($ok) {
  /**
   * Tasks:
   * If user doesn't exist (tested by LoadByOpenID) - create an account
   * If they're new - send them to an activate page with appropriate captcha logic
   * If they existed already, redirect to their home page
   */

  // tries to load a user profile using their openid identity,
  // standard stuff, that would normally be by username.
  if (!$User->LoadUserByOpenID($identity)) {
    // create a new user
    $User->CreateUserFromOpenID($identity, $_GET['openid_sreg_email']);

    // ask the user, as a once off, to prove they're human.
    Utility::Redirect('/activate');
  } else {
    // redirect the user to their home page
    Utility::Redirect('/' . $User->username);
  }
} else if ($openid->IsError() == true) {
  // There was a problem logging in.  This is captured in $error (do a var_dump for details)
  $error = $openid->GetError();

  $msg = "OpenID auth problem\nCode: {$error['code']}\nDescription: {$error['description']}\nOpenID: {$identity}\n";

  // error message handling is done further along in the code, but ensure the user
  // can pass on as much information as possible to replicate the bug.
} else {
  // General error, not due to comms
  $Error = 'Authorisation failed, please check the credentials entered and double check the use of caplocks.';
}

How to Test OpenID

Though it's not detailed anywhere, it's actually simple. There's nothing to do or configure or anything. Testing will work, so long as the OpenID server can redirect the browser back to your server, be it online or offline.

Problems I Encountered

I've provided my own copy of SimpleOpenID.class.php because I had to patch it to work with all the examples on the OpenID site.

The OpenID site provides a good list of examples where a user may already have an OpenID. So I used this as my test cases.

WordPress OpenID Problems

This bug might not just be exclusive to SimpleOpenID, since WordPress HTML isn't totally valid XHTML the parser looking for the 'openid.server' field didn't match.

It's because WordPress includes the OpenID as:

<link rel='openid.server' href='http://remysharp.wordpress.com/?openidserver=1' />

Rather than:

<link rel="openid.server" href="http://remysharp.wordpress.com/?openidserver=1" />

The trick is to make sure your parser isn't picky about XHTML.

The second problem I had with WordPress was that WordPress's OpenID server absolutely requires a trailing slash on the identity.

For example, sending http://remysharp.wordpress.com as the identity doesn't work. However, http://remysharp.wordpress.com/ does work.

Technorati OpenID Problems

From the examples over at openid.net/get/ is says Technorati supports OpenID with all accounts. However, they're very strict about the OpenID.

For example, the OpenID technorati.com/people/technorati/remysharp wouldn't work, and would serve a Technorati page saying the content had be lost.

It took me a while to realise the 'http://' part was mandatory. So http://technorati.com/people/technorati/remysharp does work.

Wrap Up

Hopefully this has helped anyone worried about the requirements of adding OpenID to their site, and I'd love to see more sites using it.