Using Zend_OpenId_Provider

Zend Framework's Zend_OpenId_Consumer makes accepting OpenID logins on your site pretty easy, but its companion, Zend_OpenId_Provider is a little harder to get on with.

There's a few reasons for this:

1 - As per the philosophy of Zend Framework, you're given key parts of the tool, but have to fill in a lot yourself.

2 - There's a lot more data to manage and store for a Provider than a Consumer, and the OpenID system deliberately puts the complexity on the Provider side.

3 - The documentation of Zend_OpenId_Provider is, frankly, not too hot. To quote:
"Building OpenID servers is less usual tasks then building OpenID-enabled sites, so this manual don't try to cover all Zend_OpenId_Provider features as it was done for Zend_OpenId_Consumer."

The documentation is, unfortunately, written in somewhat stilted english, and the examples given are over-simplified and not altogether clear.

Now, there's no point simply ranting about this, as someone will quite reasonably request more detail...

So, given that ZF is a community effort, and that I've managed to work out enough of the code to get a basic provider running, here's a quick guide on how it seems to work:

As per the flowchart at http://framework.zend.com/manual/en/zend.openid.html#zend.openid.introduction.how, it's a multi-step process:

First, the user requests a login to the consumer site by provide an OpenID URI.
The consuming server checks the page at that URI for a server delegation metatag, and then contacts the server this identifies (optionally following a further delegation chain).

The providing server must accept a direct HTTP request from the consuming server and call:
$server = new Zend_OpenId_Provider; // Instantiate server with default settings
echo $server->handle(); // By default, look in $_GET for openID data and respond to it.

This can be done as for any other page so long as you only ouput the return value.

Unfortunately, this default behaviour will simply return an OpenID CANCEL command to the consumer, because:
- The Provider will call self::hasUser($id), which in turn calls Zend_OpenId_Provider_Storage->hasUser($id)
- The default storage mechanism has no users until configured (hence the hack in the first example in the docs)
- Failing to locate the user causes Zend_OpenId_Provider to decline the request.

So, we need to provide a form of storage that will respond 'true' for an ID we should be able to serve.

The very simplest way to do this is to subclass Zend_OpenId_Provider_Storage_File and pass that in to the provider class's constructor (it accepts any object inheriting from Zend_OpenId_Provider_Storage as a 4th parameter, but uses the file variant if none is provided).

The only function you'll need to override at this time is Zend_OpenId_Provider_Storage::hasUser($id). My personal quick hack was:

public function hasUser($id){
return ($id==='http://myuser.openid.server.com/');
}


Obviously for production you'll want to do something a bit smarter, but for now it's enough to give you the idea.
You could also try to use somthing like the documentation hack to register an id in the file storage, but I'd rather make ZF work my way.

So, if the ID is known (even if it has no relation to the current provider site visitor), the Provider will associatte with the Consumer in order to be able to confirm the identity securely. This association is stored by Zend_OpenId_Provider_Storage::addAssociation(), but for now I'm letting the Zend_OpenId_Provider_Storage_File handle this.

Once it's associated, the Provider will (I believe) see if it's aware of the owner of that identity being logged into itself (via a prior call to Zend_OpenId_Provider::login()). If so, it will move on to confirming that the user trusts the consuming site (ie, is happy to confirm their identity to it).
If the user is not logged in to the provider (NOT the same as being logged into the providing SITE), the user's browser will be directed to the provider's login page, as specified as the first parameter in the provider's constructor. Note that if the login URI is not provided to the constructor, a default URI is used as per the documentation.

This login page should verify the user's identity by whatever means the providing site already uses, then call a redirect to the trust page (the second constructor parameter). Note however that Zend_OpenId_Provider is written to expect that you log into itself directly (by username and password) and is written in a way that makes it hard to write the Zend_OpenId_Provider_Storage::checkUser function that needs to validate the username and password. Unusually for Zend Framework code, the encapsulation here is too weak, and developers will often need to replace or sidestep this particular bit of functionality.

Once the trust page is reached, the provider needs to verify that the user is happy for the consumer to know their identity. There are various options here; if you use the full built-in abilities of the provider, you may have previously told it "This site is always OK" or "This site is never OK". It should use that data (as stored by the Storage, IIRC) at this point if available, otherwise it will display the Trust page.

Note - there's a *lot* going on under the hood of the provider, and without testing every route I'm not exactly sure of program flow! For this guide, I'm covering the basic operation, and the principles, but for trust storage you'll have to experiment for now. It seems likely that if the user is already logged in to the Provider, and has designated the consuming site as "Trust Always", then Zend_OpenId_Provider will not try to display either the login or trust pages.

Finally, whatever method you use to verify trust, if you the user agrees to it, you then need to call
$server->respondToConsumer($_GET);
which will be the Provider's last act, confirming your identity and returning you to the consuming site.


To summarise the code required (we'll assume an MVC environment; this code isn't tested as-is):



class OpenIdServerController extends Zend_Controller_Action {
function init()
{
$this->_provider = new Zend_OpenId_Provider(
'/openid/server/login', // login path
'/openid/server/trust', // trust path
null, // default user session manager
new My_OpenId_Provider_Storage() // our tweaked storage class
);
}

function serverAction() //This is called first, by the consuming server
{
echo $this->_provider->handle(); // handle 'GET' params by default
exit; // Don't render views, etc
}

function loginAction() // The user is directed to this page if they're not logged in to Zend_OpenId_Provider
{
if(/* we can verify this user */) {
//optionally call $this->_provider->login() to maintain user identity on providing server
Zend_OpenId::redirect("/openid/server/trust", $_GET);
} else {
// Display login form and post back to this page
}
}

function trustAction() // The user is sent to this page after being logged in
{
if(/* we have permission to trust this consumer */) {
$this->_provider->respondToConsumer($_GET);
} else {
// Display a form asking whether to trust this consumer; post back to this page
}
}
}

Refer to the ZF manual for suggested forms.

Our hacked My_OpenId_Provider_Storage just looks like:
class My_OpenId_Provider_Storage extends Zend_OpenId_Provider_Storage_File
{
public function hasUser($id){
return ($id==='http://myuser.openid.server.com/');
}
}



NOTE that this process basically expects the user to log in, or be logged in, and to re-verify trust each time. Using the "Provider Sessions" is beyond the scope of this guide.
Posted by parsingphase, 2008-11-21 12:17

Anonymous user

Login

Blog

Contact

I'm currently available for contract work in London as a Senior PHP Developer. Contact me for a CV, rates, or a chat.

Twitter @parsingphase
Email richard@phase.org
Github parsingphase
LinkedIn Richard George
Flickr parsingphase