Subversion Repositories Applications.framework

Rev

Rev 263 | Blame | Last modification | View Log | RSS feed

<?php 
/**
 * La classe OpenIdClient est une implémentation d'un client OpenId, depuis une classe Zend.
 * Elle permet d'établir une connexion avec un serveur, en fonction d'un identifiant OpenId.
 * Elle permet de communiquer de manière sécurisée avec ce serveur, et doit aboutir a une 
 * identification centralisée.
 * */

class OpenIdClient {
        
        //OpenID 2.0 namespace. Tous les messages OpenID 2.0 DOIVENT contenir la variable openid.ns et sa valeur
    const NS_2_0 = 'http://specs.openid.net/auth/2.0';
    
    
    // TODO : remplacer _storage par une gestion par cache ?
        /**
     * Variable permettant le stockage d'informations, notammenent à propos des clés DiffieHellmann
     * @var Storage $_storage
     */
    protected $_storage = null;
    
    /**
     * Tableau "cache" interne permettant d'éviter des accès inutiles au fichier storage
     * @var array $_cache
     */
    protected $_cache = array();

    // Client pour les requetes.
    private $client;
    
        /**
         * Constructeur de l'application
         * */
        function __construct()  {
                $this->client = new Client();
                $this->_storage = new StorageFile();
        }
        
        /**
         * Fonction login
         * 
         * Return true ou false
         * > Ne retourne rien si true car redirige vers l'adresse du serveur OID
         * */
        //FIXME : le paramètre immediate ?
        // A vérifier mais ça doit permettre de passer directement le mot de passe. Il reste plus qu'à trouver le nom de la variable mot de passe.
        
        function login($id, $immediate = false) {
                
                // L'original retourne la fonction checkId, avec le parametre immediate = true
                // Je ne comprends pas l'utilité, je fusionne les deux pour l'instant
                // FIXME : si pas de comportement étrange, valider.
                
                
                //Tests et arrêt si non validé : 
                        //Normaliser (traite si XRI ou URL, normalize URL)
                        //FIXME : voir avec JP pour équivalent dans framework
                        if (!$this->normalize($id))     {
                                return false;
                        }
                        
                        //Discovery
                        // Récupérer les informations sur le serveur OPEN ID
                        /*
                         FIXME : ca change la valeur de l'id !!!
                         if (!$this->_discovery($id, $server, $version)) {
                         trigger_error('Discovery failed');
                         return false;
                    }*/
                        
                    $retour_url = $this->client->consulter($id); 
                    //Le retour contient les balises suivantes :
                        /*
                         * 
                         * <link rel="openid.server" href="http://www.myopenid.com/server" />
                         * <link rel="openid2.provider" href="http://www.myopenid.com/server" />
                         */
                        $metaServeur = $this->verifierVersion($retour_url);
                        
                        //TODO : Voir avec JP : la classe client ne permet pas de vérifer le statut ?? 
                        
                        if ($retour_url === false) {
                                trigger_error('L\'adresse $id est inacessible', E_USER_ERROR);
                                return false;
                        }
                        if ($metaServeur === false)     {
                                return false;
                        }
                        if (!$this->_associate($metaServeur['serveur'], $metaServeur['version'])) {
                trigger_error('Impossible de s\'associer avec le serveur');
                }
                
                
                /*TODO : fonctionnement différent
                 if (!$this->_getAssociation(
                $server,
                $handle,
                $macFunc,
                $secret,
                $expires)) {
            /* Use dumb mode *
            unset($handle);
            unset($macFunc);
            unset($secret);
            unset($expires);*
        }*/

                        //on a la version, l'adresse du serveur et le realId si c'est une 2.0 dans metaServeur
                        if (isset($metaServeur['realId'])) {
                                $id = $metaServeur['realId']; 
                        }
                        
                        //Associate
                        //getAssociation
                
                //Organisation des paramètres :
                $params = array();
                
        
        if ($metaServeur['version'] >= 2.0) {
            $params['openid.ns'] = self::NS_2_0;
        }

        $params['openid.mode'] = $immediate ?
            'checkid_immediate' : 'checkid_setup';

        $params['openid.identity'] = $id;

        //FIXME : Ex : $params['openid.claimed_id'] = $claimedId; > jvois pas l'intéret
        $params['openid.claimed_id'] = $id;
        /*
         * TODO : gérer les sessions et namespace
         * if ($metaServeur['version'] <= 2.0) {
            if ($this->_session !== null) {
                $this->_session->identity = $id;
                $this->_session->claimed_id = $claimedId;
            } else if (defined('SID')) {
                $_SESSION["zend_openid"] = array(
                    "identity" => $id,
                    "claimed_id" => $claimedId);
            } else {
                require_once "Zend/Session/Namespace.php";
                $this->_session = new Zend_Session_Namespace("zend_openid");
                $this->_session->identity = $id;
                $this->_session->claimed_id = $claimedId;
            }
        }*/

        if (isset($handle)) {
            $params['openid.assoc_handle'] = $handle;
        }

        //FIXME : $params['openid.return_to'] = $this->absoluteUrl($returnTo);
        $params['openid.return_to'] = $this->absoluteUrl(null);

        if (empty($root)) {
            $root = $this->selfUrl();
            if ($root[strlen($root)-1] != '/') {
                $root = dirname($root);
            }
        }
        if ($metaServeur['version'] >= 2.0) {
            $params['openid.realm'] = $root;
        } else {
            $params['openid.trust_root'] = $root;
        }

        /*FIXME :: 
         
         if (!Zend_OpenId_Extension::forAll($extensions, 'prepareRequest', $params)) {
            $this->_setError("Extension::prepareRequest failure");
            return false;
        }*/

        $this->redirect($metaServeur['serveur'], $params);
        return true;
                //Renvoyer vers l'url
        }
        
        /**
     * Verifies authentication response from OpenID server.
     *
     * This is the second step of OpenID authentication process.
     * The function returns true on successful authentication and false on
     * failure.
     *
     * @param array $params HTTP query data from OpenID server
     * @param string &$identity this argument is set to end-user's claimed
     *  identifier or OpenID provider local identifier.
     * @param mixed $extensions extension object or array of extensions objects
     * @return bool
     */
    public function verify($params, &$identity = "", $extensions = null)
    {
        
        if (isset($params['openid_ns']) &&
            $params['openid_ns'] == $this->NS_2_0) {
            $version = 2.0;
        }

        if (isset($params["openid_claimed_id"])) {
            $identity = $params["openid_claimed_id"];
        } else if (isset($params["openid_identity"])){
            $identity = $params["openid_identity"];
        } else {
            $identity = "";
        }

        if ($version < 2.0 && !isset($params["openid_claimed_id"])) {
            if ($this->_session !== null) {
                if ($this->_session->identity === $identity) {
                    $identity = $this->_session->claimed_id;
                }
            } else if (defined('SID')) {
                if (isset($_SESSION["zend_openid"]["identity"]) &&
                    isset($_SESSION["zend_openid"]["claimed_id"]) &&
                    $_SESSION["zend_openid"]["identity"] === $identity) {
                    $identity = $_SESSION["zend_openid"]["claimed_id"];
                }
            } else {
                require_once "Zend/Session/Namespace.php";
                $this->_session = new Zend_Session_Namespace("zend_openid");
                if ($this->_session->identity === $identity) {
                    $identity = $this->_session->claimed_id;
                }
            }
        }

        if (empty($params['openid_mode'])) {
            $this->_setError("Missing openid.mode");
            return false;
        }
        if (empty($params['openid_return_to'])) {
            $this->_setError("Missing openid.return_to");
            return false;
        }
        if (empty($params['openid_signed'])) {
            $this->_setError("Missing openid.signed");
            return false;
        }
        if (empty($params['openid_sig'])) {
            $this->_setError("Missing openid.sig");
            return false;
        }
        if ($params['openid_mode'] != 'id_res') {
            $this->_setError("Wrong openid.mode '".$params['openid_mode']."' != 'id_res'");
            return false;
        }
        if (empty($params['openid_assoc_handle'])) {
            $this->_setError("Missing openid.assoc_handle");
            return false;
        }
        
        if ($params['openid_return_to'] != $this->selfUrl()) {
            /* Ignore query part in openid.return_to */
            $pos = strpos($params['openid_return_to'], '?');
            if ($pos === false ||
                SUBSTR($params['openid_return_to'], 0 , $pos) != $this->selfUrl()) {

                /*$this->_setError("Wrong openid.return_to '".
                    $params['openid_return_to']."' != '" . $this->selfUrl() ."'");*/
                trigger_error('Wrong openid.return_to', E_USER_ERROR);  
                        
                return false;
            }
        }

        if ($version >= 2.0) {
            if (empty($params['openid_response_nonce'])) {
                trigger_error('Missing openid.response_nonce', E_USER_ERROR);
                return false;
            }
            if (empty($params['openid_op_endpoint'])) {
                trigger_error('Missing openid.op_endpoint', E_USER_ERROR);
                return false;
            /* OpenID 2.0 (11.3) Checking the Nonce */
            } else if (!$this->_storage->isUniqueNonce($params['openid_op_endpoint'], $params['openid_response_nonce'])) {
                trigger_error('Duplicate openid.response_nonce', E_USER_ERROR);
                return false;
            }
        }

        if (!empty($params['openid_invalidate_handle'])) {
            if ($this->_storage->getAssociationByHandle(
                $params['openid_invalidate_handle'],
                $url,
                $macFunc,
                $secret,
                $expires)) {
                $this->_storage->delAssociation($url);
            }
        }

        if ($this->_storage->getAssociationByHandle(
                $params['openid_assoc_handle'],
                $url,
                $macFunc,
                $secret,
                $expires)) {
            $signed = explode(',', $params['openid_signed']);
            $data = '';
            foreach ($signed as $key) {
                $data .= $key . ':' . $params['openid_' . strtr($key,'.','_')] . "\n";
            }
            if (base64_decode($params['openid_sig']) ==
                Zend_OpenId::hashHmac($macFunc, $data, $secret)) {
                /*
                 * FIXME dépendance je sais pas pour quoi : a voir :
                 * if (!Zend_OpenId_Extension::forAll($extensions, 'parseResponse', $params)) {
                    $this->_setError("Extension::parseResponse failure");
                    return false;
                }*/
                /* OpenID 2.0 (11.2) Verifying Discovered Information */
                if (isset($params['openid_claimed_id'])) {
                    $id = $params['openid_claimed_id'];
                    if (!$this->normalize($id)) {
                        $this->_setError("Normalization failed");
                        return false;
                    } else if (!$this->_discovery($id, $discovered_server, $discovered_version)) {
                        $this->_setError("Discovery failed: " . $this->getError());
                        return false;
                    } else if ((!empty($params['openid_identity']) &&
                                $params["openid_identity"] != $id) ||
                               (!empty($params['openid_op_endpoint']) &&
                                $params['openid_op_endpoint'] != $discovered_server) ||
                               $discovered_version != $version) {
                        $this->_setError("Discovery information verification failed");
                        return false;
                    }
                }
                return true;
            }
            $this->_storage->delAssociation($url);
            $this->_setError("Signature check failed");
            return false;
        }
        else
        {
            /* Use dumb mode */
            if (isset($params['openid_claimed_id'])) {
                $id = $params['openid_claimed_id'];
            } else if (isset($params['openid_identity'])) {
                $id = $params['openid_identity'];
            } else {
                $this->_setError("Missing openid.claimed_id and openid.identity");
                return false;
            }

            if (!$this->normalize($id)) {
                trigger_error('Normalization failed', E_USER_ERROR);
                return false;
            } else if (!$this->_discovery($id, $server, $discovered_version)) {
                trigger_error('Discovery failed', E_USER_ERROR);
                return false;
            }

            /* OpenID 2.0 (11.2) Verifying Discovered Information */
            if ((isset($params['openid_identity']) &&
                 $params["openid_identity"] != $id) ||
                (isset($params['openid_op_endpoint']) &&
                 $params['openid_op_endpoint'] != $server) ||
                $discovered_version != $version) {
                trigger_error('Discovery information verification failed', E_USER_ERROR);
                return false;
            }

            $params2 = array();
            foreach ($params as $key => $val) {
                if (strpos($key, 'openid_ns_') === 0) {
                    $key = 'openid.ns.' . substr($key, strlen('openid_ns_'));
                } else if (strpos($key, 'openid_sreg_') === 0) {
                    $key = 'openid.sreg.' . substr($key, strlen('openid_sreg_'));
                } else if (strpos($key, 'openid_') === 0) {
                    $key = 'openid.' . substr($key, strlen('openid_'));
                }
                $params2[$key] = $val;
            }
            $params2['openid.mode'] = 'check_authentication';
            $ret = $this->client->modifier($serveur, $params2);
            
            //_httpRequest($server, 'POST', $params2, $status);
            if ($ret === false) {
                trigger_error("'Dumb' signature verification HTTP request failed", E_USER_ERROR);
                return false;
            }
            $r = array();
            if (is_string($ret)) {
                foreach(explode("\n", $ret) as $line) {
                    $line = trim($line);
                    if (!empty($line)) {
                        $x = explode(':', $line, 2);
                        if (is_array($x) && count($x) == 2) {
                            list($key, $value) = $x;
                            $r[trim($key)] = trim($value);
                        }
                    }
                }
            }
            $ret = $r;
            if (!empty($ret['invalidate_handle'])) {
                if ($this->_storage->getAssociationByHandle(
                    $ret['invalidate_handle'],
                    $url,
                    $macFunc,
                    $secret,
                    $expires)) {
                    $this->_storage->delAssociation($url);
                }
            }
            if (isset($ret['is_valid']) && $ret['is_valid'] == 'true') {
                if (!Zend_OpenId_Extension::forAll($extensions, 'parseResponse', $params)) {
                    $this->_setError("Extension::parseResponse failure");
                    return false;
                }
                return true;
            }
            $this->_setError("'Dumb' signature verification failed");
            return false;
        }
    }
        
        
        /**
     * Performs discovery of identity and finds OpenID URL, OpenID server URL
     * and OpenID protocol version. Returns true on succees and false on
     * failure.
     *
     * @param string &$id OpenID identity URL
     * @param string &$server OpenID server URL
     * @param float &$version OpenID protocol version
     * @return bool
     * @todo OpenID 2.0 (7.3) XRI and Yadis discovery
     */
    protected function _discovery(&$id, &$server, &$version)
    { 
        $realId = $id;
        if ($this->_storage->getDiscoveryInfo(
                $id,
                $realId,
                $server,
                $version,
                $expire)) {
            $id = $realId;
            return true;
        }
        
        /* TODO: OpenID 2.0 (7.3) XRI and Yadis discovery */

        /* HTML-based discovery */
        $clientDiscovery = new Client();
               
        //TODO : rajouter un test sur le statut de la réponse
        // Nécessite la prise en compte des entetes dans le framework
        
        /*if ($status != 200 || !is_string($response)) {
            return false;
        }*/
        $reponse = $clientDiscovery->consulter($id);
        $metaServeur = $this->verifierVersion($reponse);
        if (!isset($metaServeur) || empty($reponse))    {
                trigger_error('Aucune donnée OpenId', E_USER_ERROR);
                return false;
        }
        
        $expire = time() + 60 * 60;
        $this->_storage->addDiscoveryInfo($id, $metaServeur['realId'], $metaServeur['serveur'], $metaServeur['version'], $expire);
        if (!empty($metaServeur['realId'])) {
                $id = $metaServeur['realId'];
        }
        return true;
    }
        
        
        
        
        //Parser l'HTML de réponse pour trouver la version du serveur OPEN ID
        function verifierVersion($reponseHtml)  {
                
                // TODO : remplacer l'arlgorythme suivant par cette solution : 
                //1. Chercher l'existence d'une balise openidN.provider 
                //2. Déterminer la version en fonction de la chaine : openid2.provider => 2.0; openid.provider => 1.1
                //3. Récupérer l'url du serveur href="serveur"
                //4. SI 2.0, récupérer la valeur réelle de l'ID
                
                //TODO : penser à tester les deux versions du serveur
                
                $metaServeur = Array();
                
                if (preg_match(
                '/<link[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid2.provider[ \t]*[^"\']*\\1[^>]*href=(["\'])([^"\']+)\\2[^>]*\/?>/i',
                $reponseHtml,
                $r)) {
            $metaServeur['version'] = 2.0;
            $metaServeur['serveur'] = $r[3];
        } else if (preg_match(
                '/<link[^>]*href=(["\'])([^"\']+)\\1[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid2.provider[ \t]*[^"\']*\\3[^>]*\/?>/i',
                $reponseHtml,
                $r)) {
            $metaServeur['version'] = 2.0;
            $metaServeur['serveur'] = $r[1];
        } else if (preg_match(
                '/<link[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid.server[ \t]*[^"\']*\\1[^>]*href=(["\'])([^"\']+)\\2[^>]*\/?>/i',
                $reponseHtml,
                $r)) {
            $metaServeur['version'] = 1.1;
            $metaServeur['serveur'] = $r[3];
        } else if (preg_match(
                '/<link[^>]*href=(["\'])([^"\']+)\\1[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid.server[ \t]*[^"\']*\\3[^>]*\/?>/i',
                $reponseHtml,
                $r)) {
            $metaServeur['version'] = 1.1;
            $metaServeur['serveur'] = $r[2];
        } else {
            return false;
        }
        
        if ($metaServeur['version'] >= 2.0) {
            if (preg_match(
                    '/<link[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid2.local_id[ \t]*[^"\']*\\1[^>]*href=(["\'])([^"\']+)\\2[^>]*\/?>/i',
                    $reponseHtml,
                    $r)) {
                $metaServeur['realId'] = $r[3];
            } else if (preg_match(
                    '/<link[^>]*href=(["\'])([^"\']+)\\1[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid2.local_id[ \t]*[^"\']*\\3[^>]*\/?>/i',
                    $reponseHtml,
                    $r)) {
                $metaServeur['realId'] = $r[2];
            }
        } else {
            if (preg_match(
                    '/<link[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid.delegate[ \t]*[^"\']*\\1[^>]*href=(["\'])([^"\']+)\\2[^>]*\/?>/i',
                    $reponseHtml,
                    $r)) {
                $metaServeur['realId'] = $r[3];
            } else if (preg_match(
                    '/<link[^>]*href=(["\'])([^"\']+)\\1[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid.delegate[ \t]*[^"\']*\\3[^>]*\/?>/i',
                    $reponseHtml,
                    $r)) {
                $metaServeur['realId'] = $r[2];
            }
        }
                
        return $metaServeur;
        }
        
        /**
     * Create (or reuse existing) association between OpenID consumer and
     * OpenID server based on Diffie-Hellman key agreement. Returns true
     * on success and false on failure.
     *
     * @param string $url OpenID server url
     * @param float $version OpenID protocol version
     * @param string $priv_key for testing only
     * @return bool
     */
    protected function _associate($url, $version, $priv_key=null)
    {
        /* Check if we already have association in chace or storage */
       /* 
        * TODO : Utiliser le stockage plutot
        * */
        if ($this->_getAssociation(
                $url,
                $handle,
                $macFunc,
                $secret,
                $expires)) {
            return true;
        }

       /*
        * TODO : utiliser le fichier de config
        * if ($this->_dumbMode) {
               return true;
       }*/

        $params = array();

        if ($version >= 2.0) {
            $params = array(
                'openid.ns'           => $this->NS_2_0,
                'openid.mode'         => 'associate',
                'openid.assoc_type'   => 'HMAC-SHA256',
                'openid.session_type' => 'DH-SHA256',
            );
        } else {
            $params = array(
                'openid.mode'         => 'associate',
                'openid.assoc_type'   => 'HMAC-SHA1',
                'openid.session_type' => 'DH-SHA1',
            );
        }

        $dh = DiffieHellmanUtil::createDhKey(pack('H*', DiffieHellmanUtil::DH_P),
                                       pack('H*', DiffieHellmanUtil::DH_G),
                                       $priv_key);
        $dh_details = DiffieHellmanUtil::getDhKeyDetails($dh);

        $params['openid.dh_modulus']         = base64_encode(
            DiffieHellmanUtil::btwoc($dh_details['p']));
        $params['openid.dh_gen']             = base64_encode(
            DiffieHellmanUtil::btwoc($dh_details['g']));
        $params['openid.dh_consumer_public'] = base64_encode(
            DiffieHellmanUtil::btwoc($dh_details['pub_key']));

        while(1) {
                //FIXME : c'est pas une modification ...
            $ret = $this->client->modifier($url, $params); // FIXME : a quoi sert status ?, $status);
            if ($ret === false) {
                //$this->_setError("HTTP request failed");
                trigger_error('La requête a échoué', E_USER_ERROR);
                return false;
            }

            $r = array();
            $bad_response = false;
            foreach(explode("\n", $ret) as $line) {
                $line = trim($line);
                if (!empty($line)) {
                    $x = explode(':', $line, 2);
                    if (is_array($x) && count($x) == 2) {
                        list($key, $value) = $x;
                        $r[trim($key)] = trim($value);
                    } else {
                        $bad_response = true;
                    }
                }
            }
            if ($bad_response && strpos($ret, 'Unknown session type') !== false) {
                $r['error_code'] = 'unsupported-type';
            }
            $ret = $r;

            if (isset($ret['error_code']) &&
                $ret['error_code'] == 'unsupported-type') {
                if ($params['openid.session_type'] == 'DH-SHA256') {
                    $params['openid.session_type'] = 'DH-SHA1';
                    $params['openid.assoc_type'] = 'HMAC-SHA1';
                } else if ($params['openid.session_type'] == 'DH-SHA1') {
                    $params['openid.session_type'] = 'no-encryption';
                } else {
                    trigger_error("The OpenID service responded with: " . $ret['error_code'], E_USER_ERROR);
                    return false;
                }
            } else {
                break;
            }
        }

        /*
        FIXME : gestion du statut avec la classe client ??
        if ($status != 200) {
            $this->_setError("The server responded with status code: " . $status);
            return false;
        }*/

        if ($version >= 2.0 &&
            isset($ret['ns']) &&
            $ret['ns'] != $this->NS_2_0) {
            $this->_setError("Wrong namespace definition in the server response");
            return false;
        }

        if (!isset($ret['assoc_handle']) ||
            !isset($ret['expires_in']) ||
            !isset($ret['assoc_type']) ||
            $params['openid.assoc_type'] != $ret['assoc_type']) {
            if ($params['openid.assoc_type'] != $ret['assoc_type']) {
                $this->_setError("The returned assoc_type differed from the supplied openid.assoc_type");
            } else {
                $this->_setError("Missing required data from provider (assoc_handle, expires_in, assoc_type are required)");
            }
            return false;
        }

        $handle     = $ret['assoc_handle'];
        $expiresIn = $ret['expires_in'];

        if ($ret['assoc_type'] == 'HMAC-SHA1') {
            $macFunc = 'sha1';
        } else if ($ret['assoc_type'] == 'HMAC-SHA256' &&
            $version >= 2.0) {
            $macFunc = 'sha256';
        } else {
            $this->_setError("Unsupported assoc_type");
            return false;
        }

        if ((empty($ret['session_type']) ||
             ($version >= 2.0 && $ret['session_type'] == 'no-encryption')) &&
             isset($ret['mac_key'])) {
            $secret = base64_decode($ret['mac_key']);
        } else if (isset($ret['session_type']) &&
            $ret['session_type'] == 'DH-SHA1' &&
            !empty($ret['dh_server_public']) &&
            !empty($ret['enc_mac_key'])) {
            $dhFunc = 'sha1';
        } else if (isset($ret['session_type']) &&
            $ret['session_type'] == 'DH-SHA256' &&
            $version >= 2.0 &&
            !empty($ret['dh_server_public']) &&
            !empty($ret['enc_mac_key'])) {
            $dhFunc = 'sha256';
        } else {
            $this->_setError("Unsupported session_type");
            return false;
        }
        if (isset($dhFunc)) {
            $serverPub = base64_decode($ret['dh_server_public']);
            $dhSec = DiffieHellmanUtil::computeDhSecret($serverPub, $dh);
            if ($dhSec === false) {
                $this->_setError("DH secret comutation failed");
                return false;
            }
            $sec = $this->digest($dhFunc, $dhSec);
            if ($sec === false) {
                $this->_setError("Could not create digest");
                return false;
            }
            $secret = $sec ^ base64_decode($ret['enc_mac_key']);
        }
        if ($macFunc == 'sha1') {
            if (DiffieHellmanUtil::strlen($secret) != 20) {
                $this->_setError("The length of the sha1 secret must be 20");
                return false;
            }
        } else if ($macFunc == 'sha256') {
            if (DiffieHellmanUtil::strlen($secret) != 32) {
                $this->_setError("The length of the sha256 secret must be 32");
                return false;
            }
        }
        
        $this->_addAssociation(
            $url,
            $handle,
            $macFunc,
            $secret,
            time() + $expiresIn);
       /* $this->association['url'] = $url;
        $this->association['handle'] = $handle;
        $this->association['macFunc'] = $macFunc;
        $this->association['secret'] = $secret;
        $this->association['expiresIn'] = time() + $expiresIn;*/
        
        return true;
    }
    
        /**
     * Store assiciation in internal chace and external storage
     *
     * @param string $url OpenID server url
     * @param string $handle association handle
     * @param string $macFunc HMAC function (sha1 or sha256)
     * @param string $secret shared secret
     * @param integer $expires expiration UNIX time
     * @return void
     */
    protected function _addAssociation($url, $handle, $macFunc, $secret, $expires)
    {
        $this->_cache[$url] = array($handle, $macFunc, $secret, $expires);
        return $this->_storage->addAssociation(
            $url,
            $handle,
            $macFunc,
            $secret,
            $expires);
    }
    
        /**
     * Retrive assiciation information for given $url from internal cahce or
     * external storage
     *
     * @param string $url OpenID server url
     * @param string &$handle association handle
     * @param string &$macFunc HMAC function (sha1 or sha256)
     * @param string &$secret shared secret
     * @param integer &$expires expiration UNIX time
     * @return void
     */
    protected function _getAssociation($url, &$handle, &$macFunc, &$secret, &$expires)
    {
        if (isset($this->_cache[$url])) {
            $handle   = $this->_cache[$url][0];
            $macFunc = $this->_cache[$url][1];
            $secret   = $this->_cache[$url][2];
            $expires  = $this->_cache[$url][3];
            return true;
        }
        if ($this->_storage->getAssociation(
                $url,
                $handle,
                $macFunc,
                $secret,
                $expires)) {
            $this->_cache[$url] = array($handle, $macFunc, $secret, $expires);
            return true;
        }
        return false;
    }
    
        /**
     * Normalizes URL according to RFC 3986 to use it in comparison operations.
     * The function gets URL argument by reference and modifies it.
     * It returns true on success and false of failure.
     *
     * @param string &$id url to be normalized
     * @return bool
     */
    static public function normalizeUrl(&$id)
    {
        // RFC 3986, 6.2.2.  Syntax-Based Normalization

        // RFC 3986, 6.2.2.2 Percent-Encoding Normalization
        $i = 0;
        $n = strlen($id);
        $res = '';
        while ($i < $n) {
            if ($id[$i] == '%') {
                if ($i + 2 >= $n) {
                    return false;
                }
                ++$i;
                if ($id[$i] >= '0' && $id[$i] <= '9') {
                    $c = ord($id[$i]) - ord('0');
                } else if ($id[$i] >= 'A' && $id[$i] <= 'F') {
                    $c = ord($id[$i]) - ord('A') + 10;
                } else if ($id[$i] >= 'a' && $id[$i] <= 'f') {
                    $c = ord($id[$i]) - ord('a') + 10;
                } else {
                    return false;
                }
                ++$i;
                if ($id[$i] >= '0' && $id[$i] <= '9') {
                    $c = ($c << 4) | (ord($id[$i]) - ord('0'));
                } else if ($id[$i] >= 'A' && $id[$i] <= 'F') {
                    $c = ($c << 4) | (ord($id[$i]) - ord('A') + 10);
                } else if ($id[$i] >= 'a' && $id[$i] <= 'f') {
                    $c = ($c << 4) | (ord($id[$i]) - ord('a') + 10);
                } else {
                    return false;
                }
                ++$i;
                $ch = chr($c);
                if (($ch >= 'A' && $ch <= 'Z') ||
                    ($ch >= 'a' && $ch <= 'z') ||
                    $ch == '-' ||
                    $ch == '.' ||
                    $ch == '_' ||
                    $ch == '~') {
                    $res .= $ch;
                } else {
                    $res .= '%';
                    if (($c >> 4) < 10) {
                        $res .= chr(($c >> 4) + ord('0'));
                    } else {
                        $res .= chr(($c >> 4) - 10 + ord('A'));
                    }
                    $c = $c & 0xf;
                    if ($c < 10) {
                        $res .= chr($c + ord('0'));
                    } else {
                        $res .= chr($c - 10 + ord('A'));
                    }
                }
            } else {
                $res .= $id[$i++];
            }
        }

        if (!preg_match('|^([^:]+)://([^:@]*(?:[:][^@]*)?@)?([^/:@?#]*)(?:[:]([^/?#]*))?(/[^?#]*)?((?:[?](?:[^#]*))?)((?:#.*)?)$|', $res, $reg)) {
            return false;
        }
        $scheme = $reg[1];
        $auth = $reg[2];
        $host = $reg[3];
        $port = $reg[4];
        $path = $reg[5];
        $query = $reg[6];
        $fragment = $reg[7]; /* strip it */

        if (empty($scheme) || empty($host)) {
            return false;
        }

        // RFC 3986, 6.2.2.1.  Case Normalization
        $scheme = strtolower($scheme);
        $host = strtolower($host);

        // RFC 3986, 6.2.2.3.  Path Segment Normalization
        if (!empty($path)) {
            $i = 0;
            $n = strlen($path);
            $res = "";
            while ($i < $n) {
                if ($path[$i] == '/') {
                    ++$i;
                    while ($i < $n && $path[$i] == '/') {
                        ++$i;
                    }
                    if ($i < $n && $path[$i] == '.') {
                        ++$i;
                        if ($i < $n && $path[$i] == '.') {
                            ++$i;
                            if ($i == $n || $path[$i] == '/') {
                                if (($pos = strrpos($res, '/')) !== false) {
                                    $res = substr($res, 0, $pos);
                                }
                            } else {
                                    $res .= '/..';
                            }
                        } else if ($i != $n && $path[$i] != '/') {
                            $res .= '/.';
                        }
                    } else {
                        $res .= '/';
                    }
                } else {
                    $res .= $path[$i++];
                }
            }
            $path = $res;
        }

        // RFC 3986,6.2.3.  Scheme-Based Normalization
        if ($scheme == 'http') {
            if ($port == 80) {
                $port = '';
            }
        } else if ($scheme == 'https') {
            if ($port == 443) {
                $port = '';
            }
        }
        if (empty($path)) {
            $path = '/';
        }

        $id = $scheme
            . '://'
            . $auth
            . $host
            . (empty($port) ? '' : (':' . $port))
            . $path
            . $query;
        return true;
    }

    
    /**
     * Normaliser l'identifiant OpenId qui peut être une URL ou nom XRI
     * Retourne true ou false en cas d'erreur.
     *
     * Règles de normalisation : 
     * 1. If the user's input starts with one of the "xri://", "xri://$ip*",
     *    or "xri://$dns*" prefixes, they MUST be stripped off, so that XRIs
     *    are used in the canonical form, and URI-authority XRIs are further
     *    considered URL identifiers.
     * 2. If the first character of the resulting string is an XRI Global
     *    Context Symbol ("=", "@", "+", "$", "!"), then the input SHOULD be
     *    treated as an XRI.
     * 3. Otherwise, the input SHOULD be treated as an http URL; if it does
     *    not include a "http" or "https" scheme, the Identifier MUST be
     *    prefixed with the string "http://".
     * 4. URL identifiers MUST then be further normalized by both following
     *    redirects when retrieving their content and finally applying the
     *    rules in Section 6 of [RFC3986] to the final destination URL.
     * @param string &$id identifier to be normalized
     * @return bool
     */
    static public function normalize(&$id)
    {
        $id = trim($id);
        if (strlen($id) === 0) {
            return true;
        }

        // 7.2.1
        if (strpos($id, 'xri://$ip*') === 0) {
            $id = substr($id, strlen('xri://$ip*'));
        } else if (strpos($id, 'xri://$dns*') === 0) {
            $id = substr($id, strlen('xri://$dns*'));
        } else if (strpos($id, 'xri://') === 0) {
            $id = substr($id, strlen('xri://'));
        }

        // 7.2.2
        if ($id[0] == '=' ||
            $id[0] == '@' ||
            $id[0] == '+' ||
            $id[0] == '$' ||
            $id[0] == '!') {
            return true;
        }

        // 7.2.3
        if (strpos($id, "://") === false) {
            $id = 'http://' . $id;
        }

        // 7.2.4
        return self::normalizeURL($id);
    }
    
        /**
     * Generates a hash value (message digest) according to given algorithm.
     * It returns RAW binary string.
     *
     * This is a wrapper function that uses one of available internal function
     * dependent on given PHP configuration. It may use various functions from
     *  ext/openssl, ext/hash, ext/mhash or ext/standard.
     *
     * @param string $func digest algorithm
     * @param string $data data to sign
     * @return string RAW digital signature
     * @throws Zend_OpenId_Exception
     */
    public function digest($func, $data)
    {
        if (function_exists('openssl_digest')) {
            return openssl_digest($data, $func, true);
        } else if (function_exists('hash')) {
            return hash($func, $data, true);
        } else if ($func === 'sha1') {
            return sha1($data, true);
        } else if ($func === 'sha256') {
            if (function_exists('mhash')) {
                return mhash(MHASH_SHA256 , $data);
            }
        }
        /*require_once "Zend/OpenId/Exception.php";
        throw new Zend_OpenId_Exception(
            'Unsupported digest algorithm "' . $func . '".',
            Zend_OpenId_Exception::UNSUPPORTED_DIGEST);*/
        trigger_error('Unsupported digest algorithm '.$func , E_USER_ERROR);
    }
    
    
    
    
        
    
    
    
        /**
     * Returns a full URL that was requested on current HTTP request.
     *
     * @return string
     */
    public function selfUrl()
    {
        /*FIXME : 
         * if ($this->$selfUrl !== null) {
            return $this->$selfUrl;
        } */
        
        if (isset($_SERVER['SCRIPT_URI'])) {
            return $_SERVER['SCRIPT_URI'];
        }
        $url = '';
        $port = '';
        if (isset($_SERVER['HTTP_HOST'])) {
            if (($pos = strpos($_SERVER['HTTP_HOST'], ':')) === false) {
                if (isset($_SERVER['SERVER_PORT'])) {
                    $port = ':' . $_SERVER['SERVER_PORT'];
                }
                $url = $_SERVER['HTTP_HOST'];
            } else {
                $url = substr($_SERVER['HTTP_HOST'], 0, $pos);
                $port = substr($_SERVER['HTTP_HOST'], $pos);
            }
        } else if (isset($_SERVER['SERVER_NAME'])) {
            $url = $_SERVER['SERVER_NAME'];
            if (isset($_SERVER['SERVER_PORT'])) {
                $port = ':' . $_SERVER['SERVER_PORT'];
            }
        }
        if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
            $url = 'https://' . $url;
            if ($port == ':443') {
                $port = '';
            }
        } else {
            $url = 'http://' . $url;
            if ($port == ':80') {
                $port = '';
            }
        }

        $url .= $port;
        if (isset($_SERVER['HTTP_X_REWRITE_URL'])) {
            $url .= $_SERVER['HTTP_X_REWRITE_URL'];
        } elseif (isset($_SERVER['REQUEST_URI'])) {
            $query = strpos($_SERVER['REQUEST_URI'], '?');
            if ($query === false) {
                $url .= $_SERVER['REQUEST_URI'];
            } else {
                $url .= substr($_SERVER['REQUEST_URI'], 0, $query);
            }
        } else if (isset($_SERVER['SCRIPT_URL'])) {
            $url .= $_SERVER['SCRIPT_URL'];
        } else if (isset($_SERVER['REDIRECT_URL'])) {
            $url .= $_SERVER['REDIRECT_URL'];
        } else if (isset($_SERVER['PHP_SELF'])) {
            $url .= $_SERVER['PHP_SELF'];
        } else if (isset($_SERVER['SCRIPT_NAME'])) {
            $url .= $_SERVER['SCRIPT_NAME'];
            if (isset($_SERVER['PATH_INFO'])) {
                $url .= $_SERVER['PATH_INFO'];
            }
        }
        return $url;
    }
    
    
    //TODO : vérifier si les fonctions FWK & ZEND sont bien équivalente
        /**
     * Retourne l'url absolue d'une url donnée
     *
     * @param string $url absilute or relative URL
     * @return string
     */
    public function absoluteUrl($url)
    {
        if (!empty($ur))        {
                $urlAbsolue = new Url($url);
                $urlAbsolue->normaliser();
                $url = $urlAbsolue->getUrl();
        } else {
                $url = $this->selfUrl();
        }
        
        return $url;
        /*
        if (empty($url)) {
            return $this->selfUrl();
        } else if (!preg_match('|^([^:]+)://|', $url)) {
            if (preg_match('|^([^:]+)://([^:@]*(?:[:][^@]*)?@)?([^/:@?#]*)(?:[:]([^/?#]*))?(/[^?]*)?((?:[?](?:[^#]*))?(?:#.*)?)$|', $this->selfUrl(), $reg)) {
                $scheme = $reg[1];
                $auth = $reg[2];
                $host = $reg[3];
                $port = $reg[4];
                $path = $reg[5];
                $query = $reg[6];
                if ($url[0] == '/') {
                    return $scheme
                        . '://'
                        . $auth
                        . $host
                        . (empty($port) ? '' : (':' . $port))
                        . $url;
                } else {
                    $dir = dirname($path);
                    return $scheme
                        . '://'
                        . $auth
                        . $host
                        . (empty($port) ? '' : (':' . $port))
                        . (strlen($dir) > 1 ? $dir : '')
                        . '/'
                        . $url;
                }
            }
        }
        return $url;*/
    }
    
    
    //TODO : voir si on ne peut pas glisser ça dans client ?
        //FIXME : je met une fonction SIMPLISSIME a améliorer et reécrire
    // La fonction de Zend est plus poussée est prend en compte le cas ou l'header ne peut pas etre envoyé
    /**
     * Rediriger vers la page du serveur avec les paramètres de confiration
     *
     * @param string $url URL de retour
     * @param array $params paramètres additionnels
     */
    public function redirect($url, $params)     {
        //1. fabriquer l'url Get
        $urlRedirection = new Url($url);
        $urlRedirection->setRequete($params);
        //echo $urlRedirection->getUrl();
        try {
                header('Location:'.$urlRedirection->getUrl());
        } catch (Exception $e) {
                //TODO : voir autres méthodes de redirection
                // > balise META
                // > formulaire HTML
                // > JS
        }
    }
}
?>