TigerZF
🌐Español

53.2. Aspectos básicos de Zend_OpenId_Consumer

Zend_OpenId_Consumer puede utilizarse para implementar la autenticación OpenID en sitios web.

53.2.1. Autenticación OpenID

Desde el punto de vista del desarrollador de un sitio web, el proceso de autenticación OpenID consta de tres pasos:

  1. Mostrar el formulario de autenticación OpenID

  2. Aceptar la identidad OpenID y pasarla al proveedor OpenID

  3. Verificar la respuesta del proveedor OpenID

El protocolo de autenticación OpenID en realidad requiere más pasos, pero muchos de ellos están encapsulados dentro de Zend_OpenId_Consumer y por lo tanto son transparentes para el desarrollador.

El usuario final inicia el proceso de autenticación OpenID enviando sus credenciales de identificación mediante el formulario apropiado. El siguiente ejemplo muestra un formulario simple que acepta un identificador OpenID. Tenga en cuenta que el ejemplo solo demuestra un inicio de sesión.

Ejemplo 53.1. El formulario de inicio de sesión OpenID simple

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

Este formulario pasa la identidad OpenID al enviarse al siguiente script PHP que realiza el segundo paso de la autenticación. El script PHP solo necesita llamar al método Zend_OpenId_Consumer::login() en este paso. El primer argumento de este método es una identidad OpenID aceptada, y el segundo es la URL de un script que maneja el tercer y último paso de la autenticación.

Ejemplo 53.2. El controlador de la solicitud de autenticación

$consumer = new Zend_OpenId_Consumer();
if (!$consumer->login($_POST['openid_identifier'], 'example-1_3.php')) {
    die("OpenID login failed.");
}

El método Zend_OpenId_Consumer::login() realiza el descubrimiento de un identificador dado, y, si tiene éxito, obtiene la dirección del proveedor de identidad y su identificador local. Luego crea una asociación con el proveedor dado para que tanto el sitio como el proveedor compartan un secreto que se utiliza para firmar los mensajes subsiguientes. Finalmente, pasa una solicitud de autenticación al proveedor. Esta solicitud redirige el navegador web del usuario final a un sitio de servidor OpenID, donde el usuario puede continuar el proceso de autenticación.

Un proveedor OpenID generalmente pide a los usuarios su contraseña (si no habían iniciado sesión previamente), si el usuario confía en este sitio y qué información puede devolverse al sitio. Estas interacciones no son visibles para el consumidor OpenID, por lo que no puede obtener la contraseña del usuario u otra información que el usuario no haya indicado que el proveedor OpenID pueda compartir con él.

En caso de éxito, Zend_OpenId_Consumer::login() no retorna, sino que realiza una redirección HTTP. Sin embargo, si hay un error, puede retornar FALSE. Los errores pueden ocurrir debido a una identidad inválida, un proveedor que no responde, un error de comunicación, etc.

El tercer paso de la autenticación se inicia mediante la respuesta del proveedor OpenID, después de que este haya autenticado la contraseña del usuario. Esta respuesta se pasa de forma indirecta, como una redirección HTTP usando el navegador web del usuario final. El consumidor solo debe verificar ahora que esta respuesta sea válida.

Ejemplo 53.3. El verificador de la respuesta de autenticación

$consumer = new Zend_OpenId_Consumer();
if ($consumer->verify($_GET, $id)) {
    echo "VALID " . htmlspecialchars($id);
} else {
    echo "INVALID " . htmlspecialchars($id);
}

Esta verificación se realiza mediante el método Zend_OpenId_Consumer::verify, que toma un arreglo de los argumentos de la solicitud HTTP y comprueba que esta respuesta esté correctamente firmada por el proveedor OpenID. Puede asignar la identidad OpenID declarada que fue introducida por el usuario final en el primer paso mediante un segundo argumento opcional.

53.2.2. Combinando todos los pasos en una página

El siguiente ejemplo combina los tres pasos en un único script. No proporciona ninguna funcionalidad nueva. La ventaja de usar un único script es que el desarrollador no necesita especificar URL's para un script que maneje el siguiente paso. Por defecto, todos los pasos usan la misma URL. Sin embargo, el script ahora incluye código de despacho para ejecutar el código apropiado para cada paso de la autenticación.

Ejemplo 53.4. El script completo de inicio de sesión OpenID

<?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.";
    }
} 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 = "CANCELLED";
    }
}
?>
<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>

Además, este código diferencia entre respuestas de autenticación canceladas e inválidas. El proveedor retorna una respuesta cancelada si el proveedor de identidad no reconoce la identidad suministrada, el usuario no ha iniciado sesión, o el usuario no confía en el sitio. Una respuesta inválida indica que la respuesta no cumple con el protocolo OpenID o está firmada incorrectamente.

53.2.3. Reino del consumidor (Realm)

Cuando un sitio habilitado para OpenID pasa solicitudes de autenticación a un proveedor, se identifica con una URL de reino (realm). Esta URL puede considerarse la raíz de un sitio de confianza. Si el usuario confía en la URL del reino, también debería confiar en las URL coincidentes y subsiguientes.

Por defecto, la URL del reino se establece automáticamente como la URL del directorio en el que reside el script de inicio de sesión. Este valor predeterminado es útil en la mayoría, pero no en todos, los casos. A veces debe confiarse en un dominio completo, y no en un directorio. O incluso en una combinación de varios servidores en un dominio.

Para sobrescribir el valor predeterminado, los desarrolladores pueden pasar la URL del reino como tercer argumento del método Zend_OpenId_Consumer::login. En el siguiente ejemplo, una única interacción solicita acceso de confianza para todos los sitios de php.net.

Ejemplo 53.5. Solicitud de autenticación para un reino especificado

$consumer = new Zend_OpenId_Consumer();
if (!$consumer->login($_POST['openid_identifier'],
                      'example-3_3.php',
                      'http://*.php.net/')) {
    die("OpenID login failed.");
}

Este ejemplo implementa solamente el segundo paso de la autenticación; el primer y el tercer paso son similares a los ejemplos anteriores.

53.2.4. Verificación inmediata

En algunos casos, una aplicación solo necesita comprobar si un usuario ya ha iniciado sesión en un servidor OpenID de confianza sin ninguna interacción con el usuario. El método Zend_OpenId_Consumer::check hace precisamente eso. Se ejecuta con los mismos argumentos que Zend_OpenId_Consumer::login, pero no muestra ninguna página del servidor OpenID al usuario. Desde el punto de vista del usuario, este proceso es transparente, y parece como si nunca hubiera salido del sitio. El tercer paso tiene éxito si el usuario ya ha iniciado sesión y es de confianza para el sitio; de lo contrario, fallará.

Ejemplo 53.6. Verificación inmediata sin interacción

$consumer = new Zend_OpenId_Consumer();
if (!$consumer->check($_POST['openid_identifier'], 'example-4_3.php')) {
    die("OpenID login failed.");
}

Este ejemplo implementa solamente el segundo paso de la autenticación; el primer y el tercer paso son similares a los ejemplos anteriores.

53.2.5. Zend_OpenId_Consumer_Storage

Hay tres pasos en el procedimiento de autenticación OpenID, y cada paso se realiza mediante una solicitud HTTP distinta. Para almacenar información entre solicitudes, Zend_OpenId_Consumer utiliza un almacenamiento interno.

Los desarrolladores no necesariamente tienen que ser conscientes de este almacenamiento porque, por defecto, Zend_OpenId_Consumer utiliza almacenamiento basado en archivos bajo el directorio temporal, de forma similar a las sesiones de PHP. Sin embargo, este almacenamiento puede no ser adecuado en todos los casos. Algunos desarrolladores pueden querer almacenar información en una base de datos, mientras que otros pueden necesitar usar un almacenamiento común adecuado para granjas de servidores. Afortunadamente, los desarrolladores pueden reemplazar fácilmente el almacenamiento predeterminado por el suyo propio. Para especificar un mecanismo de almacenamiento personalizado, solo es necesario extender la clase Zend_OpenId_Consumer_Storage y pasar esta subclase al constructor de Zend_OpenId_Consumer en el primer argumento.

El siguiente ejemplo muestra un mecanismo de almacenamiento simple que usa Zend_Db como backend y expone tres grupos de funciones. El primer grupo contiene funciones para trabajar con asociaciones, mientras que el segundo grupo almacena en caché la información de descubrimiento, y el tercer grupo puede utilizarse para comprobar si una respuesta es única. Esta clase puede usarse fácilmente con bases de datos existentes o nuevas; si las tablas requeridas no existen, las creará.

Ejemplo 53.7. Almacenamiento en base de datos

class DbStorage extends Zend_OpenId_Consumer_Storage
{
    private $_db;
    private $_association_table;
    private $_discovery_table;
    private $_nonce_table;

    // Pass in the Zend_Db_Adapter object and the names of the
    // required tables
    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 the associations table doesn't exist, create it
        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 the discovery table doesn't exist, create it
        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 the nonce table doesn't exist, create it
        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->insert($table, array(
            'url'     => $url,
            'handle'  => $handle,
            'macFunc' => $macFunc,
            'secret'  => $secret,
            'expires' => $expires,
        ));
        return true;
    }

    public function getAssociation($url,
                                   &$handle,
                                   &$macFunc,
                                   &$secret,
                                   &$expires)
    {
        $table = $this->_association_table;
        $this->_db->delete(
            $table, $this->_db->quoteInto('expires < ?', time())
        );
        $select = $this-_db->select()
                ->from($table, array('handle', 'macFunc', 'secret', 'expires'))
                ->where('url = ?', $url);
        $res = $this->_db->fetchRow($select);

        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->delete(
            $table, $this->_db->quoteInto('expires < ', time())
        );
        $select = $this->_db->select()
                ->from($table, array('url', 'macFunc', 'secret', 'expires')
                ->where('handle = ?', $handle);
        $res = $select->fetchRow($select);

        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->insert($table, array(
            'id'      => $id,
            'realId'  => $realId,
            'server'  => $server,
            'version' => $version,
            'expires' => $expires,
        ));

        return true;
    }

    public function getDiscoveryInfo($id,
                                     &$realId,
                                     &$server,
                                     &$version,
                                     &$expires)
    {
        $table = $this->_discovery_table;
        $this->_db->delete($table, $this->quoteInto('expires < ?', time()));
        $select = $this->_db->select()
                ->from($table, array('realId', 'server', 'version', 'expires'))
                ->where('id = ?', $id);
        $res = $this->_db->fetchRow($select);

        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->delete($table, $this->_db->quoteInto('id = ?', $id));
        return true;
    }

    public function isUniqueNonce($nonce)
    {
        $table = $this->_nonce_table;
        try {
            $ret = $this->_db->insert($table, array(
                'nonce' => $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);

Este ejemplo no muestra el código de autenticación OpenID en sí, pero este código sería el mismo que el de los otros ejemplos de este capítulo. examples.

53.2.6. Extensión Simple Registration

Además de la autenticación, el estándar OpenID puede utilizarse para el intercambio ligero de perfiles, con el fin de hacer que la información sobre un usuario sea portable a través de varios sitios. Esta característica no está cubierta por la especificación de autenticación OpenID, sino por el protocolo de extensión OpenID Simple Registration. Este protocolo permite a los sitios habilitados para OpenID solicitar información sobre los usuarios finales a los proveedores OpenID. Dicha información puede incluir:

  • nickname - cualquier cadena UTF-8 que el usuario final utilice como apodo

  • email - la dirección de correo electrónico del usuario según se especifica en la sección 3.4.1 de RFC2822

  • fullname - una representación en cadena UTF-8 del nombre completo del usuario

  • dob - la fecha de nacimiento del usuario en el formato 'YYYY-MM-DD'. Cualquier valor cuya representación use menos del número de dígitos especificado en este formato debe rellenarse con ceros. En otras palabras, la longitud de este valor debe ser siempre 10. Si el usuario final no desea revelar alguna parte particular de este valor (es decir, año, mes o día), debe establecerse en cero. Por ejemplo, si el usuario desea especificar que su fecha de nacimiento cae en 1980, pero no especificar el mes o el día, el valor devuelto debería ser '1980-00-00'.

  • gender - el género del usuario: "M" para masculino, "F" para femenino

  • postcode - una cadena UTF-8 que se ajusta al sistema postal del país del usuario

  • country - el país de residencia del usuario según lo especificado por ISO3166

  • language - el idioma preferido del usuario según lo especificado por ISO639

  • timezone - una cadena ASCII de una base de datos de zonas horarias. Por ejemplo, "Europe/Paris" o "America/Los_Angeles".

Un sitio web habilitado para OpenID puede solicitar cualquier combinación de estos campos. También puede exigir estrictamente cierta información y permitir a los usuarios proporcionar u ocultar información adicional. El siguiente ejemplo crea una instancia de la clase Zend_OpenId_Extension_Sreg, exigiendo un nickname y solicitando opcionalmente un email y un fullname.

Ejemplo 53.8. Envío de solicitudes con la extensión Simple Registration

$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.");
}

Como puede verse, el constructor de Zend_OpenId_Extension_Sreg acepta un arreglo de campos OpenID. Este arreglo tiene los nombres de los campos como índices de un indicador que señala si el campo es obligatorio; TRUE significa que el campo es obligatorio y FALSE significa que el campo es opcional. El método Zend_OpenId_Consumer::login acepta una extensión o un arreglo de extensiones como su cuarto argumento.

En el tercer paso de la autenticación, el objeto Zend_OpenId_Extension_Sreg debe pasarse a Zend_OpenId_Consumer::verify. Luego, tras una autenticación exitosa, el método Zend_OpenId_Extension_Sreg::getProperties devolverá un arreglo asociativo de los campos solicitados.

Ejemplo 53.9. Verificación de respuestas con la extensión Simple Registration

$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 " . htmlspecialchars($id) ."<br>\n";
    $data = $sreg->getProperties();
    if (isset($data['nickname'])) {
        echo "nickname: " . htmlspecialchars($data['nickname']) . "<br>\n";
    }
    if (isset($data['email'])) {
        echo "email: " . htmlspecialchars($data['email']) . "<br>\n";
    }
    if (isset($data['fullname'])) {
        echo "fullname: " . htmlspecialchars($data['fullname']) . "<br>\n";
    }
} else {
    echo "INVALID " . htmlspecialchars($id);
}

Si el objeto Zend_OpenId_Extension_Sreg se creó sin ningún argumento, el código del usuario debe comprobar por sí mismo la existencia de los datos requeridos. Sin embargo, si el objeto se crea con la misma lista de campos requeridos que en el segundo paso, comprobará automáticamente la existencia de los datos requeridos. En este caso, Zend_OpenId_Consumer::verify devolverá FALSE si falta alguno de los campos requeridos.

Zend_OpenId_Extension_Sreg utiliza la versión 1.0 por defecto, porque la especificación de la versión 1.1 aún no está finalizada. Sin embargo, algunas bibliotecas no admiten completamente la versión 1.0. Por ejemplo, www.myopenid.com requiere un espacio de nombres SREG en las solicitudes, que solo está disponible en 1.1. Para trabajar con un servidor de este tipo, debe establecer explícitamente la versión en 1.1 en el constructor de Zend_OpenId_Extension_Sreg.

El segundo argumento del constructor de Zend_OpenId_Extension_Sreg es una URL de política, que debe ser proporcionada al usuario por el proveedor de identidad.

53.2.7. Integración con Zend_Auth

Zend Framework proporciona una clase especial para dar soporte a la autenticación de usuarios: Zend_Auth. Esta clase puede usarse junto con Zend_OpenId_Consumer. El siguiente ejemplo muestra cómo OpenIdAdapter implementa Zend_Auth_Adapter_Interface con el método authenticate(). Esto realiza una consulta de autenticación y su verificación.

La gran diferencia entre este adaptador y los ya existentes, es que funciona sobre dos solicitudes HTTP e incluye código de despacho para realizar el segundo o tercer paso de la autenticación OpenID.

Ejemplo 53.10. Adaptador Zend_Auth para OpenID

<?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;
                $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<br>\n";
        }
    }
} else if ($auth->hasIdentity()) {
    if (isset($_POST['openid_action']) &&
        $_POST['openid_action'] == "logout") {
        $auth->clearIdentity();
    } else {
        $status = "You are logged in as " . $auth->getIdentity() . "<br>\n";
    }
}
?>
<html><body>
<?php echo htmlspecialchars($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>

Con Zend_Auth, la identidad del usuario final se guarda en los datos de la sesión. Puede comprobarse con Zend_Auth::hasIdentity y Zend_Auth::getIdentity.

53.2.8. Integración con Zend_Controller

Finalmente, unas palabras sobre la integración en aplicaciones Modelo-Vista-Controlador: dichas aplicaciones de Zend Framework se implementan usando la clase Zend_Controller y utilizan objetos de la clase Zend_Controller_Response_Http para preparar respuestas HTTP y enviarlas de vuelta al navegador web del usuario.

Zend_OpenId_Consumer no proporciona ninguna capacidad de interfaz gráfica, pero realiza redirecciones HTTP al tener éxito Zend_OpenId_Consumer::login y Zend_OpenId_Consumer::check. Estas redirecciones pueden funcionar incorrectamente o no funcionar en absoluto si ya se ha enviado algún dato al navegador web. Para realizar correctamente la redirección HTTP en código MVC, se debe enviar el objeto real Zend_Controller_Response_Http a Zend_OpenId_Consumer::login o Zend_OpenId_Consumer::check como último argumento.