How to integrate OpenID as your login system

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:

  • OpenID: http://remysharp.myopenid.com/ => Identity: remysharp.myopenid.com
  • OpenID: remysharp.myopenid.com => Identity: remysharp.myopenid.com
  • OpenID: http://remysharp.myopenid.com => Identity: remysharp.myopenid.com
  • OpenID: http://remysharp.com => Identity: remysharp.myopenid.com (since remysharp.com is a delegate)
  • etc.

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.

11 Responses to “How to integrate OpenID as your login system”

  1. This is a great overview. One of my frustrations with the OpenID system has been the documentation. Now I’ll see if I can get it working, and perhaps I’ll do a page similar to this one once it’s done. I use flat files for DB type transactions (a throwback to the days when my hosting people didn’t have DBs), so it’ll be slightly different.

    Thanks for putting this up!

  2. Great help for when I’ll be creating a system of my own! Very clear.

    OpenID really is one of the ways forward for ID’s on the web, it seems, and in a strange way a go-back to the days where you are a “trusted person” of a larger organisation that by name-dropping will grant you access everywhere (- like money letters).

  3. This is a nice resource. I think my issue with Open ID is this-> Most, if not all websites need some information about users in the database, information like first name, last name, country, state etc… if users don’t sign up, how would website administrators get these critical information in their database?

  4. Thanks for the great tutorial on integrating OpenID – just what I was looking for. Got OID up and running in 15 minutes – all I’ve got to do now is to make the user creation / login work ;-)

    Just one question. Is it posible to ask the openid serwer to send the data back as POST rather than get? I’m using mod_rewrite and the URL is nasty looking :D

  5. [...] Krótkie googlowanie znalazło klasę do obslugi OpenID na blogu Remy Sharp-a. [...]

  6. hey this is nice one but this article is not overcome my confusion. actually im also working on openid authentication using prebuild lib but i m unable to call them. here im only developing one application take openid url and get redirect to provider website and after authentication will get back to same page.

    does anyone give me idea regarding this.
    plz help me just show me that how can i call API in my application.
    thanks

    bye
    rahul s

  7. Exactly how does User::GetOpenIDServer check the database table ‘user_openids’ ? I don’t understand the syntax ‘User::GetOpenIDServer’. You have a function GetOpenIDServer in your class, but this does not check my database. I would think it would be necessary for me to write a function GetOpenIDServer that looks up the openid_url (if it exists). Confused.

  8. We need more examples of systems featuring OpenID as an OPTION, please!

  9. @Methodise… Proceed logically. Take the same handler for OpenID and add a check to see if your login form was an OpenID request or a standard request which can be done with hidden form values and have your script respond accordingly. If the hidden value says it wasn’t OpenID simply have your script skip the OpenID stuff and follow another routine that fits your alternative method of auth. Easiest way would be to encapsulate the entire OpenID section above in its own if statement and have an else section proceed with your own routine. I know that comment is very old but I figured I’d add what I can.

  10. Sorry for this newbie question but…. where is this “User class” coming from?
    Do I have to include it somewhere? is it part of PHP’s API? Do I have to write it down by myself? Can I find it in some sort of framework? Does it has something to do with PHP’s ini files?

    Hope somebody can help me with this one, even though this thread is more than one year old.

  11. OpenID: http://remysharp.myopenid.com/ => Identity: remysharp.myopenid.com
    WRONG
    OpenID: remysharp.myopenid.com => Identity: remysharp.myopenid.com
    WRONG

    remysharp.myopenid.com would be http://remysharp.myopenid.com/ as if no http or https is added http should be added

    http://remysharp.myopenid.com/ != https://remysharp.myopenid.com/

    there for http / https should be part of the ID

    http and https could in theory be two different servers hosted by two different company’s and or private persons … in such they should be treated as two different users …