[TYPO3-dev] Tangled-up user authentication -> Fixing extension attached
Christopher Lörken
christopher at loerken.net
Mon Apr 20 14:22:12 CEST 2009
Dear all,
I've written a fairly simple extension which extends tslib_feUserAuth
and replaces part of the code which causes the described behavior
(mainly the subsequent calls to fetchUserSession which I have replaced
with a method that has a "memory").
We are currently testing the extension and ... so far ... noone made it
to the forum to complain ;)
The effective reduced workload is:
Guest users: 1 DB call instead of 7
Authenticated users: 2-3 calls instead of 6-8 (depending on timestamp
updates)
I've added config flags for disabling fe_session_data and (@Krystian)
even the whole authentication procedure. Of course, both authentication
and access on fe_session_data are enabled by default.
I'd highly appreciate if someone who knows this stuff could review this
code, otherwise I would not load it to the TER.
I've especially got the following questions:
1: I never understood this session fixation thing. AFAIK, the code below
is allright, but since I didn't get the actual problem it might as well
be in there.
2: I've tried to make my changes minimally invasive so I don't think
that it will break anything. I am not aware of any reason why extensions
(or the core) would call one of the overwritten methods themselves and
/ or if this might break anything.
3: If I shall go on and upload this extension to TER, I wondered how to
move those config flags that currently have to be set to TYPO3_CONF_VARS
by hand to be configurable in TS? Did only find that for plugins...
I hope the pasted source gets out formatted ok...
Any comments are highly appreciated.
Cheers,
Christopher
--------------------------------
<?php
/***************************************************************
* Copyright notice
*
* (c) 2009 Bytro Labs
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
require_once PATH_tslib.'class.tslib_feuserauth.php';
/**
* fastauth extension to speed up the authentication of front end users.
*
* This class extends ''tslib_feUserAuth'' replacing some overly
complex login functions
* which result in redundant and unnecessary database calls.
*
* In other words, the original userauth causes the following DB
requests with _each and every_ page hit:
* - 7 DB calls on fe_sessions, fe_users, and fe_session_data for
guest users.
* - 6-8 calls for logged in and authenticated users.
*
* In a typical setup, this extension reduces the calls to:
* - 1 DB call for guests
* - 2-3 DB calls for registered users
*
* How ist it done?
* The data that is being read from the fe_sessions and fe_users table
simply
* is being rembered instead of read, thrown away, reread, ...
* Example 1: fetchUserSession normaly gets called twice on every
page hit, once, the result is only transferred
* into a boolean while the read data which could be read with the
same effort is disregarded.
* Example 2: When a user authenticates him/herself, the session and
login data is stored in the DB. And normally read
* immediately afterwards... This extension remembers those values.
*
* Configuration:
* You can place the following flags in your TYPO3_CONF_VARS array
(edit your localconf.php to do it)
* - disableSessionData
* Default setting is: 0
* $TYPO3_CONF_VARS['FE']['fastauth']['disableSessionData'] = 0/1
* If set to 1, all calls to the fe_session_data table are skipped.
These calls are unnecessary
* for sites that do not use extensions which use the
fe_session_data table.
* WARNING: This will break extensions that _do_ use that table so
only switch this on if you know
* what you're doing.
* - sessionUpdateTime
* Default setting is: 60
* $TYPO3_CONF_VARS['FE']['fastauth']['sessionUpdateTime'] = 60;
//or some integer
* Each fe_sessions record contains a ses_tstamp that is normally
being updated everytime a user loads a
* page. Set this to some value in seconds and the timestamp will
only be updated in the selected interval.
* - disableFrontendAuthentification
* Default setting is: 0
*
$TYPO3_CONF_VARS['FE']['fastauth']['disableFrontendAuthentification'] = 0/1;
* If set to 0, this will completely switch off FE user
authentification and session handling.
* WARNING: Use this only, if you neither have FE users, NOR use
fe_session_data, since _all_
* authentication calls will be skipped (all 7 calls for guest
users)!!
*
*
* -----------------------------------------------------------------
* Implementation details:
* 1) Calls are mainly avoided by RAM-"caching" the read session and
user details.
* 2) Main changes are in fetchUserSession
* 3) The comment of all methods contains a note CHANGES which explains
* in short the changes made to the overwritten method.
*
*
* -----------------------------------------------------------------
*
*
* @author Christopher Lörken <typo3 at bytro.com>
* @copyright 2009 Bytro Labs
* @version 2009-04-20
*/
class ux_tslib_feUserAuth extends tslib_feUserAuth {
/**
* If set to 1 via
$TYPO3_CONF_VARS['FE']['fastauth']['disableSessionData'] = 1; in
localconf.php,
* the fe_session_data table will be ignored.
* WARNING: This can result in extensions not working properly if they
use that table. If you don't use it, this will save you
* a couple of database requests.
* @var integer
*/
protected $_disableSessionData = 0;
/**
* Specifies the time in seconds that has to pass between updating the
timestamps in fe_sessions and fe_users.
* The default value is an update every 60 seconds.
* You can adjust the value in your localconf.php. E.g.:
* $TYPO3_CONF_VARS['FE']['fastauth']['sessionUpdateTime'] = 60;
* @var integer
*/
protected $_sessionUpdateTime = 60;
/**
* Flag that can be set as
*
$TYPO3_CONF_VARS['FE']['fastauth']['disableFrontendAuthentification'] = 1;
*
* WARNING: This flag completly skips _all_ calls to fe_sessions and
fe_session_data so no page which
* uses either of those tables will work!!!.
* @var integer
*/
protected $_disableFrontendAuthentification = 0;
/**
* This variable is used to buffer multiple calls to fetchUserSession.
Subsequent calls return the first result.
* @var array assoc containing the entries of fe_users and fe_sessions
*/
protected $_singletonSession;
/**
* CHANGES:
* - The fastauth config settings are read from TYPO3_CONF_VARS.
* - The minimally altered userauth->start is called.
*
* (non-PHPdoc)
* @see typo3/sysext/cms/tslib/tslib_feUserAuth#start()
*/
public function start() {
global $TYPO3_CONF_VARS;
///////////////////////////////////////////////
// these are fastauth configuration options
if (isset($TYPO3_CONF_VARS['FE']['fastauth']['disableSessionData'])) {
$this->_disableSessionData =
$TYPO3_CONF_VARS['FE']['fastauth']['disableSessionData'];
}
if (isset($TYPO3_CONF_VARS['FE']['fastauth']['sessionUpdateTime'])) {
$this->_sessionUpdateTime =
intval($TYPO3_CONF_VARS['FE']['fastauth']['sessionUpdateTime']);
}
if
(isset($TYPO3_CONF_VARS['FE']['fastauth']['disableFrontendAuthentification']))
{
$this->_disableFrontendAuthentification =
intval($TYPO3_CONF_VARS['FE']['fastauth']['disableFrontendAuthentification']);
}
// these are fastauth configuration options
///////////////////////////////////////////////
///////////////////////////////////////////////
// content of feuserauth->start()
if (intval($this->auth_timeout_field)>0 &&
intval($this->auth_timeout_field) < $this->lifetime) {
// If server session timeout is non-zero but less than client
session timeout: Copy this value instead.
$this->auth_timeout_field = $this->lifetime;
}
// content of feuserauth->start()
///////////////////////////////////////////////
///////////////////////////////////////////////
// calling minimally adjusted userauth->start()
$this->userauth_start();
// calling minimally adjusted userauth->start()
///////////////////////////////////////////////
}
/**
* CHANGES: This method is completly rewritten.
*
* Modified method for avoiding multiple calls to fe_sessions and fe_users.
* This method:
* - Reads fe_sessions and fe_users only _once_ per page hit.
* - Updates them only if they need to be updated.
* - Validates the session against timeout, IPlock and so on.
*
* Subsequent calls to this method will return the same value as the
first call. See $this->_singletonSession.
*
* NOTE: When a user logs in, this method will identify him as guest.
Afterwards, createUserSession will be called by t3lib_userauth,
* which overwrites the value of $this->_singletonSession making
this method to return the newly created session without
* having to read what has just been written.
*
* @return array user session data
*/
function fetchUserSession() {
$user = '';
if ($this->_disableFrontendAuthentification) return $user;
$session = '';
$sessionID = $this->id;
//early return if we have already read the users session
if (isset($this->_singletonSession)
&& $this->_singletonSession['ses_id'] == $this->id
&& $this->_singletonSession['ses_name'] == $this->name) {
//if a the singletonSession from a previous call contains a valid
user, reutrn it.
if (isset($this->_singletonSession[$this->userid_column])) {
return $this->_singletonSession;
} else {
//else return the empty user string.
return $user;
}
} else {
if ($this->writeDevLog) t3lib_div::devLog('Fetch session ses_id =
'.$this->id, 't3lib_userAuth');
$whereClause= 'ses_id =
'.$GLOBALS['TYPO3_DB']->fullQuoteStr($this->id, $this->session_table).'
AND ses_name = '.$GLOBALS['TYPO3_DB']->fullQuoteStr($this->name,
$this->session_table);
//1st: read row from fe_sessions
$dbres = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*',
$this->session_table, $whereClause);
if ($dbres !== false) {
$session = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($dbres);
if (isset($session['ses_id'])) {
//check IP Lock, HashLock, user where clause and session timestamp
$sessionAuthenticated = true;
$isExistingSession = true;
if ($this->lockIP) {
$checkIP = $this->ipLockClause_remoteIPNumber($this->lockIP);
if ($session['ses_iplock'] != $checkIP && $session['ses_iplock']
!= '[DISABLED]') {
$sessionAuthenticated = false;
}
}
if ($sessionAuthenticated && $session['ses_hashlock'] !=
$this->hashLockClause_getHashInt()) {
$sessionAuthenticated = false;
}
} else {
$isExistingSession = false;
$sessionAuthenticated = false;
}
} else {
$isExistingSession = false;
$sessionAuthenticated = false;
}
//2nd: get the user record from user_table
if ($sessionAuthenticated) {
$userID = $session['ses_userid'];
$dbres = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*',
$this->user_table, $this->userid_column.'='.intval($userID));
if ($dbres !== false) {
$user = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($dbres);
// A user was found, check if the session is still valid and if the
user is allowed to log in (e.g. not disabled...)
if (is_string($this->auth_timeout_field)) {
$timeout = intval($user[$this->auth_timeout_field]); // Get
timeout-time from usertable
} else {
$timeout = intval($this->auth_timeout_field); // Get timeout
from object
}
if ($timeout>0 && ($GLOBALS['EXEC_TIME'] >
($session['ses_tstamp']+$timeout))) {
//the session has expired
$sessionAuthenticated = false;
}
//The following are the checks that were formerly performed by
selecting only those useres who meet the
//user_where_clause() condition:
// (($this->enablecolumns['rootLevel']) ? 'AND
'.$this->user_table.'.pid=0 ' : '').
// (($this->enablecolumns['disabled']) ? ' AND
'.$this->user_table.'.'.$this->enablecolumns['disabled'].'=0' : '').
// (($this->enablecolumns['deleted']) ? ' AND
'.$this->user_table.'.'.$this->enablecolumns['deleted'].'=0' : '').
// (($this->enablecolumns['starttime']) ? ' AND
('.$this->user_table.'.'.$this->enablecolumns['starttime'].'<='.time().')'
: '').
// (($this->enablecolumns['endtime']) ? ' AND
('.$this->user_table.'.'.$this->enablecolumns['endtime'].'=0 OR
'.$this->user_table.'.'.$this->enablecolumns['endtime'].'>'.time().')' :
'');
if ($sessionAuthenticated && $this->enablecolumns['rootLevel'] &&
$user['pid'] != 0) {
$sessionAuthenticated = false;
}
if ($sessionAuthenticated && $this->enablecolumns['disabled'] &&
$user['disabled'] != 0) {
$sessionAuthenticated = false;
}
if ($sessionAuthenticated && $this->enablecolumns['deleted'] &&
$user['deleted'] != 0) {
$sessionAuthenticated = false;
}
if ($sessionAuthenticated && $this->enablecolumns['starttime'] &&
$user[$this->enablecolumns['starttime']] > time()) {
$sessionAuthenticated = false;
}
if ($sessionAuthenticated && $this->enablecolumns['endtime'] &&
$user[$this->enablecolumns['endtime']] != 0 &&
$user[$this->enablecolumns['endtime']] < time()) {
$sessionAuthenticated = false;
}
}
}
//3rd: remember the singleton variable for future reference
if ($sessionAuthenticated) {
//valid fe_session and valid fe_user
$user = array_merge($session, $user);
$this->updateTimestamps($user);
$this->_singletonSession = $user;
} else if ($isExistingSession) {
//existing fe_session that has expired or otherwise been
invalideted. Remove it and change session id. Return value will be
empty string.
$this->logoff();
$this->_singletonSession = array (
'ses_id' => $this->id,
'ses_name' => $this->name
);
} else {
//this is a fresh guest user who did not have a session. No need to
update any tables. Return value will be empty string.
$this->_singletonSession = array (
'ses_id' => $this->id,
'ses_name' => $this->name
);
}
}
return $user;
}
/**
* Method gets called for a user who has just entered his / her password.
*
* CHANGES: Additional to the original method is, that the values that
are written to the DB are
* immediately remembered in RAM so they do not have to be reread
in fetchUserSession.
*/
function createUserSession ($tempuser) {
parent::createUserSession($tempuser);
////////////////////////////////////////////////
// fastauth Modification for remembering this user:
//this only builds an array
$insertFields = $this->getNewSessionRecord($tempuser);
$tempuser[$this->lastLogin_column] = $GLOBALS['EXEC_TIME'];
$this->_singletonSession = array_merge($insertFields, $tempuser);
// fastauth Modification for remembering this user:
////////////////////////////////////////////////
}
/**
* Updates the fe_sessions ses_tstamp when the last update was more than
* $this->_sessionUpdateTime seconds ago (DEFAULT: 60 seconds).
* @param $user - pointer to assoc user array, the function sets the
lastlogin and ses_tstamp in the user array when they are updated
* @return void
*/
public function updateTimestamps(&$user) {
if ($GLOBALS['EXEC_TIME'] > $user['ses_tstamp'] +
$this->_sessionUpdateTime) {
//update session table
$GLOBALS['TYPO3_DB']->exec_UPDATEquery(
$this->session_table,
'ses_id='.$GLOBALS['TYPO3_DB']->fullQuoteStr($this->id,
$this->session_table).'
AND
ses_name='.$GLOBALS['TYPO3_DB']->fullQuoteStr($this->name,
$this->session_table),
array('ses_tstamp' => $GLOBALS['EXEC_TIME'])
);
$user['ses_tstamp'] = $GLOBALS['EXEC_TIME']; // Make sure that the
timestamp is also updated in the array
}
}
/**
* CHANGES: always returns true:
*
* This function is overwritten to actually disable the call to
isExistingSessionRecord. This matter is now handled
* in fetchUserSession resulting in two things:
* - No unnecessary additional calls to the database.
* - No changing of the ses_id of guest users with every page hit and
thus no spamming of MySQL query cache.
*
* (non-PHPdoc)
* @see
typo3/sysext/cms/tslib/tslib_feUserAuth#isExistingSessionRecord($id)
*/
public function isExistingSessionRecord($id) {
return true;
}
/**
* CHANGES: Fetches the session data from fe_session_data _if and only
if_ flag $this->_disableSessionData has not been set.
* (non-PHPdoc)
* @see typo3/sysext/cms/tslib/tslib_feUserAuth#fetchSessionData()
*/
public function fetchSessionData() {
if (!$this->_disableSessionData) {
parent::fetchSessionData();
}
}
/**
* CHANGES: Since we have removed isExistingSessionRecord, we'll change
the ses_id whenever a session is invalidated.
* @see #getNewSessionID()
*/
public function logoff() {
parent::logoff();
$this->getNewSessionID();
}
/**
* Mere Helper method.
* Generates a new session ID. Code taken from t3lib_userauth.
* @return void
*/
public function getNewSessionID() {
$this->id = substr(md5(uniqid('').getmypid()),0,$this->hash_length);
$this->newSessionID = TRUE;
}
/**
* This is the method of t3lib_userauth.
*
* CHANGES: The only change is, that all references to the local
field $id have
* been exchanged with access to $this->id.
* This is necessary to be able to change the session ID when the
user logs out or the session
* otherwise became invalid (e.g. timed out).
* Without this adjustment, the user would always keep the same ses_id.
*
* (non-PHPdoc)
* @see typo3/sysext/cms/tslib/tslib_feUserAuth#start()
*/
function userauth_start() {
global $TYPO3_CONF_VARS;
// backend or frontend login - used for auth services
$this->loginType = ($this->name=='fe_typo_user') ? 'FE' : 'BE';
// set level to normal if not already set
$this->security_level = $this->security_level ? $this->security_level
: 'normal';
// enable dev logging if set
if
($TYPO3_CONF_VARS['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['writeDevLog'])
$this->writeDevLog = TRUE;
if
($TYPO3_CONF_VARS['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['writeDevLog'.$this->loginType])
$this->writeDevLog = TRUE;
if (TYPO3_DLOG) $this->writeDevLog = TRUE;
if ($this->writeDevLog) t3lib_div::devLog('## Beginning of auth
logging.', 't3lib_userAuth');
// Init vars.
$mode = '';
$this->newSessionID = FALSE;
// $this->id is set to ses_id if cookie is present. Else set to
false, which will start a new session
$this->id = isset($_COOKIE[$this->name]) ?
stripslashes($_COOKIE[$this->name]) : '';
$this->hash_length = t3lib_div::intInRange($this->hash_length,6,32);
$this->svConfig = $TYPO3_CONF_VARS['SVCONF']['auth'];
// If fallback to get mode....
if (!$this->id && $this->getFallBack && $this->get_name) {
$this->id = isset($_GET[$this->get_name]) ?
t3lib_div::_GET($this->get_name) : '';
if (strlen($this->id)!=$this->hash_length) $this->id='';
$mode='get';
}
$this->cookieId = $this->id;
// If new session or client tries to fix session...
if (!$this->id || !$this->isExistingSessionRecord($this->id)) {
// New random session-$this->id is made
$this->id = substr(md5(uniqid('').getmypid()),0,$this->hash_length);
// New session
$this->newSessionID = TRUE;
}
// Internal var 'id' is set
//$this->id = $this->id;
// If fallback to get mode....
if ($mode=='get' && $this->getFallBack && $this->get_name) {
$this->get_URL_ID = '&'.$this->get_name.'='.$this->id;
}
// Make certain that NO user is set initially
$this->user = '';
// Check to see if anyone has submitted login-information and if so
register the user with the session. $this->user[uid] may be used to
write log...
$this->checkAuthentication();
// Make certain that NO user is set initially. ->check_authentication
may have set a session-record which will provide us with a user record
in the next section:
unset($this->user);
// re-read user session
$this->user = $this->fetchUserSession();
if ($this->writeDevLog && is_array($this->user))
t3lib_div::devLog('User session finally read:
'.t3lib_div::arrayToLogString($this->user,
array($this->userid_column,$this->username_column)), 't3lib_userAuth', -1);
if ($this->writeDevLog && !is_array($this->user))
t3lib_div::devLog('No user session found.', 't3lib_userAuth', 2);
// Setting cookies
if ($TYPO3_CONF_VARS['SYS']['cookieDomain']) {
if ($TYPO3_CONF_VARS['SYS']['cookieDomain']{0} == '/') {
$matchCnt = @preg_match($TYPO3_CONF_VARS['SYS']['cookieDomain'],
t3lib_div::getIndpEnv('TYPO3_HOST_ONLY'), $match);
if ($matchCnt === FALSE) {
t3lib_div::sysLog('The regular expression of
$TYPO3_CONF_VARS[SYS][cookieDomain] contains errors. The session is not
shared across sub-domains.', 'Core', 3);
} elseif ($matchCnt) {
$cookieDomain = $match[0];
}
} else {
$cookieDomain = $TYPO3_CONF_VARS['SYS']['cookieDomain'];
}
}
// If new session and the cookie is a sessioncookie, we need to set
it only once!
if ($this->isSetSessionCookie()) {
if (!$this->dontSetCookie) {
if ($cookieDomain) {
SetCookie($this->name, $this->id, 0, '/', $cookieDomain);
} else {
SetCookie($this->name, $this->id, 0, '/');
}
if ($this->writeDevLog) t3lib_div::devLog('Set new Cookie:
'.$this->id.($cookieDomain ? ', '.$cookieDomain : ''), 't3lib_userAuth');
}
}
// If it is NOT a session-cookie, we need to refresh it.
if ($this->isRefreshTimeBasedCookie()) {
if (!$this->dontSetCookie) {
if ($cookieDomain) {
SetCookie($this->name, $this->id, time()+$this->lifetime, '/',
$cookieDomain);
} else {
SetCookie($this->name, $this->id, time()+$this->lifetime, '/');
}
if ($this->writeDevLog) t3lib_div::devLog('Update Cookie:
'.$this->id.($cookieDomain ? ', '.$cookieDomain : ''), 't3lib_userAuth');
}
}
// Hook for alternative ways of filling the $this->user array (is
used by the "timtaw" extension)
if
(is_array($TYPO3_CONF_VARS['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postUserLookUp']))
{
foreach
($TYPO3_CONF_VARS['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postUserLookUp']
as $funcName) {
$_params = array(
'pObj' => &$this,
);
t3lib_div::callUserFunction($funcName,$_params,$this);
}
}
// If any redirection (inclusion of file) then it will happen in this
function
$this->redirect();
// Set all posible headers that could ensure that the script is not
cached on the client-side
if ($this->sendNoCacheHeaders) {
header('Expires: 0');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
}
// Set $this->gc_time if not explicitely specified
if ($this->gc_time==0) {
$this->gc_time = ($this->auth_timeout_field==0 ? 86400 :
$this->auth_timeout_field); // Default to 1 day if
$this->auth_timeout_field is 0
}
// If we're lucky we'll get to clean up old sessions....
if ((rand()%100) <= $this->gc_probability) {
$this->gc();
}
}
}
//if (defined('TYPO3_MODE') &&
$TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['tslib/class.tslib_feuserauth.php'])
{
//
include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['tslib/class.tslib_feuserauth.php']);
//}
?>
More information about the TYPO3-dev
mailing list