Index: typo3/sysext/openid/ext_tables.sql =================================================================== --- typo3/sysext/openid/ext_tables.sql (revision 6512) +++ typo3/sysext/openid/ext_tables.sql (working copy) @@ -2,12 +2,46 @@ # Table structure for table 'be_users' # CREATE TABLE be_users ( - tx_openid_openid varchar(255) DEFAULT '' NOT NULL + tx_openid_openid varchar(255) DEFAULT '' NOT NULL, ); # # Table structure for table 'fe_users' # CREATE TABLE fe_users ( - tx_openid_openid varchar(255) DEFAULT '' NOT NULL -); \ No newline at end of file + tx_openid_openid varchar(255) DEFAULT '' NOT NULL, +); + +# +# Table structure for table 'tx_openid_assoc_store'. +# +CREATE TABLE tx_openid_assoc_store ( + uid int(11) unsigned NOT NULL auto_increment, + pid int(11) unsigned DEFAULT '0' NOT NULL, + crdate int(11) unsigned DEFAULT '0' NOT NULL, + tstamp int(11) unsigned DEFAULT '0' NOT NULL, + expires int(11) unsigned DEFAULT '0' NOT NULL, + server_url varchar(2047) DEFAULT '' NOT NULL, + assoc_handle varchar(255) DEFAULT '' NOT NULL, + content blob, + + PRIMARY KEY (uid), + KEY assoc_handle (assoc_handle(8)), + KEY expires (expires) +) ENGINE=InnoDB; + +# +# Table structure for table 'tx_openid_nonce_store'. +# +CREATE TABLE tx_openid_nonce_store ( + uid int(11) unsigned NOT NULL auto_increment, + pid int(11) unsigned DEFAULT '0' NOT NULL, + crdate int(11) unsigned DEFAULT '0' NOT NULL, + tstamp int(11) unsigned DEFAULT '0' NOT NULL, + server_url varchar(2047) DEFAULT '' NOT NULL, + salt char(40), + + PRIMARY KEY (uid), + UNIQUE KEY nonce (server_url(255),tstamp,salt), + KEY crdate (crdate) +) ENGINE=InnoDB; Index: typo3/sysext/openid/TODO =================================================================== --- typo3/sysext/openid/TODO (revision 6512) +++ typo3/sysext/openid/TODO (working copy) @@ -1,2 +1 @@ - -* use DB (the sessions or the caching framework) instead of the filesystem to store OpenID data (class.tx_openid_sv1.php) +None \ No newline at end of file Index: typo3/sysext/openid/sv1/class.tx_openid_store.php =================================================================== --- typo3/sysext/openid/sv1/class.tx_openid_store.php (revision 0) +++ typo3/sysext/openid/sv1/class.tx_openid_store.php (revision 0) @@ -0,0 +1,320 @@ + + * @package TYPO3 + * @subpackage tx_openid + */ +class tx_openid_store extends Auth_OpenID_OpenIDStore { + + const ASSOCIATION_TABLE_NAME = 'tx_openid_assoc_store'; + + const ASSOCIATION_EXPIRATION_SAFETY_INTERVAL = 120; /* 2 minutes */ + + const NONCE_TABLE_NAME = 'tx_openid_nonce_store'; + + const NONCE_STORAGE_TIME = 864000; /* 10 days */ + + /** + * Checks if database tables are created. If not, executes CREATE TABLE + * for extension's tables. + * + * @return void + */ + public function __construct() { + $this->checkAndCreateTables(); + } + + /** + * Sores the association for future use + * + * @param string $serverUrl Server URL + * @param Auth_OpenID_Association $association OpenID association + * @return void + */ + public function storeAssociation($serverUrl, $association) { + /* @var $association Auth_OpenID_Association */ + $GLOBALS['TYPO3_DB']->sql_query('START TRANSACTION'); + + if ($this->doesAssociationExist($serverUrl, $association->handle)) { + $this->updateExistingAssociation($serverUrl, $association); + } + else { + $this->storeNewAssociation($serverUrl, $association); + } + + $GLOBALS['TYPO3_DB']->sql_query('COMMIT'); + } + + /** + * Removes all expired associations. + * + * @return int A number of removed associations + */ + public function cleanupAssociations() { + $where = sprintf('expires<=%d', time()); + $GLOBALS['TYPO3_DB']->exec_DELETEquery(self::ASSOCIATION_TABLE_NAME, $where); + return $GLOBALS['TYPO3_DB']->sql_affected_rows(); + } + + /** + * Obtains the association to the server + * + * @param string $serverUrl Server URL + * @param string $handle Association handle (optional) + * @return Auth_OpenID_Association + */ + public function getAssociation($serverUrl, $handle = null) { + $this->cleanupAssociations(); + + $where = sprintf('server_url=%s AND expires>%d', + $GLOBALS['TYPO3_DB']->fullQuoteStr($serverUrl, self::ASSOCIATION_TABLE_NAME), + time()); + if ($handle != null) { + $where .= sprintf(' AND assoc_handle=%s', + $GLOBALS['TYPO3_DB']->fullQuoteStr($handle, self::ASSOCIATION_TABLE_NAME)); + $sort = ''; + } + else { + $sort = 'tstamp DESC'; + } + list($row) = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('uid,content', + self::ASSOCIATION_TABLE_NAME, $where, '', $sort, '1'); + + $result = null; + if (is_array($row)) { + $result = @unserialize($row['content']); + $this->updateAssociationTimeStamp($row['tstamp']); + } + return $result; + } + + /** + * Removes the association + * + * @param string $serverUrl Server URL + * @param string $handle Association handle (optional) + * @return boolean true if the association existed + */ + function removeAssociation($serverUrl, $handle) { + $where = sprintf('server_url=%s AND assoc_handle=%s', + $GLOBALS['TYPO3_DB']->fullQuoteStr($serverUrl, self::ASSOCIATION_TABLE_NAME), + $GLOBALS['TYPO3_DB']->fullQuoteStr($handle, self::ASSOCIATION_TABLE_NAME)); + $GLOBALS['TYPO3_DB']->exec_DELETEquery(self::ASSOCIATION_TABLE_NAME, $where); + $deletedCount = $GLOBALS['TYPO3_DB']->sql_affected_rows(); + return ($deletedCount > 0); + } + + /** + * Removes old nonces + * + * @return void + */ + public function cleanupNonces() { + $where = sprintf('crdate<%d', time() - self::NONCE_STORAGE_TIME); + $GLOBALS['TYPO3_DB']->exec_DELETEquery(self::NONCE_TABLE_NAME, $where); + } + + /** + * Checks if this nonce was already used + * @param $serverUrl Server URL + * @param $timestamp Time stamp + * @param $salt Nonce value + * @return boolean true if nonce was not used before anc can be used now + */ + public function useNonce($serverUrl, $timestamp, $salt) { + $result = false; + + if (abs($timestamp - time()) < $GLOBALS['Auth_OpenID_SKEW']) { + $values = array( + 'crdate' => time(), + 'salt' => $salt, + 'server_url' => $serverUrl, + 'tstamp' => $timestamp + ); + $GLOBALS['TYPO3_DB']->exec_INSERTquery(self::NONCE_TABLE_NAME, + $values); + $affectedRows = $GLOBALS['TYPO3_DB']->sql_affected_rows(); + $result = ($affectedRows > 0); + } + + return $result; + } + + /** + * Resets the store by removing all data in it + * + * @return void + */ + public function reset() { + $GLOBALS['TYPO3_DB']->exec_DELETEquery(self::ASSOCIATION_TABLE_NAME, '1=1'); + $GLOBALS['TYPO3_DB']->exec_DELETEquery(self::NONCE_TABLE_NAME, '1=1'); + } + + /** + * Checks if such association exists. + * + * @param string $serverUrl Server URL + * @param Auth_OpenID_Association $association OpenID association + * @return boolean + */ + protected function doesAssociationExist($serverUrl, $association) { + $where = sprintf('server_url=%s AND assoc_handle=%s AND expires>%d', + $GLOBALS['TYPO3_DB']->fullQuoteStr($serverUrl, self::ASSOCIATION_TABLE_NAME), + $GLOBALS['TYPO3_DB']->fullQuoteStr($association->handle, self::ASSOCIATION_TABLE_NAME), + time()); + list($row) = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows( + 'COUNT(*) as assocCount', self::ASSOCIATION_TABLE_NAME, $where); + return ($row['assocCount'] > 0); + } + + /** + * Updates existing association. + * + * @param string $serverUrl Server URL + * @param Auth_OpenID_Association $association OpenID association + * @return void + */ + protected function updateExistingAssociation($serverUrl, Auth_OpenID_Association $association) { + $where = sprintf('server_url=%s AND assoc_handle=%s AND expires>%d', + $GLOBALS['TYPO3_DB']->fullQuoteStr($serverUrl, self::ASSOCIATION_TABLE_NAME), + $GLOBALS['TYPO3_DB']->fullQuoteStr($association->handle, self::ASSOCIATION_TABLE_NAME), + time()); + $serializedAssociation = serialize($association); + $values = array( + 'content' => $serializedAssociation, + 'tstamp' => time(), + ); + $GLOBALS['TYPO3_DB']->exec_UPDATEquery(self::ASSOCIATION_TABLE_NAME, $where, $values); + } + + /** + * Stores new association to the database. + * + * @param $serverUrl Server URL + * @param $association OpenID association + * @return void + */ + protected function storeNewAssociation($serverUrl, $association) { + $serializedAssociation = serialize($association); + $values = array( + 'assoc_handle' => $association->handle, + 'content' => $serializedAssociation, + 'crdate' => $association->issued, + 'tstamp' => time(), + 'expires' => $association->issued + $association->lifetime - self::ASSOCIATION_EXPIRATION_SAFETY_INTERVAL, + 'server_url' => $serverUrl + ); + // In the next query we can get race conditions. sha1_hash prevents many + // asociations from being stored for one server + $GLOBALS['TYPO3_DB']->exec_INSERTquery(self::ASSOCIATION_TABLE_NAME, $values); + } + + /** + * Updates association time stamp. + * + * @param $recordId Association record id in the database + * @return void + */ + protected function updateAssociationTimeStamp($recordId) { + $where = sprintf('uid=%d', $recordId); + $values = array( + 'tstamp' => time() + ); + $GLOBALS['TYPO3_DB']->exec_UPDATEquery(self::ASSOCIATION_TABLE_NAME, $where, $values); + } + + /** + * Checks and database tables for this store. + * + * @return void + */ + protected function checkAndCreateTables() { + $tableNames = $this->findTableToCreate(); + if (count($tableNames) > 0) { + $this->doCreateTables($tableNames); + } + } + + /** + * Finds all missing database tables + * + * @return array + */ + protected function findTableToCreate() { + $tablesToCreate = array(); + $existingTables = array_keys($GLOBALS['TYPO3_DB']->admin_get_tables()); + // Do that only if could get table list! + if (count($existingTables) > 0) { + foreach (array('tx_openid_assoc_store', 'tx_openid_nonce_store') as $tableName) { + if (!in_array($tableName, $existingTables)) { + $tablesToCreate[] = $tableName; + } + } + } + return $tablesToCreate; + } + + /** + * Creates tables in the database. + * + * @param array $tableNames Table names + * @return void + */ + protected function doCreateTables(array $tableNames) { + $sqlFile = file_get_contents(t3lib_extMgm::extPath('openid', 'ext_tables.sql')); + foreach ($tableNames as $tableName) { + $this->doCreateTable($sqlFile, $tableName); + } + } + + /** + * Creates a single database table + * + * @param string $sqlFile ext_tables.sql content + * @param $tableName Table name + * @return void + */ + protected function doCreateTable($sqlFile, $tableName) { + $regularExpression = '/.*(CREATE\s*TABLE\s*`?' . $tableName . '`?\s*\([^;]+).*/is'; + $tableDefinition = preg_replace($regularExpression, '\1', $sqlFile); + if ($tableDefinition != '') { + $GLOBALS['TYPO3_DB']->sql_query($tableDefinition); + } + } +} + +if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/openid/class.tx_openid_store.php']) { + include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/openid/class.tx_openid_store.php']); +} + +?> \ No newline at end of file Index: typo3/sysext/openid/sv1/class.tx_openid_sv1.php =================================================================== --- typo3/sysext/openid/sv1/class.tx_openid_sv1.php (revision 6512) +++ typo3/sysext/openid/sv1/class.tx_openid_sv1.php (working copy) @@ -44,6 +44,7 @@ */ require_once(PATH_t3lib . 'class.t3lib_svbase.php'); +require_once(t3lib_extMgm::extPath('openid', 'sv1/class.tx_openid_store.php')); /** * Service "OpenID Authentication" for the "openid" extension. @@ -276,7 +277,6 @@ // Include files require_once($phpOpenIDLibPath . '/Auth/OpenID/Consumer.php'); - require_once($phpOpenIDLibPath . '/Auth/OpenID/FileStore.php'); // Restore path @set_include_path($oldIncludePath); @@ -319,16 +319,10 @@ * @return Auth_OpenID_Consumer Consumer instance */ protected function getOpenIDConsumer() { - // TODO Change this to a TYPO3-specific database-based store in future. - // File-based store is ineffective and insecure. After changing - // get rid of the FileStore include in includePHPOpenIDLibrary() - $openIDStorePath = PATH_site . 'typo3temp' . DIRECTORY_SEPARATOR . 'tx_openid'; + $openIDStore = t3lib_div::makeInstance('tx_openid_store'); + /* @var $openIDStore tx_openid_store */ + $openIDStore->cleanup(); - // For now we just prevent any web access to these files - if (!file_exists($openIDStorePath . '/.htaccess')) { - file_put_contents($openIDStorePath . '/.htaccess', 'deny from all'); - } - $openIDStore = new Auth_OpenID_FileStore($openIDStorePath); return new Auth_OpenID_Consumer($openIDStore); }