Zend_OpenId_Consumer
is used to implement the OpenID
authentication schema on web sites.
From a site developers point of view, the OpenID authentication process consists of three steps:
Show OpenID authentication form.
Accept OpenID identity and pass it to the OpenID provider.
Verify response from the OpenID provider.
In actual fact the OpenID authentication protocol performs more
steps, but most of them are encapsulated inside the
Zend_OpenId_Consumer
, and they are transparent to the
developer.
The OpenID authentication process is initiated by the end-user by filling in their identification into the appropriate form and submiting it. The following example shows a simple form that accepts an OpenID identifier. Note that the example shows only a login.
Пример 29.1. The Simple OpenID Login form
<html><body> <form method="post" action="example-1_2.php"><fieldset> <legend>OpenID Login</legend> <input type="text" name="openid_identifier"> <input type="submit" name="openid_action" value="login"> </fieldset></form></body></html>
On submit this form passes the OpenID identity to the following PHP
script that performs a second step of authentication. The only thing the
PHP script needs to do in this step is call the
Zend_OpenId_Consumer::login()
method. The first argument of this
method is an accepted OpenID identity and the second is a URL of a script
that handles the third and last step of authentication.
Пример 29.2. The Authentication Request Handler
<?php require_once "Zend/OpenId/Consumer.php"; $consumer = new Zend_OpenId_Consumer(); if (!$consumer->login($_POST['openid_identifier'], 'example-1_3.php')) { die("OpenID login failed."); }
The Zend_OpenId_Consumer::login()
performs discovery on
a given identifier and on success, finds out the address of the identity
provider and its local identifier. Then, it creates an association to the
given provider so that both the site and provider know the same secret
that is used to sign the following messages. Then it passes an
authentication request to the provider. Note this request redirects the
end-user's web browser to an OpenID server site, where users are able to
continue the authentication process.
An OpenID Server usually asks users for; their password (if they weren't previously logged-in), if the user trusts this site and what information may be returned to the site. These interactions are not visible to the OpenID-enabled site so there is no what for it to get the user's password or other information that was not opened.
On success, Zend_OpenId_Consumer::login()
never
returns, because it performs an HTTP redirection, however in case of error
it may return false. Errors may occure due to an invalid identity, dead
provider, communication error, etc
The third step of authentication is initiated by a response from the OpenID provider, after it has already authenticated the user's password. This response is passed indirectly, as an HTTP redirection of the end-user's web browser. And the only thing that site must do is to check if this response is valid.
Пример 29.3. The Authentication Response Verifier
<?php require_once "Zend/OpenId/Consumer.php"; $consumer = new Zend_OpenId_Consumer(); if ($consumer->verify($_GET, $id)) { echo "VALID $id"; } else { echo "INVALID $id"; }
This check is performed using the Zend_OpenId_Consumer::verify
method, that takes the whole array of
the HTTP request's arguments and checks if this response is properly
signed by an appropriate OpenID provider. It also may assign
the claimed OpenID identity that was entered by end-user in the
first step into the second (optional) argument.
The following example combines all three steps together. It doesn't provide any additional functionality. The only advantage is that now developers don't need to specify any URL's of scripts that handle the next step. By default, all steps use the same URL. However, the script now includes a dispatch code that calls appropriate code for each step of authentication.
Пример 29.4. The Complete OpenID Login Script
<?php require_once "Zend/OpenId/Consumer.php"; $status = ""; if (isset($_POST['openid_action']) && $_POST['openid_action'] == "login" && !empty($_POST['openid_identifier'])) { $consumer = new Zend_OpenId_Consumer(); if (!$consumer->login($_POST['openid_identifier'])) { $status = "OpenID login failed.<br>"; } } else if (isset($_GET['openid_mode'])) { if ($_GET['openid_mode'] == "id_res") { $consumer = new Zend_OpenId_Consumer(); if ($consumer->verify($_GET, $id)) { $status = "VALID $id"; } else { $status = "INVALID $id"; } } else if ($_GET['openid_mode'] == "cancel") { $status = "CANCELED"; } } ?> <html><body> <?php echo "$status<br>";?> <form method="post"><fieldset> <legend>OpenID Login</legend> <input type="text" name="openid_identifier" value=""> <input type="submit" name="openid_action" value="login"> </fieldset></form></body></html>
In addition, this code differenciates between canceled and wrong authentication responses. The provider retuns a canceled responce in cases when an identity provider doesn't know the supplied identity or the user is not logged-in or they don't trust the site. A wrong response assumes that the responce is wrong or incorrectly signed.
When an OpenID-enabled site passes authentication requests to a provider, it identifies itself with a realm URL. This URL may be considered as a root of a trusted site. If the user trusts the URL they will also trust to matched and subsequent URLs.
By default, the realm URL is automatically set to the URL of the directory where the login script is. This decision is useful for most, but not all cases. Sometimes a whole site and not directory is used, or even a combination of several servers from one domain.
To implement this ability, developers may pass the realm value as a
third argument to the Zend_OpenId_Consumer::login
method. In
the following example the single interaction asks for trusted access to
all php.net sites.
Пример 29.5. Authentication Request for Specified Realm
<?php require_once "Zend/OpenId/Consumer.php"; $consumer = new Zend_OpenId_Consumer(); if (!$consumer->login($_POST['openid_identifier'], 'example-3_3.php', 'http://*.php.net/')) { die("OpenID login failed."); }
The example below only implements the second step of authentication, the first and third steps are the same as in the first example.
In some situations it is necissary to see if a user is already
logged-in into a trusted OpenID server without any interaction with the
user. The Zend_OpenId_Consumer::check
method does precisely
that. It is executed with exactly the same arguments as
Zend_OpenId_Consumer::login
but it doesn't show the user any
OpenID server pages. Therefore from the users point of view it is
transparent and it seems as if they never left the site. The third step
succeedes if user is already logged-in and trusted to the site otherwise
it will fail.
Пример 29.6. Immediate Check without Interaction
<?php require_once "Zend/OpenId/Consumer.php"; $consumer = new Zend_OpenId_Consumer(); if (!$consumer->check($_POST['openid_identifier'], 'example-4_3.php')) { die("OpenID login failed."); }
The example below only implements the second step of authentication, first and third steps are the same as in the first example.
There are three steps to the OpenID authentication procedure, each
step is performed by a separate HTTP request. To store information between
requests Zend_OpenId_Consumer
uses internal storage.
Developers may not care about this storage because by default
Zend_OpenId_Consumer
uses file-based storage under /tmp
similar to PHP sessions. However, this storage may be not suitable in all
cases. Some may want to store information in a database while others may
need to use common storage suitable for big web-farms. Fortunately,
developers may easily replace the default storage with their own. The only
thing to implement is it's own storage class as a child of
the Zend_OpenId_Consumer_Storage
method and pass it as a first
argument to the Zend_OpenId_Consumer
constructor.
The following example demonstrates a simple storage that uses
Zend_Db
as the backend containing three groups of functions.
the first is for working with associations, the second is to cache
discovery information and the third is to check responce uniqueness. The
class is implemented in such a way that it can be easily used with
existing or new databases. If necessary, it will create database tables if
they don't exist.
Пример 29.7. Databse Storage
<?php class DbStorage extends Zend_OpenId_Consumer_Storage { private $_db; private $_association_table; private $_discovery_table; private $_nonce_table; public function __construct($db, $association_table = "association", $discovery_table = "discovery", $nonce_table = "nonce") { $this->_db = $db; $this->_association_table = $association_table; $this->_discovery_table = $discovery_table; $this->_nonce_table = $nonce_table; $tables = $this->_db->listTables(); if (!in_array($association_table, $tables)) { $this->_db->getConnection()->exec( "create table $association_table (" . " url varchar(256) not null primary key," . " handle varchar(256) not null," . " macFunc char(16) not null," . " secret varchar(256) not null," . " expires timestamp" . ")"); } if (!in_array($discovery_table, $tables)) { $this->_db->getConnection()->exec( "create table $discovery_table (" . " id varchar(256) not null primary key," . " realId varchar(256) not null," . " server varchar(256) not null," . " version float," . " expires timestamp" . ")"); } if (!in_array($nonce_table, $tables)) { $this->_db->getConnection()->exec( "create table $nonce_table (" . " nonce varchar(256) not null primary key," . " created timestamp default current_timestamp" . ")"); } } public function addAssociation($url, $handle, $macFunc, $secret, $expires) { $table = $this->_association_table; $secret = base64_encode($secret); $this->_db->query("insert into $table (url, handle, macFunc, secret, expires) " . "values ('$url', '$handle', '$macFunc', '$secret', $expires)"); return true; } public function getAssociation($url, &$handle, &$macFunc, &$secret, &$expires) { $table = $this->_association_table; $this->_db->query("delete from $table where expires < " . time()); $res = $this->_db->fetchRow("select handle, macFunc, secret, expires from $table where url = '$url'"); if (is_array($res)) { $handle = $res['handle']; $macFunc = $res['macFunc']; $secret = base64_decode($res['secret']); $expires = $res['expires']; return true; } return false; } public function getAssociationByHandle($handle, &$url, &$macFunc, &$secret, &$expires) { $table = $this->_association_table; $this->_db->query("delete from $table where expires < " . time()); $res = $this->_db->fetchRow("select url, macFunc, secret, expires from $table where handle = '$handle'"); if (is_array($res)) { $url = $res['url']; $macFunc = $res['macFunc']; $secret = base64_decode($res['secret']); $expires = $res['expires']; return true; } return false; } public function delAssociation($url) { $table = $this->_association_table; $this->_db->query("delete from $table where url = '$url'"); return true; } public function addDiscoveryInfo($id, $realId, $server, $version, $expires) { $table = $this->_discovery_table; $this->_db->query("insert into $table (id, realId, server, version, expires) " . "values ('$id', '$realId', '$server', $version, $expires)"); return true; } public function getDiscoveryInfo($id, &$realId, &$server, &$version, &$expires) { $table = $this->_discovery_table; $this->_db->query("delete from $table where expires < " . time()); $res = $this->_db->fetchRow("select realId, server, version, expires from $table where id = '$id'"); if (is_array($res)) { $realId = $res['realId']; $server = $res['server']; $version = $res['version']; $expires = $res['expires']; return true; } return false; } public function delDiscoveryInfo($id) { $table = $this->_discovery_table; $this->_db->query("delete from $table where id = '$id'"); return true; } public function isUniqueNonce($nonce) { $table = $this->_nonce_table; try { $ret = $this->_db->query("insert into $table (nonce) values ('$nonce')"); } catch (Zend_Db_Statement_Exception $e) { return false; } return true; } public function purgeNonces($date=null) { } } $db = Zend_Db::factory('Pdo_Sqlite', array('dbname'=>'/tmp/openid_consumer.db')); $storage = new DbStorage($db); $consumer = new Zend_OpenId_Consumer($storage);
The example doesn't include OpenID authentication code itself, but it is based on the same logic as in the previous or following examples.
In addition to authentication, the OpenID can be used for light-weight profile exchange. This feature is not covered by OpenID authentication specification but by the OpenID Simple Registration Extension protocol. This protocol allows OpenID-enabled sites to ask for information about an end-user from OpenID providers. Such information may include:
nickname - any UTF-8 string that the end user wants to use as a nickname.
email - the email address of the end user as specified in section 3.4.1 of RFC2822.
fullname - a UTF-8 string representation of the end user's full name.
dob - the end user's date of birth as YYYY-MM-DD. Any values whose representation uses fewer than the specified number of digits should be zero-padded. The length of this value must always be 10. If the end user does not want to reveal any particular component of this value, it must be set to zero. For instance, if a end user wants to specify that his date of birth is in 1980, but not the month or day, the value returned shall be "1980-00-00".
gender - the end user's gender, "M" for male, "F" for female.
postcode - UTF-8 string that should conform to the end user's country's postal system.
country - the End User's country of residence as specified by ISO3166.
language - end User's preferred language as specified by ISO639.
timezone - ASCII string from TimeZone database. For example, "Europe/Paris" or "America/Los_Angeles".
An OpenID-enabled web site may ask for any combination of these
fields. It may also strictly require some information and allow end-users
to provide or hide other information. The following example creates an
object of the Zend_OpenId_Extension_Sreg
class that requires
a nickname and optionally ask for
email and fullname.
Пример 29.8. Sending Requests with a Simple Registration Extension
<?php require_once "Zend/OpenId/Consumer.php"; require_once "Zend/OpenId/Extension/Sreg.php"; $sreg = new Zend_OpenId_Extension_Sreg(array( 'nickname'=>true, 'email'=>false, 'fullname'=>false), null, 1.1); $consumer = new Zend_OpenId_Consumer(); if (!$consumer->login($_POST['openid_identifier'], 'example-6_3.php', null, $sreg)) { die("OpenID login failed."); }
As you can see the Zend_OpenId_Extension_Sreg
constructor accepts an array of asked fields. This array has the names of
fields as indexes and requirements flag as values.
true means the field is required and
false means the field is optional. The
Zend_OpenId_Consumer::login
accepts extensions or list of
extensions as a fourth argument.
On the third step of authentication, the
Zend_OpenId_Extension_Sreg
object should be passed to
Zend_OpenId_Consumer::verify
. Then on successful authentication
Zend_OpenId_Extension_Sreg::getProperties
will return an
associative array of requested fields.
Пример 29.9. Verifying Responses with a Simple Registration Extension
<?php require_once "Zend/OpenId/Consumer.php"; require_once "Zend/OpenId/Extension/Sreg.php"; $sreg = new Zend_OpenId_Extension_Sreg(array( 'nickname'=>true, 'email'=>false, 'fullname'=>false), null, 1.1); $consumer = new Zend_OpenId_Consumer(); if ($consumer->verify($_GET, $id, $sreg)) { echo "VALID $id<br>\n"; $data = $sreg->getProperties(); if (isset($data['nickname'])) { echo "nickname: " . $data['nickname'] . "<br>\n"; } if (isset($data['email'])) { echo "email: " . $data['email'] . "<br>\n"; } if (isset($data['fullname'])) { echo "fullname: " . $data['fullname'] . "<br>\n"; } } else { echo "INVALID $id"; }
If Zend_OpenId_Extension_Sreg
was created without any
arguments, the user code should check for the existence of the required
data itself. However, if the object is created with the same list of
required fields as on the second step, it will automatically check for the
existence of required data. In this case, Zend_OpenId_Consumer::verify
will return false if any of the required fields are
missing.
By default, Zend_OpenId_Extension_Sreg
uses version
1.0, because the specification for version 1.1 is not yet finalized.
However, some libraries don't fully support version 1.0. For example,
www.myopenid.com requires an SREG namespace in requests which is only
available in 1.1. To work with this server, explicitly set the version to
1.1 in the Zend_OpenId_Extension_Sreg
constructor.
The second argument of the Zend_OpenId_Extension_Sreg
constructor is a policy URL, that should be provided to the end-user by
the identity provider.
Zend Framework provides a special class to support user
authentication - Zend_Auth
. This class can be used together
with Zend_OpenId_Consumer
. The following example shows how
OpenIdAdapter
implements
the Zend_Auth_Adapter_Interface
with the
authenticate
method.This performs an authentication query and
verification.
The big difference between this adapter and existing ones, is that it works on two HTTP requests and includes a dispatch code to perform the second or third step of OpenID authentication.
Пример 29.10. Zend_Auth Adapter for OpenID
<?php require_once "Zend/OpenId/Consumer.php"; require_once "Zend/Auth.php"; require_once "Zend/Auth/Adapter/Interface.php"; class OpenIdAdapter implements Zend_Auth_Adapter_Interface { private $_id = null; public function __construct($id = null) { $this->_id = $id; } public function authenticate() { $id = $this->_id; if (!empty($id)) { $consumer = new Zend_OpenId_Consumer(); if (!$consumer->login($id)) { $ret = false; $mdg = "Authentication failed."; } } else { $consumer = new Zend_OpenId_Consumer(); if ($consumer->verify($_GET, $id)) { $ret = true; $msg = "Authentication successful"; } else { $ret = false; $msg = "Authentication failed"; } } return new Zend_Auth_Result($ret, $id, array($msg)); } } $status = ""; $auth = Zend_Auth::getInstance(); if ((isset($_POST['openid_action']) && $_POST['openid_action'] == "login" && !empty($_POST['openid_identifier'])) || isset($_GET['openid_mode'])) { $adapter = new OpenIdAdapter(@$_POST['openid_identifier']); $result = $auth->authenticate($adapter); if ($result->isValid()) { Zend_OpenId::redirect(Zend_OpenId::selfURL()); } else { $auth->clearIdentity(); foreach ($result->getMessages() as $message) { $status .= "$message<br>\n"; } } } else if ($auth->hasIdentity()) { if (isset($_POST['openid_action']) && $_POST['openid_action'] == "logout") { $auth->clearIdentity(); } else { $status = "Yoy are logged-in as " . $auth->getIdentity() . "<br>\n"; } } ?> <html><body> <?php echo "$status";?> <form method="post"><fieldset> <legend>OpenID Login</legend> <input type="text" name="openid_identifier" value=""> <input type="submit" name="openid_action" value="login"> <input type="submit" name="openid_action" value="logout"> </fieldset></form></body></html>
With Zend_Auth
the end-user's identity is saved in the
session's data. It may be checked with Zend_Auth::hasIdentity
and Zend_Auth::getIdentity
.
Finally a couple of words about integration into
Model-View-Controller applications. Such Zend Framework applications are
implemented using the Zend_Controller
class and they use
objects of the Zend_Controller_Response_Http
class to prepare
HTTP responses and send them back to the end user's web-browser.
Zend_OpenId_Consumer
doesn't provide any GUI
capabilities but it performs HTTP redirections on success of
Zend_OpenId_Consumer::login
and
Zend_OpenId_Consumer::check
. These redirections, may work
incorrectly or not work at all if some data was already sent to the
web-browser. To properly perform HTTP redirection in MVC code the real
Zend_Controller_Response_Http
should be sent to
Zend_OpenId_Consumer::login
or
Zend_OpenId_Consumer::check
as the last argument.