From 52497a6378bcbeb231818620a1827ef83a4452f7 Mon Sep 17 00:00:00 2001 From: vinnl Date: Sat, 22 Mar 2008 23:51:58 +0100 Subject: [PATCH] Users can now login with their OpenID. When it is their first login, additional details are asked using OpenID's Simple Registration Extension. These details are then cached, and are not requested again until the cache has expired. The details are also saved to the database for later retrieval. When the cache expires (and thus new details are requested), this database entry will be updated with the new details. Furthermore, I've created Pivip_Auth, a subclass of Zend_Auth which was created because Zend_Auth could not store additional information besides the identifier. Pivip_Auth has the added method getIdentityProperties() which allows you to retrieve a Pivip_Auth_Identity object. --- project/library/Pivip/Auth.php | 100 +++++++++++ .../Auth/{Identity/Abstract.php => Identity.php} | 46 ++++- .../modules/notify/controllers/IndexController.php | 6 +- project/modules/openid/Module.php | 197 ++++++++++++++------- .../controllers/AuthenticationController.php | 184 ++++++++++++++++++- .../openid/views/scripts/authentication/menu.phtml | 13 +- project/modules/translate/Module.php | 2 +- project/public/styles/nl/screen.css | 2 +- .../openid/controllers/OpenidLoginFormSpec.php | 5 + 9 files changed, 477 insertions(+), 78 deletions(-) create mode 100644 project/library/Pivip/Auth.php rename project/library/Pivip/Auth/{Identity/Abstract.php => Identity.php} (74%) diff --git a/project/library/Pivip/Auth.php b/project/library/Pivip/Auth.php new file mode 100644 index 0000000..5da3d05 --- /dev/null +++ b/project/library/Pivip/Auth.php @@ -0,0 +1,100 @@ + + */ + +/** + * Pivip authentication class + */ +class Pivip_Auth extends Zend_Auth +{ + /** + * @var null|Pivip_Auth Singleton pattern implementation + */ + protected static $_instance = null; + + /** + * Singleton pattern implementation + */ + private function __construct() + {} + + /** + * Singleton pattern implementation + */ + private function __clone() + {} + + /** + * Singleton pattern implementation + * + * @return Pivip_Auth + */ + public static function getInstance() + { + if(null == self::$_instance) + { + self::$_instance = new self; + } + return self::$_instance; + } + + /** + * Retrieve the identity as a string from storage + * + * @return string|null null when no identity is available + */ + public function getIdentity() + { + $storage = $this->getStorage(); + + if($storage->isEmpty()) + { + return null; + } + + $identity = $storage->read(); + if($identity instanceof Pivip_Auth_Identity) + { + $identity = $identity->__toString(); + } + return $identity; + } + + /** + * Retrieve the identity from storage + * + * @return mixed|null null when no identity is available + */ + public function getIdentityProperties() + { + $storage = $this->getStorage(); + + if($storage->isEmpty()) + { + return null; + } + return $storage->read(); + } +} \ No newline at end of file diff --git a/project/library/Pivip/Auth/Identity/Abstract.php b/project/library/Pivip/Auth/Identity.php similarity index 74% rename from project/library/Pivip/Auth/Identity/Abstract.php rename to project/library/Pivip/Auth/Identity.php index bd29862..6c1bc0c 100644 --- a/project/library/Pivip/Auth/Identity/Abstract.php +++ b/project/library/Pivip/Auth/Identity.php @@ -1,4 +1,4 @@ -mo_properties = array('id' => $id, 'aclRole' => $aclRole, + 'fn' => $fn); + foreach($properties as $key => $property) + { + if($property instanceof Pivip_Auth_Identity_Property) + { + $this->_properties[$key] = $property; + } + } + } + + /** + * Convert the class to a string + * + * @return string String representation of this class + */ + public function __toString() + { + return $this->_properties['id']->value; } /** @@ -71,6 +90,11 @@ abstract class Pivip_Auth_Identity_Abstract */ public function __get($property) { + if('acl_role' == $property) + { + $property = 'aclRole'; + } + return $this->_properties[$property]; } /** @@ -80,8 +104,10 @@ abstract class Pivip_Auth_Identity_Abstract * @param Pivip_Auth_Identity_Property $value Value of the property * @return Pivip_Auth_Identity_Property The property */ - public function __set($property, $value) + public function __set($property, Pivip_Auth_Identity_Property $value) { + $this->_properties[$property] = $value; + return $this->_properties[$property]; } /** @@ -92,6 +118,11 @@ abstract class Pivip_Auth_Identity_Abstract */ public function __isset($property) { + if('acl_role' == $property) + { + $property = 'aclRole'; + } + return isset($this->_properties[$property]); } /** @@ -102,5 +133,10 @@ abstract class Pivip_Auth_Identity_Abstract */ public function __unset($property) { + if('acl_role' == $property) + { + $property = 'aclRole'; + } + unset($this->_properties[$property]); } } \ No newline at end of file diff --git a/project/modules/notify/controllers/IndexController.php b/project/modules/notify/controllers/IndexController.php index c39305a..8db4c2e 100644 --- a/project/modules/notify/controllers/IndexController.php +++ b/project/modules/notify/controllers/IndexController.php @@ -50,9 +50,11 @@ class Notify_IndexController extends Pivip_Controller_Module_Abstract $tip = $this->_flashMessenger->setNamespace('tip')->getMessages(); foreach($tip as $notification) { - $this->view->placeholder('information')->set($notification); + $this->view->placeholder('tip')->set($notification); } - $information = $this->_flashMessenger->resetNamespace()->getMessages(); + $inf = $this->_flashMessenger->setNamespace('information')->getMessages(); + $general = $this->_flashMessenger->resetNamespace()->getMessages(); + $information = array_merge($inf, $general); foreach($information as $notification) { $this->view->placeholder('information')->set($notification); diff --git a/project/modules/openid/Module.php b/project/modules/openid/Module.php index 522fd19..94d05a2 100644 --- a/project/modules/openid/Module.php +++ b/project/modules/openid/Module.php @@ -132,11 +132,11 @@ class Openid_Module extends Pivip_Module_Abstract $sreg = new Zend_OpenId_Extension_Sreg($requirements, null, 1.1); if($request->isPost()) { - $id = $request->getParam('openid_identifier'); + $id = $request->getParam('superuser'); $consumer = new Zend_OpenId_Consumer(); if(!$consumer->login($id, null, null, $sreg)) { - $error = $translate->_('Invalid OpenID supplied: ') . $id; + $error = sprintf($translate->_('OpenID %s is invalid.'), $id); $logger->err($error); $flashMessenger->setNamespace('error')->addMessage($error); } @@ -148,54 +148,10 @@ class Openid_Module extends Pivip_Module_Abstract { $config->administration->superuser = $id; $details = $sreg->getProperties(); + $details['id'] = $id; if(!empty($details['nickname'])) { - $data = array('nickname' => $details['nickname'], - 'uid' => $id); - if(!empty($details['fullname'])) - { - $data['fn'] = $details['fullname']; - } else { - $data['fn'] = $details['nickname']; - } - if(!empty($details['email'])) - { - $data['email'] = $details['email']; - } - if(!empty($details['dob']) && 10 == strlen($details['dob'])) - { - $data['bday'] = $details['dob']; - } - if(!empty($details['gender'])) - { - $data['sex'] = strtolower($details['gender']); - } - if(!empty($details['postcode'])) - { - $data['adr'] = ';;;;;' . $details['postcode']; - } else { - $data['adr'] = ';;;;;'; - } - if(!empty($details['country'])) - { - $locale = new Zend_Locale(); - $data['adr'] .= ';' . $locale->getCountryTranslation( - $details['country']); - } else { - $data['adr'] .= ';'; - } - if(!empty($details['language'])) - { - $data['locale'] = $details['language']; - if(!empty($details['country'])) - { - $data['locale'] .= '_' . $details['country']; - } - } - if(!empty($details['timezone'])) - { - $data['tz'] = 'VALUE=text:' . $details['timezone']; - } + $data = $this->sregToProperties($details); try { $openidTable->insert($data); @@ -219,12 +175,13 @@ class Openid_Module extends Pivip_Module_Abstract $flashMessenger->setNamespace('error')->addMessage($error); } } else { - $error = $translate->_('Could not authenticate OpenID: ') . $id; + $error = sprintf($translate->_('Could not authenticate OpenID %s.'), + $id); $logger->err($error); $flashMessenger->setNamespace('error')->addMessage($error); } } - $superuser = new Zend_Form_Element_Text('openid_identifier'); + $superuser = new Zend_Form_Element_Text('superuser'); $superuser->setLabel('Administration OpenID') ->setRequired(true); $form = new Zend_Form(); @@ -262,6 +219,115 @@ class Openid_Module extends Pivip_Module_Abstract } /** + * Convert OpenID's to cache ID's + * + * @param string $openid OpenID + * @return string Cache id + */ + public static function openidToCacheId($openid) + { + return 'openid_id_' . + str_replace(':', '__colon__', + str_replace('.', '__fullstop__', + str_replace(';', '__semicolon__', + str_replace('%', '__percent__', + str_replace('/', '__fwdslash__', + str_replace('\\', '__bwdslash__', + str_replace('#', '__hash__', + str_replace('?', '__qmark__', + str_replace('&', '__amp__', + str_replace('-', '__hyphen__', + str_replace('=', '__equal__', + Zend_Filter::get($openid, 'HtmlEntities')))))))))))); + } + + /** + * Change details retreived using Sreg to the Identity Properties format + * + * @param array $details Details retrieved using Sreg + * @return array Array containing the Identity Properties + */ + public static function sregToProperties(array $details) + { + if(empty($details['nickname']) || empty($details['id'])) + { + throw new InvalidArgumentException(); + } + $data = array('nickname' => $details['nickname'], + 'uid' => $details['id']); + if(!empty($details['fullname'])) + { + $data['fn'] = $details['fullname']; + } else { + $data['fn'] = $details['nickname']; + } + if(!empty($details['email'])) + { + $data['email'] = $details['email']; + } + if(!empty($details['dob']) && 10 == strlen($details['dob'])) + { + $data['bday'] = $details['dob']; + } + if(!empty($details['gender'])) + { + $data['sex'] = strtolower($details['gender']); + } + if(!empty($details['postcode'])) + { + $data['adr'] = ';;;;;' . $details['postcode']; + } else { + $data['adr'] = ';;;;;'; + } + if(!empty($details['country'])) + { + $locale = new Zend_Locale(); + $data['adr'] .= ';' . $locale->getCountryTranslation( + $details['country']); + } else { + $data['adr'] .= ';'; + } + if(!empty($details['language'])) + { + $data['locale'] = $details['language']; + if(!empty($details['country'])) + { + $data['locale'] .= '_' . $details['country']; + } + } + if(!empty($details['timezone'])) + { + $data['tz'] = 'VALUE=text:' . $details['timezone']; + } + return $data; + } + + /** + * Load the OpenID cache + * + * @return Zend_Cache + */ + public static function loadCache() + { + $cacheConfig = Zend_Registry::get('cacheConfig'); + $options = $cacheConfig->toArray(); + $separator = ''; + if('/' != substr($options['cache']['cache_root'], -1)) + { + $separator = '/'; + } + $options['backendOptions']['cache_dir'] = $options['cache']['cache_root'] + . $separator + . 'modules/openid'; + $frontendOptions = array('automatic_serialization' => true); + $cache = Zend_Cache::factory('Page', + $cacheConfig->cache->backend, + $frontendOptions, + $options['backendOptions']); + return $cache; + } + + /** * Load the module * * Register the OpenID Authentication adapter with Zend_Auth, process a login @@ -288,21 +354,13 @@ class Openid_Module extends Pivip_Module_Abstract // Could not connect to the database, do not install this module } } - $cacheConfig = Zend_Registry::get('cacheConfig'); - $options = $cacheConfig->toArray(); - $separator = ''; - if('/' != substr($options['cache_root'], -1)) - { - $separator = '/'; - } - $options['backendOptions']['cache_dir'] = $options['cache']['cache_root'] - . $separator - . 'modules/openid'; - $frontendOptions = array('automatic_serialization' => true); - $cache = Zend_Cache::factory('Page', - $cacheConfig->cache->backend, - $frontendOptions, - $options['backendOptions']); + + $nextRequest = new Zend_Controller_Request_Simple('process', + 'authentication', + 'openid'); + self::_pushStack($nextRequest); + + $cache = self::loadCache(); if(!$acl = $cache->load('openid_acl')) { $acl = new Zend_Acl(); @@ -310,8 +368,15 @@ class Openid_Module extends Pivip_Module_Abstract ->addRole(new Zend_Acl_Role('member')) ->addRole(new Zend_Acl_Role('admin')) ->addRole(new Zend_Acl_Role('superuser')); - $cache->save($acl, 'openid_acl'); + $cache->save($acl, 'openid_acl', array('acl')); } Zend_Registry::set('acl', $acl); + + $options = array('module' => 'openid', 'controller' => 'authentication', + 'action' => 'logout'); + $route = new Zend_Controller_Router_Route_Static('account/logout', + $options); + $router = Zend_Controller_Front::getInstance()->getRouter(); + $router->addRoute('openidLogout', $route); } } \ No newline at end of file diff --git a/project/modules/openid/controllers/AuthenticationController.php b/project/modules/openid/controllers/AuthenticationController.php index 9aeca71..5897127 100644 --- a/project/modules/openid/controllers/AuthenticationController.php +++ b/project/modules/openid/controllers/AuthenticationController.php @@ -39,8 +39,9 @@ class Openid_AuthenticationController extends Page_Abstract $openid_identifier = new Zend_Form_Element_Text('openid_identifier'); $openid_identifier->setRequired(true) ->setLabel('OpenID'); - $submit = new Zend_Form_Element_Submit('submit'); - $submit->setLabel('Sign in'); + $submit = new Zend_Form_Element_Submit('openid_action'); + $submit->setLabel('Sign in') + ->setValue('login'); $form = new Zend_Form(); $form->setAction($action) ->setMethod('post') @@ -51,6 +52,185 @@ class Openid_AuthenticationController extends Page_Abstract } /** + * Process a login + * + * @todo Allow registration of new OpenIDs when 1 != count($rows) + */ + public function processAction() + { + $this->_helper->viewRenderer->setNoRender(); + $auth = Pivip_Auth::getInstance(); + $translate = Zend_Registry::get('Zend_Translate'); + $openid_identifier = $this->_request->getPost('openid_identifier'); + $openid_action = $this->_request->getPost('openid_action'); + $openid_mode = $this->_request->getQuery('openid_mode'); + + $requirements = array('nickname' => true, 'email' => false, + 'fullname' => false, 'dob' => false, + 'gender' => false, 'postcode' => false, + 'country' => false, 'language' => false, + 'timezone' => false); + $sreg = new Zend_OpenId_Extension_Sreg($requirements, null, 1.1); + + if($this->_request->isPost() && + !empty($openid_identifier) && + !empty($openid_action)) + { + $normalizedId = $openid_identifier; + Zend_OpenId::normalize($normalizedId); + $adapter = new Zend_Auth_Adapter_OpenId($openid_identifier); + $cache = Openid_Module::loadCache(); + $cacheId = Openid_Module::openidToCacheId($normalizedId); + if(!$cache->test($cacheId)) + { + require_once './modules/openid/models/Openid.php'; + $openidTable = new Openid(); + $rows = $openidTable->find($normalizedId); + if(0 == count($rows)) + { + $adapter->setExtensions($sreg); + } + } + $result = $auth->authenticate($adapter); + if(!$result->isValid()) + { + $auth->clearIdentity(); + $this->_flashMessenger->setNamespace('error')->addMessage( + sprintf($translate->_('OpenID %s is invalid.'), + $openid_identifier)); + $this->_redirect(); + } + } else if(!empty($openid_mode)) { + $cache = Openid_Module::loadCache(); + $id = $this->_request->getQuery('openid_claimed_id'); + $cacheId = Openid_Module::openidToCacheId($id); + $adapter = new Zend_Auth_Adapter_OpenId(); + if(!$identity = $cache->load($cacheId)) + { + $adapter->setExtensions($sreg); + } + $result = $auth->authenticate($adapter); + $id = $auth->getIdentity(); + switch($result->getCode()) + { + case Zend_Auth_Result::SUCCESS: + if(!$identity = $cache->load($cacheId)) + { + $details = $sreg->getProperties(); + if(!empty($details)) + { + $details['id'] = $id; + $data = Openid_Module::sregToProperties($details); + $id = new Pivip_Auth_Identity_Property('id', $id); + $aclRole = new Pivip_Auth_Identity_Property('aclRole', + 'member'); + $fn = new Pivip_Auth_Identity_Property('fn', + $data['fn']); + $properties = array(); + foreach($data as $k => $v) + { + if(!is_string($v)) + { + continue; + } + $properties[$k] = new Pivip_Auth_Identity_Property($k, + $v); + } + $identity = new Pivip_Auth_Identity($id, $aclRole, $fn, + $properties); + require_once './modules/openid/models/Openid.php'; + $openidTable = new Openid(); + $id = $auth->getIdentity(); + try + { + $rows = $openidTable->find($id); + if(1 == count($rows)) + { + $where = $openidTable->getAdapter()->quoteInto( + 'uid=?', $id); + $openidTable->update($data, $where); + } else { + $openidTable->insert($data); + } + } catch(Exception $e) {die($e->getMessage()); + $translate = Zend_Registry::get('Zend_Translate'); + $error = $translate->_(sprintf( + 'Could not save OpenID %s to the database.', + $id)); + $logger = Zend_Registry::get('logger'); + $logger->err($error); + } + $cache->save($identity, $cacheId, + array('identity')); + } else { + require_once './modules/openid/models/Openid.php'; + $openidTable = new Openid(); + $rows = $openidTable->find($id); + $row = $rows->current(); + $id = new Pivip_Auth_Identity_Property('id', $id); + $aclRole = new Pivip_Auth_Identity_Property('acl_role', + $row->acl_role); + $fn = new Pivip_Auth_Identity_Property('fn', $row->fn); + $properties = array(); + foreach($row as $k => $v) + { + if(!is_string($v)) + { + continue; + } + $properties[$k] = new Pivip_Auth_Identity_Property($k, $v); + } + $identity = new Pivip_Auth_Identity($id, $aclRole, $fn, + $properties); + } + } + $storage = $auth->getStorage(); + $storage->write($identity); + $this->_flashMessenger->resetNamespace()->addMessage( + sprintf($translate->_('Logged in with OpenID %s.'), + $auth->getIdentity())); + $this->_redirect(); + break; + case Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND: + $this->_flashMessenger->setNamespace('error')->addMessage( + sprintf($translate->_('OpenID %s is invalid.'), + $auth->getIdentity())); + $auth->clearIdentity(); + $this->_redirect(); + break; + case Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID: + $this->_flashMessenger->setNamespace('error')->addMessage( + sprintf($translate->_('Could not authenticate OpenID %s.'), + $auth->getIdentity())); + $auth->clearIdentity(); + $this->_redirect(); + break; + default: + $this->_flashMessenger->setNamespace('error')->addMessage( + sprintf($translate->_('Problem logging in with OpenID %s.'), + $auth->getIdentity())); + $auth->clearIdentity(); + $this->_redirect(); + break; + } + } + } + + /** + * Logout + */ + public function logoutAction() + { + $this->_helper->viewRenderer->setNoRender(); + $auth = Zend_Auth::getInstance(); + $auth->clearIdentity(); + $translate = Zend_Registry::get('Zend_Translate'); + $this->_flashMessenger->resetNamespace()->addMessage( + $translate->_('Logged out.')); + $this->_redirect(); + } + + /** * Display a login form/logout link in the menu section */ public function menuAction() diff --git a/project/modules/openid/views/scripts/authentication/menu.phtml b/project/modules/openid/views/scripts/authentication/menu.phtml index 066238a..69ea69b 100644 --- a/project/modules/openid/views/scripts/authentication/menu.phtml +++ b/project/modules/openid/views/scripts/authentication/menu.phtml @@ -1,2 +1,13 @@
-openidForm . '
'; \ No newline at end of file +openidForm)) +{ + echo $this->openidForm; +} else { + echo +'' . $this->translate('Logout') . +''; +} +?> + \ No newline at end of file diff --git a/project/modules/translate/Module.php b/project/modules/translate/Module.php index 957ab3a..3b74239 100644 --- a/project/modules/translate/Module.php +++ b/project/modules/translate/Module.php @@ -70,7 +70,7 @@ class Translate_Module extends Pivip_Module_Abstract $cacheConfig = Zend_Registry::get('cacheConfig'); $options = $cacheConfig->toArray(); $separator = ''; - if('/' != substr($options['cache_root'], -1)) + if('/' != substr($options['cache']['cache_root'], -1)) { $separator = '/'; } diff --git a/project/public/styles/nl/screen.css b/project/public/styles/nl/screen.css index 4cd684a..44d7109 100644 --- a/project/public/styles/nl/screen.css +++ b/project/public/styles/nl/screen.css @@ -49,7 +49,7 @@ body{ text-decoration: underline; } -.header .trail{ +.header .quickview{ clear: left; float: left; margin-top: 16px; diff --git a/specifications/modules/openid/controllers/OpenidLoginFormSpec.php b/specifications/modules/openid/controllers/OpenidLoginFormSpec.php index 3b3c764..dc0dac7 100644 --- a/specifications/modules/openid/controllers/OpenidLoginFormSpec.php +++ b/specifications/modules/openid/controllers/OpenidLoginFormSpec.php @@ -23,4 +23,9 @@ class DescribeOpenidLoginForm extends PHPSpec_Context { $this->pending(); } + + public function itShouldBeAbleToProcessALoginForm() + { + $this->pending(); + } } \ No newline at end of file -- 2.11.4.GIT