Previous Next

Zend_OpenId_Consumer Basics

Zend_OpenId_Consumer is used to implement the OpenID authentication schema on web sites.

OpenID Authentication

From a site developers point of view, the OpenID authentication process consists of three steps:

  1. Show OpenID authentication form.

  2. Accept OpenID identity and pass it to the OpenID provider.

  3. 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.

Example #1 The Simple OpenID Login form


OpenID Login

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.

Example #2 The Authentication Request Handler

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.

Example #3 The Authentication Response Verifier

verify($_GET, $id)) {
    echo "VALID " . htmlspecialchars($id);
} else {
    echo "INVALID " . htmlspecialchars($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.

Combine all Steps in One Page

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.

Example #4 The Complete OpenID Login Script

login($_POST['openid_identifier'])) {
        $status = "OpenID login failed.
"; } } else if (isset($_GET['openid_mode'])) { if ($_GET['openid_mode'] == "id_res") { $consumer = new Zend_OpenId_Consumer(); if ($consumer->verify($_GET, $id)) { $status = "VALID " . htmlspecialchars($id); } else { $status = "INVALID " . htmlspecialchars($id); } } else if ($_GET['openid_mode'] == "cancel") { $status = "CANCELED"; } } ?> ";?>
OpenID Login

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.

Realm

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.

Example #5 Authentication Request for Specified Realm

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.

Immediate Check

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.

Example #6 Immediate Check without Interaction

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.

Zend_OpenId_Consumer_Storage

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.

Example #7 Databse Storage

_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.

Simple Registration Extension

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.

Example #8 Sending Requests with a Simple Registration Extension

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.

Example #9 Verifying Responses with a Simple Registration Extension

true,
    'email'=>false,
    'fullname'=>false), null, 1.1);
$consumer = new Zend_OpenId_Consumer();
if ($consumer->verify($_GET, $id, $sreg)) {
    echo "VALID " . htmlspecialchars($id) ."
\n"; $data = $sreg->getProperties(); if (isset($data['nickname'])) { echo "nickname: " . htmlspecialchars($data['nickname']) . "
\n"; } if (isset($data['email'])) { echo "email: " . htmlspecialchars($data['email']) . "
\n"; } if (isset($data['fullname'])) { echo "fullname: " . htmlspecialchars($data['fullname']) . "
\n"; } } else { echo "INVALID " . htmlspecialchars($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.

Integration with Zend_Auth

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.

Example #10 Zend_Auth Adapter for OpenID

_id = $id;
    }

    public function authenticate() {
        $id = $this->_id;
        if (!empty($id)) {
            $consumer = new Zend_OpenId_Consumer();
            if (!$consumer->login($id)) {
                $ret = false;
                $msg = "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
\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() . "
\n"; } } ?>
OpenID Login

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.

Integration with Zend_Controller

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.

Previous Next
Introduction to Zend Framework
Présentation
Installation
Zend_Acl
Introduction
Affiner les Contrôles d'Accès
Utilisation avancée
Zend_Auth
Introduction
Authentification avec une table de base de données
Authentification "Digest"
Adaptateur d'authentification HTTP
LDAP Authentication
Authentification OpenID
Zend_Cache
Introduction
La théorie du cache
Les frontends Zend_Cache
Les backends Zend_Cache
Zend_Captcha
Introduction
Captcha Operation
Captcha Adapters
Zend_Config
Introduction
Point de vue théorique
Zend_Config_Ini
Zend_Config_Xml
Zend_Console_Getopt
Introduction à Getopt
Déclarer les règles Getopt
Extraire les options et les arguments
Configurer Zend_Console_Getopt
Zend_Controller
Zend_Controller - Démarrage rapide
Fondations de Zend_Controller
Le contrôleur frontal (Front Controller)
L'objet Requête
Routeur Standard : Zend_Controller_Router_Rewrite
Le dispatcheur
Contrôleurs d'action
Aides d'action (Helper)
Objet de réponse
Plugins
Utilisation de conventions de dossiers modulaires
Exceptions avec MVC
Migrer depuis des versions précédentes
Zend_Currency
Introduction à Zend_Currency
How to work with currencies
Migrer depuis des versions antérieures
Zend_Date
Introduction
Point de vue théorique
Méthodes de base
Zend_Date API Overview
Créer des dates
Constants for General Date Functions
Exemples concrets
Zend_Db
Zend_Db_Adapter
Zend_Db_Statement
Zend_Db_Profiler
Zend_Db_Select
Zend_Db_Table
Zend_Db_Table_Row
Zend_Db_Table_Rowset
Relations Zend_Db_Table
Zend_Debug
Afficher des informations
Zend_Dojo
Introduction
Zend_Dojo_Data: dojo.data Envelopes
Les aides de vues Dojo
Les éléments de formulaire et les décorateurs Dojo
Zend_Dom
Introduction
Zend_Dom_Query
Zend_Exception
Utiliser les exceptions
Zend_Feed
Introduction
Importer des flux
Obtenir des flux à partir de pages Web
Consommer un flux RSS
Consommer un flux Atom
Consommer une entrée Atom particulière
Modifier la structure du flux ou des entrées
Classes personnalisées pour les flux et entrées
Zend_File
Zend_File_Transfer
Validators for Zend_File_Transfer
Zend_Filter
Introduction
Classes de filtre standards
Chaînes de filtrage
Écriture de filtres
Zend_Filter_Input
Zend_Filter_Inflector
Zend_Form
Zend_Form
Zend_Form Quick Start
Creating Form Elements Using Zend_Form_Element
Creating Forms Using Zend_Form
Creating Custom Form Markup Using Zend_Form_Decorator
Standard Form Elements Shipped With Zend Framework
Standard Form Decorators Shipped With Zend Framework
Internationalization of Zend_Form
Advanced Zend_Form Usage
Zend_Gdata
Introduction à Gdata
Authentification par procédé AuthSub
Authentification avec ClientLogin
Using Google Calendar
Using Google Documents List Data API
Using Google Spreadsheets
Using Google Apps Provisioning
Using Google Base
Utiliser l'API YouTube
Utilisation des albums Web Picasa
Attraper les exceptions Gdata
Zend_Http
Zend_Http_Client - Introduction
Zend_Http_Client - Utilisation avancée
Zend_Http_Client - Adaptateurs de connexion
Zend_Http_Cookie and Zend_Http_CookieJar
Zend_Http_Response
Zend_InfoCard
Introduction
Zend_Json
Introduction
Utilisation de base
Objets JSON
XML to JSON conversion
Zend_Json_Server - JSON-RPC server
Zend_Layout
Introduction
Zend_Layout - Démarrage rapide
Zend_Layout options de configuration
Zend_Layout, utilisation avancée
Zend_Ldap
Introduction
Zend_Loader
Charger les fichiers et les classes dynamiquement
Chargeur de Plugins
Zend_Locale
Introduction
Using Zend_Locale
Normalization and Localization
Working with Dates and Times
Supported Languages for Locales
Supported Regions for Locales
Zend_Log
Présentation
Rédacteurs (Writers)
Formateurs (mise en forme)
Filtres
Zend_Mail
Introduction
Envoyer des emails en utilisant SMTP
Envoyer plusieurs emails par connexion SMTP
Utiliser différents transports
Email HTML
Fichiers joints
Ajouter des destinataires
Contrôler les limites MIME
Entêtes additionnelles
Jeux de caractères
Encodage
Authentification SMTP
Sécuriser les transports SMTP
Lire des emails
Zend_Measure
Introduction
Création d'une mesure
Récupérer des mesures
Manipuler des mesures
Types de mesures
Zend_Memory
Présentation
Manager de mémoire
Objet mémoire
Zend_Mime
Zend_Mime
Zend_Mime_Message
Zend_Mime_Part
Zend_OpenId
Introduction
Zend_OpenId_Consumer Basics
Zend_OpenId_Provider
Zend_Paginator
Introduction
Usage
Configuration
Advanced usage
Zend_Pdf
Introduction.
Créer et charger des documents PDF
Sauvegarder les changement dans un document PDF
Les pages d'un document
Dessiner
Informations du document et métadonnées.
Exemple d'utilisation du module Zend_Pdf
Zend_Registry
Utiliser le registre
Zend_Rest
Introduction
Zend_Rest_Client
Zend_Rest_Server
Zend_Search_Lucene
Overview
Building Indexes
Searching an Index
Query Language
Query Construction API
Character Set
Extensibility
Interoperating with Java Lucene
Advanced
Best Practices
Zend_Server
Introduction
Zend_Server_Reflection
Zend_Service
Introduction
Zend_Service_Akismet
Zend_Service_Amazon
Zend_Service_Audioscrobbler
Zend_Service_Delicious
Zend_Service_Flickr
Zend_Service_Nirvanix
Zend_Service_ReCaptcha
Zend_Service_Simpy
Introduction
Zend_Service_StrikeIron
Zend_Service_StrikeIron: Bundled Services
Zend_Service_StrikeIron: Advanced Uses
Zend_Service_Technorati
Zend_Service_Yahoo
Zend_Session
Introduction
Usage basique
Utilisation avancée
Global Session Management
Zend_Session_SaveHandler_DbTable
Zend_Soap
Zend_Soap_Server
Zend_Soap_Client
WSDL Accessor
AutoDiscovery. Introduction
Class autodiscovering.
Functions autodiscovering.
Autodiscovering. Datatypes.
Zend_Test
Introduction
Zend_Test_PHPUnit
Zend_Text
Zend_Text_Figlet
Zend_TimeSync
Introduction
Utiliser Zend_TimeSync
Zend_Translate
Introduction
Adaptateurs pour Zend_Translate
Utiliser les adaptateurs de traduction
Zend_Uri
Zend_Uri
Zend_Validate
Introduction
Classes de validation standard
Chaînes de validation
Ecrire des validateurs
Zend_Version
Lire la version du Zend Framework
Zend_View
Introduction
Scripts de contrôleur
Scripts de vue
Aides de vue
Zend_View_Abstract
Zend_Wildfire
Zend_Wildfire
Zend_XmlRpc
Introduction
Zend_XmlRpc_Client
Zend_XmlRpc_Server
Configuration système requise par le Zend Framework
Version de PHP requise
Extensions PHP
Les composants du Zend Framework
Dépendances internes du Zend Framework
Convention de codage PHP du Zend Framework
Vue d'ensemble
Formatage des fichiers PHP
Conventions de nommage
Style de codage
Informations de copyright