Session and ACL Login Class update

Session:
regenerate session id after some time or random.
Default is 'never', can be 'interval' form 0 to 1h and random from always to 1 in 100
Session also checks that strict session settings are enabled

Login class:
Automatic re-read of acl settings after some time (default 5min, can be chnaged via option).
Default set strict headers, can be turned off via option
Moved various parts into their own methods and cleaned up double call logic.
Login is now recorded in the last login entry
no more debug flags are read from the database anymore
All options are set via array and not with a single option (was auto login)
This commit is contained in:
Clemens Schwaighofer
2024-12-11 21:02:21 +09:00
parent 2b0434e36b
commit 8d3882a6fe
9 changed files with 944 additions and 527 deletions

View File

@@ -37,6 +37,8 @@ CREATE TABLE edit_user (
protected SMALLINT NOT NULL DEFAULT 0, protected SMALLINT NOT NULL DEFAULT 0,
-- is admin user -- is admin user
admin SMALLINT NOT NULL DEFAULT 0, admin SMALLINT NOT NULL DEFAULT 0,
-- force lgout counter
force_logout INT DEFAULT 0,
-- last login log -- last login log
last_login TIMESTAMP WITHOUT TIME ZONE, last_login TIMESTAMP WITHOUT TIME ZONE,
-- login error -- login error
@@ -74,6 +76,7 @@ COMMENT ON COLUMN edit_user.strict IS 'If too many failed logins user will be lo
COMMENT ON COLUMN edit_user.locked IS 'Locked from too many wrong password logins'; COMMENT ON COLUMN edit_user.locked IS 'Locked from too many wrong password logins';
COMMENT ON COLUMN edit_user.protected IS 'User can only be chnaged by admin user'; COMMENT ON COLUMN edit_user.protected IS 'User can only be chnaged by admin user';
COMMENT ON COLUMN edit_user.admin IS 'If set, this user is SUPER admin'; COMMENT ON COLUMN edit_user.admin IS 'If set, this user is SUPER admin';
COMMENT ON COLUMN edit_user.force_logout IS 'Counter for forced log out, if this one is higher than the session set one the session gets terminated';
COMMENT ON COLUMN edit_user.last_login IS 'Last succesfull login tiemstamp'; COMMENT ON COLUMN edit_user.last_login IS 'Last succesfull login tiemstamp';
COMMENT ON COLUMN edit_user.login_error_count IS 'Number of failed logins, reset on successful login'; COMMENT ON COLUMN edit_user.login_error_count IS 'Number of failed logins, reset on successful login';
COMMENT ON COLUMN edit_user.login_error_date_last IS 'Last login error date'; COMMENT ON COLUMN edit_user.login_error_date_last IS 'Last login error date';

View File

@@ -1185,7 +1185,6 @@ final class CoreLibsACLLoginTest extends TestCase
foreach ($session as $session_var => $session_value) { foreach ($session as $session_var => $session_value) {
$_SESSION[$session_var] = $session_value; $_SESSION[$session_var] = $session_value;
} }
/** @var \CoreLibs\ACL\Login&MockObject */ /** @var \CoreLibs\ACL\Login&MockObject */
$login_mock = $this->getMockBuilder(\CoreLibs\ACL\Login::class) $login_mock = $this->getMockBuilder(\CoreLibs\ACL\Login::class)
->setConstructorArgs([ ->setConstructorArgs([
@@ -1204,7 +1203,7 @@ final class CoreLibsACLLoginTest extends TestCase
. 'locale' . DIRECTORY_SEPARATOR, . 'locale' . DIRECTORY_SEPARATOR,
] ]
]) ])
->onlyMethods(['loginTerminate', 'loginReadPageName', 'loginPrintLogin']) ->onlyMethods(['loginTerminate', 'loginReadPageName', 'loginPrintLogin', 'loginEnhanceHttpSecurity'])
->getMock(); ->getMock();
$login_mock->expects($this->any()) $login_mock->expects($this->any())
->method('loginTerminate') ->method('loginTerminate')
@@ -1222,6 +1221,10 @@ final class CoreLibsACLLoginTest extends TestCase
->method('loginPrintLogin') ->method('loginPrintLogin')
->willReturnCallback(function () { ->willReturnCallback(function () {
}); });
$login_mock->expects($this->any())
->method('loginEnhanceHttpSecurity')
->willReturnCallback(function () {
});
// if mock_settings: enabled OFF // if mock_settings: enabled OFF
// run DB update and set off // run DB update and set off

View File

@@ -581,6 +581,8 @@ CREATE TABLE edit_user (
protected SMALLINT NOT NULL DEFAULT 0, protected SMALLINT NOT NULL DEFAULT 0,
-- is admin user -- is admin user
admin SMALLINT NOT NULL DEFAULT 0, admin SMALLINT NOT NULL DEFAULT 0,
-- forced logout counter
force_logout INT DEFAULT 0,
-- last login log -- last login log
last_login TIMESTAMP WITHOUT TIME ZONE, last_login TIMESTAMP WITHOUT TIME ZONE,
-- login error -- login error
@@ -697,6 +699,7 @@ CREATE TABLE edit_log (
action_value VARCHAR, -- in action_data action_value VARCHAR, -- in action_data
action_type VARCHAR, -- in action_data action_type VARCHAR, -- in action_data
action_error VARCHAR -- in action_data action_error VARCHAR -- in action_data
) INHERITS (edit_generic) WITHOUT OIDS;
-- END: table/edit_log.sql -- END: table/edit_log.sql
-- START: table/edit_log_overflow.sql -- START: table/edit_log_overflow.sql
-- AUTHOR: Clemens Schwaighofer -- AUTHOR: Clemens Schwaighofer

View File

@@ -54,7 +54,9 @@ final class CoreLibsCreateSessionTest extends TestCase
'getSessionId' => '1234abcd4567' 'getSessionId' => '1234abcd4567'
], ],
'sessionNameGlobals', 'sessionNameGlobals',
false, [
'auto_write_close' => false,
],
], ],
'auto write close' => [ 'auto write close' => [
'sessionNameAutoWriteClose', 'sessionNameAutoWriteClose',
@@ -66,7 +68,9 @@ final class CoreLibsCreateSessionTest extends TestCase
'getSessionId' => '1234abcd4567' 'getSessionId' => '1234abcd4567'
], ],
'sessionNameAutoWriteClose', 'sessionNameAutoWriteClose',
true, [
'auto_write_close' => true,
],
], ],
]; ];
} }
@@ -81,13 +85,14 @@ final class CoreLibsCreateSessionTest extends TestCase
* @param string $input * @param string $input
* @param array<mixed> $mock_data * @param array<mixed> $mock_data
* @param string $expected * @param string $expected
* @param array<string,mixed> $options
* @return void * @return void
*/ */
public function testStartSession( public function testStartSession(
string $input, string $input,
array $mock_data, array $mock_data,
string $expected, string $expected,
?bool $auto_write_close, ?array $options,
): void { ): void {
/** @var \CoreLibs\Create\Session&MockObject $session_mock */ /** @var \CoreLibs\Create\Session&MockObject $session_mock */
$session_mock = $this->createPartialMock( $session_mock = $this->createPartialMock(
@@ -174,9 +179,14 @@ final class CoreLibsCreateSessionTest extends TestCase
4, 4,
'/^\[SESSION\] Failed to activate session/' '/^\[SESSION\] Failed to activate session/'
], ],
'expired session' => [
\RuntimeException::class,
5,
'/^\[SESSION\] Expired session found/'
],
'not a valid session id returned' => [ 'not a valid session id returned' => [
\UnexpectedValueException::class, \UnexpectedValueException::class,
5, 6,
'/^\[SESSION\] getSessionId did not return a session id/' '/^\[SESSION\] getSessionId did not return a session id/'
], */ ], */
]; ];
@@ -206,7 +216,8 @@ final class CoreLibsCreateSessionTest extends TestCase
$this->expectException($exception); $this->expectException($exception);
$this->expectExceptionCode($exception_code); $this->expectExceptionCode($exception_code);
$this->expectExceptionMessageMatches($expected_error); $this->expectExceptionMessageMatches($expected_error);
new \CoreLibs\Create\Session($session_name); // cannot set ini after header sent, plus we are on command line there are no headers
new \CoreLibs\Create\Session($session_name, ['session_strict' => false]);
} }
/** /**

View File

@@ -7,6 +7,10 @@ ALTER TABLE edit_log ADD http_data JSONB;
ALTER TABLE edit_log ADD ip_address JSONB; ALTER TABLE edit_log ADD ip_address JSONB;
ALTER TABLE edit_log ADD action_data JSONB; ALTER TABLE edit_log ADD action_data JSONB;
ALTER TABLE edit_log ADD request_scheme VARCHAR; ALTER TABLE edit_log ADD request_scheme VARCHAR;
ALTER TABLE edit_user ADD force_logout INT DEFAULT 0;
COMMENT ON COLUMN edit_user.force_logout IS 'Counter for forced log out, if this one is higher than the session set one the session gets terminated';
ALTER TABLE edit_user ADD last_login TIMESTAMP WITHOUT TIME ZONE;
COMMENT ON COLUMN edit_user.last_login IS 'Last succesfull login tiemstamp';
-- update set_edit_gneric -- update set_edit_gneric
-- adds the created or updated date tags -- adds the created or updated date tags

View File

@@ -21,7 +21,10 @@ $SET_SESSION_NAME = EDIT_SESSION_NAME;
use CoreLibs\Debug\Support; use CoreLibs\Debug\Support;
// init login & backend class // init login & backend class
$session = new CoreLibs\Create\Session($SET_SESSION_NAME); $session = new CoreLibs\Create\Session($SET_SESSION_NAME, [
'regenerate' => 'interval',
'regenerate_interval' => 10, // every 10 seconds
]);
$log = new CoreLibs\Logging\Logging([ $log = new CoreLibs\Logging\Logging([
'log_folder' => BASE . LOG, 'log_folder' => BASE . LOG,
'log_file_id' => $LOG_FILE_ID, 'log_file_id' => $LOG_FILE_ID,
@@ -90,6 +93,8 @@ print <<<HTML
</div> </div>
HTML; HTML;
echo "SESSION ID: " . $session->getSessionIdCall() . "<br>";
echo "CHECK PERMISSION: " . ($login->loginCheckPermissions() ? 'OK' : 'BAD') . "<br>"; echo "CHECK PERMISSION: " . ($login->loginCheckPermissions() ? 'OK' : 'BAD') . "<br>";
echo "IS ADMIN: " . ($login->loginIsAdmin() ? 'OK' : 'BAD') . "<br>"; echo "IS ADMIN: " . ($login->loginIsAdmin() ? 'OK' : 'BAD') . "<br>";
echo "MIN ACCESS BASE: " . ($login->loginCheckAccessBase('admin') ? 'OK' : 'BAD') . "<br>"; echo "MIN ACCESS BASE: " . ($login->loginCheckAccessBase('admin') ? 'OK' : 'BAD') . "<br>";
@@ -118,8 +123,7 @@ if (isset($login->loginGetAcl()['unit'])) {
print "Something went wrong with the login<br>"; print "Something went wrong with the login<br>";
} }
echo "<hr>"; // echo "<hr>";
// IP check: 'REMOTE_ADDR', 'HTTP_X_FORWARDED_FOR', 'CLIENT_IP' in _SERVER // IP check: 'REMOTE_ADDR', 'HTTP_X_FORWARDED_FOR', 'CLIENT_IP' in _SERVER
// Agent check: 'HTTP_USER_AGENT' // Agent check: 'HTTP_USER_AGENT'

View File

@@ -146,7 +146,7 @@ $_SESSION['this_will_be_written'] = 'not empty';
// open again with same name // open again with same name
$session_name = 'class-test-session'; $session_name = 'class-test-session';
try { try {
$session_alt = new Session($session_name, auto_write_close:true); $session_alt = new Session($session_name, ['auto_write_close' => true]);
print "[4 SET] Current session id: " . $session_alt->getSessionId() . "<br>"; print "[4 SET] Current session id: " . $session_alt->getSessionId() . "<br>";
print "[4 SET] Current session auto write close: " . ($session_alt->checkAutoWriteClose() ? 'Yes' : 'No') . "<br>"; print "[4 SET] Current session auto write close: " . ($session_alt->checkAutoWriteClose() ? 'Yes' : 'No') . "<br>";
print "[START AGAIN] Current session id: " . $session_alt->getSessionId() . "<br>"; print "[START AGAIN] Current session id: " . $session_alt->getSessionId() . "<br>";

View File

@@ -217,6 +217,36 @@ class Login
'path' => '', 'path' => '',
]; ];
// lock status bitmap (smallint, 256)
/** @var int enabled flag */
public const ENABLED = 1;
/** @var int deleted flag */
public const DELETED = 2;
/** @var int locked flag */
public const LOCKED = 4;
/** @var int banned/suspened flag [not implemented] */
public const BANNED = 8;
/** @var int password reset in progress [not implemented] */
public const RESET = 16;
/** @var int confirm/paending, eg waiting for confirm of email [not implemented] */
public const CONFIRM = 32;
/** @var int strict, on error lock */
public const STRICT = 64;
/** @var int proected, cannot delete */
public const PROTECTED = 128;
/** @var int master admin flag */
public const ADMIN = 256;
/** @var int resync interval time in minutes */
private const DEFAULT_AUTH_RESYNC_INTERVAL = 5 * 60;
/** @var int the session max garbage collection life time */
// private const DEFAULT_SESSION_GC_MAXLIFETIME = ;
private int $default_session_gc_maxlifetime;
/** @var int in how many minutes an auth resync is done */
private int $auth_resync_interval;
/** @var bool set the enhanced header security */
private bool $header_enhance_security = false;
/** @var \CoreLibs\Logging\Logging logger */ /** @var \CoreLibs\Logging\Logging logger */
public \CoreLibs\Logging\Logging $log; public \CoreLibs\Logging\Logging $log;
/** @var \CoreLibs\DB\IO database */ /** @var \CoreLibs\DB\IO database */
@@ -248,156 +278,19 @@ class Login
// attach session class // attach session class
$this->session = $session; $this->session = $session;
$this->default_session_gc_maxlifetime = (int)ini_get("session.gc_maxlifetime");
// set and check options // set and check options
if (false === $this->loginSetOptions($options)) { if (false === $this->loginSetOptions($options)) {
// on failure, exit // on failure, exit
echo "<b>Could not set options</b>"; echo "<b>Could not set options</b>";
$this->loginTerminate('Could not set options', 3000); $this->loginTerminate('Could not set options', 3000);
} }
// init error array
// string key, msg: string, flag: e (error), o (ok) $this->loginInitErrorMessages();
$this->login_error_msg = [ // acess right list
'0' => [ $this->loginLoadAccessRightList();
'msg' => 'No error', // log allowed write flags
'flag' => 'o'
],
// actually obsolete
'100' => [
'msg' => '[EUCUUID] set from GET/POST!',
'flag' => 'e',
],
// query errors
'1009' => [
'msg' => 'Login query reading failed',
'flag' => 'e',
],
// user not found
'1010' => [
'msg' => 'Login Failed - Wrong Username or Password',
'flag' => 'e'
],
// blowfish password wrong
/* '1011' => [
'msg' => 'Login Failed - Wrong Username or Password',
'flag' => 'e'
], */
// fallback md5 password wrong
'1012' => [
'msg' => 'Login Failed - Wrong Username or Password',
'flag' => 'e'
],
// new password_hash wrong
'1013' => [
'msg' => 'Login Failed - Wrong Username or Password',
'flag' => 'e'
],
'1101' => [
'msg' => 'Login Failed - Login User ID must be validated',
'flag' => 'e'
],
'1102' => [
'msg' => 'Login Failed - Login User ID is outside valid date range',
'flag' => 'e'
],
'102' => [
'msg' => 'Login Failed - Please enter username and password',
'flag' => 'e'
],
'103' => [
'msg' => 'You do not have the rights to access this Page',
'flag' => 'e'
],
'104' => [
'msg' => 'Login Failed - User not enabled',
'flag' => 'e'
],
'105' => [
'msg' => 'Login Failed - User is locked',
'flag' => 'e'
],
'106' => [
'msg' => 'Login Failed - User is deleted',
'flag' => 'e'
],
'107' => [
'msg' => 'Login Failed - User in locked via date period',
'flag' => 'e'
],
'108' => [
'msg' => 'Login Failed - User is locked via Login User ID',
'flag' => 'e'
],
'109' => [
'msg' => 'Check permission query reading failed',
'flag' => 'e'
],
// actually this is an illegal user, but I mask it
'220' => [
'msg' => 'Password change - The user could not be found',
'flag' => 'e'
],
'200' => [
'msg' => 'Password change - Please enter username and old password',
'flag' => 'e'
],
'201' => [
'msg' => 'Password change - The user could not be found',
'flag' => 'e'
],
'202' => [
'msg' => 'Password change - The old password is not correct',
'flag' => 'e'
],
'203' => [
'msg' => 'Password change - Please fill out both new password fields',
'flag' => 'e'
],
'204' => [
'msg' => 'Password change - The new passwords do not match',
'flag' => 'e'
],
// we should also not here WHAT is valid
'205' => [
'msg' => 'Password change - The new password is not in a valid format',
'flag' => 'e'
],
// for OK password change
'300' => [
'msg' => 'Password change successful',
'flag' => 'o'
],
// this is bad bad error
'9999' => [
'msg' => 'Necessary crypt engine could not be found. Login is impossible',
'flag' => 'e'
],
];
// read the current edit_access_right list into an array
$q = <<<SQL
SELECT
level, type, name
FROM
edit_access_right
WHERE
level >= 0
ORDER BY
level
SQL;
while (is_array($res = $this->db->dbReturn($q))) {
// level to description format (numeric)
$this->default_acl_list[$res['level']] = [
'type' => $res['type'],
'name' => $res['name']
];
$this->default_acl_list_type[(string)$res['type']] = (int)$res['level'];
}
// write that into the session
$this->session->setMany([
'LOGIN_DEFAULT_ACL_LIST' => $this->default_acl_list,
'LOGIN_DEFAULT_ACL_LIST_TYPE' => $this->default_acl_list_type,
]);
$this->loginSetEditLogWriteTypeAvailable(); $this->loginSetEditLogWriteTypeAvailable();
// this will be deprecated // this will be deprecated
@@ -425,6 +318,7 @@ class Login
} else { } else {
$this->log->critical($message, ['code' => $code]); $this->log->critical($message, ['code' => $code]);
} }
// TODO throw error and not exit
exit($code); exit($code);
} }
@@ -577,6 +471,20 @@ class Login
} }
$this->password_forgot = $options['forgot_flow']; $this->password_forgot = $options['forgot_flow'];
// sync _SESSION acl settings
if (
!isset($options['auth_resync_interval']) ||
!is_numeric($options['auth_resync_interval']) ||
$options['auth_resync_interval'] < 0 ||
$options['auth_resync_interval'] > $this->default_session_gc_maxlifetime
) {
// default 5 minutues
$options['auth_resync_interval'] = self::DEFAULT_AUTH_RESYNC_INTERVAL;
} else {
$options['auth_resync_interval'] = (int)$options['auth_resync_interval'];
}
$this->auth_resync_interval = $options['auth_resync_interval'];
// *** LANGUAGE // *** LANGUAGE
// LANG: LOCALE PATH // LANG: LOCALE PATH
if (empty($options['locale_path'])) { if (empty($options['locale_path'])) {
@@ -631,12 +539,210 @@ class Login
$options['site_encoding'] = defined('SITE_ENCODING') && !empty(SITE_ENCODING) ? $options['site_encoding'] = defined('SITE_ENCODING') && !empty(SITE_ENCODING) ?
SITE_ENCODING : 'UTF-8'; SITE_ENCODING : 'UTF-8';
} }
// set enhancded security flag
if (
empty($options['enhanced_security']) ||
!is_bool($options['enhanced_security'])
) {
$options['enhanced_security'] = true;
}
$this->header_enhance_security = $options['enhanced_security'];
// write array to options // write array to options
$this->options = $options; $this->options = $options;
return true; return true;
} }
/**
* sets the login error message array
*
* @return void
*/
private function loginInitErrorMessages()
{
// string key, msg: string, flag: e (error), o (ok)
$this->login_error_msg = [
'0' => [
'msg' => 'No error',
'flag' => 'o'
],
// actually obsolete
'100' => [
'msg' => '[EUCUUID] set from GET/POST!',
'flag' => 'e',
],
// query errors
'1009' => [
'msg' => 'Login query reading failed',
'flag' => 'e',
],
// user not found
'1010' => [
'msg' => 'Login Failed - Wrong Username or Password',
'flag' => 'e'
],
// general login error
'1011' => [
'msg' => 'Login Failed - General authentication error',
'flag' => 'e'
],
// fallback md5 password wrong
'1012' => [
'msg' => 'Login Failed - Wrong Username or Password',
'flag' => 'e'
],
// new password_hash wrong
'1013' => [
'msg' => 'Login Failed - Wrong Username or Password',
'flag' => 'e'
],
'1101' => [
'msg' => 'Login Failed - Login User ID must be validated',
'flag' => 'e'
],
'1102' => [
'msg' => 'Login Failed - Login User ID is outside valid date range',
'flag' => 'e'
],
'102' => [
'msg' => 'Login Failed - Please enter username and password',
'flag' => 'e'
],
'103' => [
'msg' => 'You do not have the rights to access this Page',
'flag' => 'e'
],
'104' => [
'msg' => 'Login Failed - User not enabled',
'flag' => 'e'
],
'105' => [
'msg' => 'Login Failed - User is locked',
'flag' => 'e'
],
'106' => [
'msg' => 'Login Failed - User is deleted',
'flag' => 'e'
],
'107' => [
'msg' => 'Login Failed - User in locked via date period',
'flag' => 'e'
],
'108' => [
'msg' => 'Login Failed - User is locked via Login User ID',
'flag' => 'e'
],
'109' => [
'msg' => 'Check permission query reading failed',
'flag' => 'e'
],
'110' => [
'msg' => 'Forced logout',
'flag' => '',
],
// actually this is an illegal user, but I mask it
'220' => [
'msg' => 'Password change - The user could not be found',
'flag' => 'e'
],
'200' => [
'msg' => 'Password change - Please enter username and old password',
'flag' => 'e'
],
'201' => [
'msg' => 'Password change - The user could not be found',
'flag' => 'e'
],
'202' => [
'msg' => 'Password change - The old password is not correct',
'flag' => 'e'
],
'203' => [
'msg' => 'Password change - Please fill out both new password fields',
'flag' => 'e'
],
'204' => [
'msg' => 'Password change - The new passwords do not match',
'flag' => 'e'
],
// we should also not here WHAT is valid
'205' => [
'msg' => 'Password change - The new password is not in a valid format',
'flag' => 'e'
],
// for OK password change
'300' => [
'msg' => 'Password change successful',
'flag' => 'o'
],
// this is bad bad error
'9999' => [
'msg' => 'Necessary crypt engine could not be found. Login is impossible',
'flag' => 'e'
],
];
}
/**
* loads the access right list from the database
*
* @return void
*/
private function loginLoadAccessRightList(): void
{
// read the current edit_access_right list into an array
$q = <<<SQL
SELECT
level, type, name
FROM
edit_access_right
WHERE
level >= 0
ORDER BY
level
SQL;
while (is_array($res = $this->db->dbReturn($q))) {
// level to description format (numeric)
$this->default_acl_list[$res['level']] = [
'type' => $res['type'],
'name' => $res['name']
];
$this->default_acl_list_type[(string)$res['type']] = (int)$res['level'];
}
// write that into the session
$this->session->setMany([
'LOGIN_DEFAULT_ACL_LIST' => $this->default_acl_list,
'LOGIN_DEFAULT_ACL_LIST_TYPE' => $this->default_acl_list_type,
]);
}
/**
* Improves the application's security over HTTP(S) by setting specific headers
*
* @return void
*/
protected function loginEnhanceHttpSecurity(): void
{
// skip if not wanted
if (!$this->header_enhance_security) {
return;
}
// remove exposure of PHP version (at least where possible)
header_remove('X-Powered-By');
// if the user is signed in
if ($this->permission_okay) {
// prevent clickjacking
header('X-Frame-Options: sameorigin');
// prevent content sniffing (MIME sniffing)
header('X-Content-Type-Options: nosniff');
// disable caching of potentially sensitive data
header('Cache-Control: no-store, no-cache, must-revalidate', true);
header('Expires: Thu, 19 Nov 1981 00:00:00 GMT', true);
header('Pragma: no-cache', true);
}
}
// MARK: validation checks // MARK: validation checks
/** /**
@@ -649,6 +755,7 @@ class Login
* @param int $locked Locked because of too many invalid passwords * @param int $locked Locked because of too many invalid passwords
* @param int $locked_period Locked because of time period set * @param int $locked_period Locked because of time period set
* @param int $login_user_id_locked Locked from using Login User Id * @param int $login_user_id_locked Locked from using Login User Id
* @param int $force_logout Force logout counter, if higher than session, permission is false
* @return bool * @return bool
*/ */
private function loginValidationCheck( private function loginValidationCheck(
@@ -656,7 +763,8 @@ class Login
int $enabled, int $enabled,
int $locked, int $locked,
int $locked_period, int $locked_period,
int $login_user_id_locked int $login_user_id_locked,
int $force_logout
): bool { ): bool {
$validation = false; $validation = false;
if ($deleted) { if ($deleted) {
@@ -674,6 +782,8 @@ class Login
} elseif ($login_user_id_locked) { } elseif ($login_user_id_locked) {
// user is locked, either set or auto set // user is locked, either set or auto set
$this->login_error = 108; $this->login_error = 108;
} elseif ($force_logout > $this->session->get('LOGIN_FORCE_LOGOUT')) {
$this->login_error = 110;
} else { } else {
$validation = true; $validation = true;
} }
@@ -757,7 +867,112 @@ class Login
return $login_id_ok; return $login_id_ok;
} }
// MARK: login user action /**
* write error data for login errors
*
* @param array<string,mixed> $res
* @return void
*/
private function loginWriteLoginError(array $res)
{
if (!$this->login_error) {
return;
}
$login_error_date_first = '';
if ($res['login_error_count'] == 0) {
$login_error_date_first = ", login_error_date_first = NOW()";
}
// update login error count for this user
$q = <<<SQL
UPDATE edit_user
SET
login_error_count = login_error_count + 1,
login_error_date_last = NOW()
{LOGIN_ERROR_SQL}
WHERE edit_user_id = $1
SQL;
$this->db->dbExecParams(
str_replace('{LOGIN_ERROR_SQL}', $login_error_date_first, $q),
[$res['edit_user_id']]
);
// totally lock the user if error max is reached
if (
$this->max_login_error_count != -1 &&
$res['login_error_count'] + 1 > $this->max_login_error_count
) {
// do some alert reporting in case this error is too big
// if strict is set, lock this user
// this needs manual unlocking by an admin user
if ($res['strict'] && !in_array($this->username, $this->lock_deny_users)) {
$q = <<<SQL
UPDATE edit_user
SET locked = 1
WHERE edit_user_id = $1
SQL;
// [$res['edit_user_id']]
}
}
}
/**
* set the core edit_user table id/cuid/cuuid
*
* @param array<string,mixed> $res
* @return void
*/
private function loginSetEditUserUidData(array $res)
{
// normal user processing
// set class var and session var
$this->edit_user_id = (int)$res['edit_user_id'];
$this->edit_user_cuid = (string)$res['cuid'];
$this->edit_user_cuuid = (string)$res['cuuid'];
$this->session->setMany([
'LOGIN_EUID' => $this->edit_user_id,
'LOGIN_EUCUID' => $this->edit_user_cuid,
'LOGIN_EUCUUID' => $this->edit_user_cuuid,
]);
}
/**
* check for re-loading of ACL data after a period of time
* or if any of the core session vars is not set
*
* @return void
*/
private function loginAuthResync()
{
if (!$this->session->get('LOGIN_LAST_AUTH_RESYNC')) {
$this->session->set('LOGIN_LAST_AUTH_RESYNC', 0);
}
// reauth on missing session vars and timed out re-sync interval
$mandatory_session_vars = [
'LOGIN_USER_NAME', 'LOGIN_GROUP_NAME', 'LOGIN_EUCUID', 'LOGIN_EUCUUID',
'LOGIN_USER_ADDITIONAL_ACL', 'LOGIN_GROUP_ADDITIONAL_ACL',
'LOGIN_ADMIN', 'LOGIN_GROUP_ACL_LEVEL', 'LOGIN_PAGES_ACL_LEVEL', 'LOGIN_USER_ACL_LEVEL',
'LOGIN_UNIT', 'LOGIN_UNIT_DEFAULT_EACUID'
];
$force_reauth = false;
foreach ($mandatory_session_vars as $_session_var) {
if (!isset($_SESSION[$_session_var])) {
$force_reauth = true;
break;
}
}
if (
$this->session->get('LOGIN_LAST_AUTH_RESYNC') + $this->auth_resync_interval <= time() &&
$force_reauth == false
) {
return;
}
if (($res = $this->loginLoadUserData($this->edit_user_cuuid)) === false) {
return;
}
// set the session vars
$this->loginSetSession($res);
}
// MARK: MAIN LOGIN ACTION
/** /**
* if user pressed login button this script is called, * if user pressed login button this script is called,
@@ -769,6 +984,10 @@ class Login
{ {
// if pressed login at least and is not yet loggined in // if pressed login at least and is not yet loggined in
if ($this->edit_user_cuuid || (!$this->login && !$this->login_user_id)) { if ($this->edit_user_cuuid || (!$this->login && !$this->login_user_id)) {
// run reload user data based on re-auth timeout, but only if we got a set cuuid
if ($this->edit_user_cuuid) {
$this->loginAuthResync();
}
return; return;
} }
// if not username AND password where given // if not username AND password where given
@@ -778,16 +997,105 @@ class Login
$this->permission_okay = false; $this->permission_okay = false;
return; return;
} }
// have to get the global stuff here for setting it later // load user data, abort on error
// we have to get the themes in here too if (($res = $this->loginLoadUserData()) === false) {
return;
}
// if login errors is half of max errors and the last login error
// was less than 10s ago, forbid any new login try
// check flow
// - user is enabled
// - user is not locked
// - password is readable
// - encrypted password matches
// - plain password matches
if (
!$this->loginValidationCheck(
(int)$res['deleted'],
(int)$res['enabled'],
(int)$res['locked'],
(int)$res['locked_period'],
(int)$res['login_user_id_locked'],
(int)$res['force_logout']
)
) {
// error set in method (104, 105, 106, 107, 108)
} elseif (
empty($this->username) &&
!empty($this->login_user_id) &&
!$this->loginLoginUserIdCheck(
(int)$res['login_user_id_valid_date'],
(int)$res['login_user_id_revalidate']
)
) {
// check done in loginLoginIdCheck method
// aborts on must revalidate and not valid (date range)
} elseif (
!empty($this->username) &&
!$this->loginPasswordCheck($res['password'])
) {
// none to be set, set in login password check
// this is not valid password input error here
// all error codes are set in loginPasswordCheck method
// also valid if login_user_id is ok
} else {
// check if the current password is an invalid hash and do a rehash and set password
// $this->debug('LOGIN', 'Hash: '.$res['password'].' -> VERIFY: '
// .($Password::passwordVerify($this->password, $res['password']) ? 'OK' : 'FAIL')
// .' => HASH: '.(Password::passwordRehashCheck($res['password']) ? 'NEW NEEDED' : 'OK'));
if (Password::passwordRehashCheck($res['password'])) {
// update password hash to new one now
$q = <<<SQL
UPDATE edit_user
SET password = $1
WHERE edit_user_id = $2
SQL;
$this->db->dbExecParams($q, [
Password::passwordSet($this->password),
$res['edit_user_id']
]);
}
// normal user processing
// set class var and session var
$this->loginSetEditUserUidData($res);
// set the last login time stamp for normal login only (not for reauthenticate)
$this->db->dbExecParams(<<<SQL
UPDATE edit_user SET
last_login = NOW()
WHERE
edit_user_id = $1
SQL, [$this->edit_user_id]);
// set the session vars
$this->loginSetSession($res);
} // user was not enabled or other login error
// check for login error and write to the user
$this->loginWriteLoginError($res);
// if there was an login error, show login screen
if ($this->login_error) {
// reset the perm var, to confirm logout
$this->permission_okay = false;
}
}
/**
* load user data and all connect4ed settings
*
* @param ?string $edit_user_cuuid for re-auth
* @return array<string,mixed>|false
*/
private function loginLoadUserData(?string $edit_user_cuuid = null): array|false
{
$q = <<<SQL $q = <<<SQL
SELECT SELECT
eu.edit_user_id, eu.cuid, eu.cuuid, eu.username, eu.password, eu.edit_user_id, eu.cuid, eu.cuuid, eu.username, eu.password, eu.email,
eu.edit_group_id, eu.edit_group_id,
eg.name AS edit_group_name, eu.admin, eg.name AS edit_group_name, eu.admin,
-- additinal acl lists -- additinal acl lists
eu.additional_acl AS user_additional_acl, eu.additional_acl AS user_additional_acl,
eg.additional_acl AS group_additional_acl, eg.additional_acl AS group_additional_acl,
-- force logoutp counter
eu.force_logout,
-- login error + locked -- login error + locked
eu.login_error_count, eu.login_error_date_last, eu.login_error_count, eu.login_error_date_last,
eu.login_error_date_first, eu.strict, eu.locked, eu.login_error_date_first, eu.strict, eu.locked,
@@ -802,8 +1110,6 @@ class Login
OR (eu.lock_after IS NOT NULL AND NOW() <= eu.lock_after) OR (eu.lock_after IS NOT NULL AND NOW() <= eu.lock_after)
) )
) THEN 0::INT ELSE 1::INT END locked_period, ) THEN 0::INT ELSE 1::INT END locked_period,
-- debug (legacy)
eu.debug, eu.db_debug,
-- enabled -- enabled
eu.enabled, eu.deleted, eu.enabled, eu.deleted,
-- for checks only -- for checks only
@@ -851,8 +1157,12 @@ class Login
SQL; SQL;
$params = []; $params = [];
$replace_string = ''; $replace_string = '';
// either login_user_id OR password must be given // if login is OK and we have edit_user_cuuid as parameter, then this is internal re-auth
if (!empty($this->login_user_id && empty($this->username))) { // else login_user_id OR password must be given
if (!empty($edit_user_cuuid)) {
$replace_string = 'eu.cuuid = $1';
$params = [$this->edit_user_cuuid];
} elseif (!empty($this->login_user_id) && empty($this->username)) {
// check with login id if set and NO username // check with login id if set and NO username
$replace_string = 'eu.login_user_id = $1'; $replace_string = 'eu.login_user_id = $1';
$params = [$this->login_user_id]; $params = [$this->login_user_id];
@@ -874,83 +1184,39 @@ class Login
if (!empty($this->db->dbGetLastError())) { if (!empty($this->db->dbGetLastError())) {
$this->login_error = 1009; $this->login_error = 1009;
$this->permission_okay = false; $this->permission_okay = false;
return; return false;
} elseif (!is_array($res)) { } elseif (!is_array($res)) {
// username is wrong, but we throw for wrong username // username is wrong, but we throw for wrong username
// and wrong password the same error // and wrong password the same error
// unless with have edit user cuuid set then we run an general ACL error
if (empty($edit_user_cuuid)) {
$this->login_error = 1010; $this->login_error = 1010;
} else {
$this->login_error = 1011;
}
$this->permission_okay = false; $this->permission_okay = false;
return false;
}
return $res;
}
// MARK: login set all session variables
/**
* set all the _SESSION variables
*
* @param array<string,mixed> $res user data loaded query result
* @return void
*/
private function loginSetSession(array $res): void
{
// user has permission to THIS page
if ($this->login_error != 0) {
return; return;
} }
// if login errors is half of max errors and the last login error
// was less than 10s ago, forbid any new login try
// check flow
// - user is enabled
// - user is not locked
// - password is readable
// - encrypted password matches
// - plain password matches
if (
!$this->loginValidationCheck(
(int)$res['deleted'],
(int)$res['enabled'],
(int)$res['locked'],
(int)$res['locked_period'],
(int)$res['login_user_id_locked']
)
) {
// error set in method (104, 105, 106, 107, 108)
} elseif (
empty($this->username) &&
!empty($this->login_user_id) &&
!$this->loginLoginUserIdCheck(
(int)$res['login_user_id_valid_date'],
(int)$res['login_user_id_revalidate']
)
) {
// check done in loginLoginIdCheck method
// aborts on must revalidate and not valid (date range)
} elseif (
!empty($this->username) &&
!$this->loginPasswordCheck($res['password'])
) {
// none to be set, set in login password check
// this is not valid password input error here
// all error codes are set in loginPasswordCheck method
// also valid if login_user_id is ok
} else {
// check if the current password is an invalid hash and do a rehash and set password
// $this->debug('LOGIN', 'Hash: '.$res['password'].' -> VERIFY: '
// .($Password::passwordVerify($this->password, $res['password']) ? 'OK' : 'FAIL')
// .' => HASH: '.(Password::passwordRehashCheck($res['password']) ? 'NEW NEEDED' : 'OK'));
if (Password::passwordRehashCheck($res['password'])) {
// update password hash to new one now
$q = <<<SQL
UPDATE edit_user
SET password = $1
WHERE edit_user_id = $2
SQL;
$this->db->dbExecParams($q, [
Password::passwordSet($this->password),
$res['edit_user_id']
]);
}
// normal user processing
// set class var and session var
$this->edit_user_id = (int)$res['edit_user_id'];
$this->edit_user_cuid = (string)$res['cuid'];
$this->edit_user_cuuid = (string)$res['cuuid'];
$this->session->setMany([
'LOGIN_EUID' => $this->edit_user_id, // DEPRECATED
'LOGIN_EUCUID' => $this->edit_user_cuid,
'LOGIN_EUCUUID' => $this->edit_user_cuuid,
]);
// check if user is okay
$this->loginCheckPermissions();
if ($this->login_error == 0) {
// set the dit group id // set the dit group id
$edit_group_id = $res["edit_group_id"]; $edit_group_id = $res["edit_group_id"];
$edit_user_id = (int)$res['edit_user_id'];
// update last revalidate flag // update last revalidate flag
if ( if (
!empty($res['login_user_id']) && !empty($res['login_user_id']) &&
@@ -961,7 +1227,7 @@ class Login
SET login_user_id_last_revalidate = NOW() SET login_user_id_last_revalidate = NOW()
WHERE edit_user_id = $1 WHERE edit_user_id = $1
SQL; SQL;
$this->db->dbExecParams($q, [$this->edit_user_id]); $this->db->dbExecParams($q, [$edit_user_id]);
} }
$locale = $res['locale'] ?? 'en'; $locale = $res['locale'] ?? 'en';
$encoding = $res['encoding'] ?? 'UTF-8'; $encoding = $res['encoding'] ?? 'UTF-8';
@@ -970,8 +1236,13 @@ class Login
// DEBUG flag is deprecated // DEBUG flag is deprecated
// 'DEBUG_ALL' => $this->db->dbBoolean($res['debug']), // 'DEBUG_ALL' => $this->db->dbBoolean($res['debug']),
// 'DB_DEBUG' => $this->db->dbBoolean($res['db_debug']), // 'DB_DEBUG' => $this->db->dbBoolean($res['db_debug']),
// login timestamp
'LOGIN_LAST_AUTH_RESYNC' => time(),
// current forced logout counter
'LOGIN_FORCE_LOGOUT' => $res['force_logout'],
// general info for user logged in // general info for user logged in
'LOGIN_USER_NAME' => $res['username'], 'LOGIN_USER_NAME' => $res['username'],
'LOGIN_EMAIL' => $res['email'],
'LOGIN_ADMIN' => $res['admin'], 'LOGIN_ADMIN' => $res['admin'],
'LOGIN_GROUP_NAME' => $res['edit_group_name'], 'LOGIN_GROUP_NAME' => $res['edit_group_name'],
'LOGIN_USER_ACL_LEVEL' => $res['user_level'], 'LOGIN_USER_ACL_LEVEL' => $res['user_level'],
@@ -1014,7 +1285,7 @@ class Login
login_error_date_first = NULL login_error_date_first = NULL
WHERE edit_user_id = $1 WHERE edit_user_id = $1
SQL; SQL;
$this->db->dbExecParams($q, [$this->edit_user_id]); $this->db->dbExecParams($q, [$edit_user_id]);
} }
$edit_page_ids = []; $edit_page_ids = [];
$pages = []; $pages = [];
@@ -1143,7 +1414,7 @@ class Login
$eacuid = []; $eacuid = [];
$unit_acl = []; $unit_acl = [];
$unit_uid_lookup = []; $unit_uid_lookup = [];
while (is_array($res = $this->db->dbReturnParams($q, [$this->edit_user_id]))) { while (is_array($res = $this->db->dbReturnParams($q, [$edit_user_id]))) {
// read edit access data fields and drop them into the unit access array // read edit access data fields and drop them into the unit access array
$q_sub = <<<SQL $q_sub = <<<SQL
SELECT name, value SELECT name, value
@@ -1171,6 +1442,10 @@ class Login
'cuid' => $res['cuid'], 'cuid' => $res['cuid'],
]; ];
// set the default unit // set the default unit
$this->session->setMany([
'LOGIN_UNIT_DEFAULT_EAID' => null,
'LOGIN_UNIT_DEFAULT_EACUID' => null,
]);
if ($res['edit_default']) { if ($res['edit_default']) {
$this->session->set('LOGIN_UNIT_DEFAULT_EAID', (int)$res['edit_access_id']); // DEPRECATED $this->session->set('LOGIN_UNIT_DEFAULT_EAID', (int)$res['edit_access_id']); // DEPRECATED
$this->session->set('LOGIN_UNIT_DEFAULT_EACUID', (int)$res['cuid']); $this->session->set('LOGIN_UNIT_DEFAULT_EACUID', (int)$res['cuid']);
@@ -1191,49 +1466,6 @@ class Login
'LOGIN_EAID' => $eaid, // DEPRECATED 'LOGIN_EAID' => $eaid, // DEPRECATED
'LOGIN_EACUID' => $eacuid, 'LOGIN_EACUID' => $eacuid,
]); ]);
} // user has permission to THIS page
} // user was not enabled or other login error
if ($this->login_error && is_array($res)) {
$login_error_date_first = '';
if ($res['login_error_count'] == 0) {
$login_error_date_first = ", login_error_date_first = NOW()";
}
// update login error count for this user
$q = <<<SQL
UPDATE edit_user
SET
login_error_count = login_error_count + 1,
login_error_date_last = NOW()
{LOGIN_ERROR_SQL}
WHERE edit_user_id = $1
SQL;
$this->db->dbExecParams(
str_replace('{LOGIN_ERROR_SQL}', $login_error_date_first, $q),
[$res['edit_user_id']]
);
// totally lock the user if error max is reached
if (
$this->max_login_error_count != -1 &&
$res['login_error_count'] + 1 > $this->max_login_error_count
) {
// do some alert reporting in case this error is too big
// if strict is set, lock this user
// this needs manual unlocking by an admin user
if ($res['strict'] && !in_array($this->username, $this->lock_deny_users)) {
$q = <<<SQL
UPDATE edit_user
SET locked = 1
WHERE edit_user_id = $1
SQL;
// [$res['edit_user_id']]
}
}
}
// if there was an login error, show login screen
if ($this->login_error) {
// reset the perm var, to confirm logout
$this->permission_okay = false;
}
} }
// MARK: login set ACL // MARK: login set ACL
@@ -1361,7 +1593,7 @@ class Login
$this->acl['show_ea_extra'] = false; $this->acl['show_ea_extra'] = false;
} }
// set the default edit access // set the default edit access
$this->acl['default_edit_access'] = $_SESSION['UNIT_DEFAULT'] ?? null; $this->acl['default_edit_access'] = $_SESSION['LOGIN_UNIT_DEFAULT_EACUID'];
// integrate the type acl list, but only for the keyword -> level // integrate the type acl list, but only for the keyword -> level
$this->acl['min'] = $this->default_acl_list_type; $this->acl['min'] = $this->default_acl_list_type;
// set the full acl list too (lookup level number and get level data) // set the full acl list too (lookup level number and get level data)
@@ -2198,7 +2430,7 @@ HTML;
// row 2 // row 2
$_SERVER["REMOTE_ADDR"] ?? null, $_SERVER["REMOTE_ADDR"] ?? null,
Json::jsonConvertArrayTo([ Json::jsonConvertArrayTo([
'REMOTE_ADDR' => $_SERVER["REMOTE_ADDR"], 'REMOTE_ADDR' => $_SERVER["REMOTE_ADDR"] ?? null,
'HTTP_X_FORWARDED_FOR' => !empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? 'HTTP_X_FORWARDED_FOR' => !empty($_SERVER['HTTP_X_FORWARDED_FOR']) ?
explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']) explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])
: [], : [],
@@ -2262,7 +2494,7 @@ HTML;
// **** PUBLIC INTERNAL // **** PUBLIC INTERNAL
// ************************************************************************* // *************************************************************************
// MARK: PUBLIC LOGIN CALL // MARK: MASTER PUBLIC LOGIN CALL
/** /**
* Main call that needs to be run to actaully check for login * Main call that needs to be run to actaully check for login
@@ -2355,10 +2587,12 @@ HTML;
// if username & password & !$euid start login // if username & password & !$euid start login
$this->loginLoginUser(); $this->loginLoginUser();
// checks if $euid given check if user is okay for that side // checks if $euid given check if user is okay for that site
$this->loginCheckPermissions(); $this->loginCheckPermissions();
// logsout user // logout user
$this->loginLogoutUser(); $this->loginLogoutUser();
// set headers for enhanced security
$this->loginEnhanceHttpSecurity();
// ** LANGUAGE SET AFTER LOGIN ** // ** LANGUAGE SET AFTER LOGIN **
$this->loginSetLocale(); $this->loginSetLocale();
// load translator // load translator
@@ -2731,7 +2965,7 @@ HTML;
} }
$q = <<<SQL $q = <<<SQL
SELECT SELECT
ep.filename, eu.edit_user_id, eu.cuid, eu.cuuid, ep.filename, eu.edit_user_id, eu.cuid, eu.cuuid, eu.force_logout,
-- base lock flags -- base lock flags
eu.deleted, eu.enabled, eu.locked, eu.deleted, eu.enabled, eu.locked,
-- date based lock -- date based lock
@@ -2786,7 +3020,8 @@ HTML;
(int)$res['enabled'], (int)$res['enabled'],
(int)$res['locked'], (int)$res['locked'],
(int)$res['locked_period'], (int)$res['locked_period'],
(int)$res['login_user_id_locked'] (int)$res['login_user_id_locked'],
(int)$res['force_logout']
) )
) { ) {
// errors set in method // errors set in method
@@ -2810,14 +3045,7 @@ HTML;
$this->login_error = 103; $this->login_error = 103;
} }
// set all the internal vars // set all the internal vars
$this->edit_user_id = (int)$res['edit_user_id']; $this->loginSetEditUserUidData($res);
$this->edit_user_cuid = (string)$res['cuid'];
$this->edit_user_cuuid = (string)$res['cuuid'];
$this->session->setMany([
'LOGIN_EUID' => $this->edit_user_id, // DEPRECATED
'LOGIN_EUCUID' => $this->edit_user_cuid,
'LOGIN_EUCUUID' => $this->edit_user_cuuid,
]);
// if called from public, so we can check if the permissions are ok // if called from public, so we can check if the permissions are ok
return $this->permission_okay; return $this->permission_okay;
} }

View File

@@ -21,21 +21,107 @@ class Session
private string $session_id = ''; private string $session_id = '';
/** @var bool flag auto write close */ /** @var bool flag auto write close */
private bool $auto_write_close = false; private bool $auto_write_close = false;
/** @var string regenerate option, default never */
private string $regenerate = 'never';
/** @var int regenerate interval either 1 to 100 for random or 0 to 3600 for interval */
private int $regenerate_interval = 0;
/** @var array<string> allowed session id regenerate (rotate) options */
private const ALLOWED_REGENERATE_OPTIONS = ['none', 'random', 'interval'];
/** @var int default random interval */
public const DEFAULT_REGENERATE_RANDOM = 100;
/** @var int default rotate internval in minutes */
public const DEFAULT_REGENERATE_INTERVAL = 5 * 60;
/** @var int maximum time for regenerate interval is one hour */
public const MAX_REGENERATE_INTERAL = 60 * 60;
/** /**
* init a session, if array is empty or array does not have session_name set * init a session, if array is empty or array does not have session_name set
* then no auto init is run * then no auto init is run
* *
* @param string $session_name if set and not empty, will start session * @param string $session_name if set and not empty, will start session
* @param array<string,bool> $options
*/ */
public function __construct(string $session_name, bool $auto_write_close = false) public function __construct(
{ string $session_name,
array $options = []
) {
$this->setOptions($options);
$this->initSession($session_name); $this->initSession($session_name);
$this->auto_write_close = $auto_write_close;
} }
// MARK: private methods // MARK: private methods
/**
* set session class options
*
* @param array<string,bool> $options
* @return void
*/
private function setOptions(array $options): void
{
if (
!isset($options['auto_write_close']) ||
!is_bool($options['auto_write_close'])
) {
$options['auto_write_close'] = false;
}
$this->auto_write_close = $options['auto_write_close'];
if (
!isset($options['session_strict']) ||
!is_bool($options['session_strict'])
) {
$options['session_strict'] = true;
}
// set strict options, on not started sessiononly
if (
$options['session_strict'] &&
$this->getSessionStatus() === PHP_SESSION_NONE
) {
// use cookies to store session IDs
ini_set('session.use_cookies', 1);
// use cookies only (do not send session IDs in URLs)
ini_set('session.use_only_cookies', 1);
// do not send session IDs in URLs
ini_set('session.use_trans_sid', 0);
}
// session regenerate id options
if (
empty($options['regenerate']) ||
!in_array($options['regenerate'], self::ALLOWED_REGENERATE_OPTIONS)
) {
$options['regenerate'] = 'never';
}
$this->regenerate = (string)$options['regenerate'];
// for regenerate: 'random' (default 100)
// regenerate_interval must be between (1 = always) and 100 (1 in 100)
// for regenerate: 'interval' (default 5min)
// regenerate_interval must be 0 = always, to 3600 (every hour)
if (
$options['regenerate'] == 'random' &&
(
!isset($options['regenerate_interval']) ||
!is_numeric($options['regenerate_interval']) ||
$options['regenerate_interval'] < 0 ||
$options['regenerate_interval'] > 100
)
) {
$options['regenerate_interval'] = self::DEFAULT_REGENERATE_RANDOM;
}
if (
$options['regenerate'] == 'interval' &&
(
!isset($options['regenerate_interval']) ||
!is_numeric($options['regenerate_interval']) ||
$options['regenerate_interval'] < 1 ||
$options['regenerate_interval'] > self::MAX_REGENERATE_INTERAL
)
) {
$options['regenerate_interval'] = self::DEFAULT_REGENERATE_INTERVAL;
}
$this->regenerate_interval = (int)($options['regenerate_interval'] ?? 0);
}
/** /**
* Start session * Start session
* startSession should be called for complete check * startSession should be called for complete check
@@ -72,6 +158,72 @@ class Session
return false; return false;
} }
// MARK: regenerate session
/**
* auto rotate session id
*
* @return void
* @throws \RuntimeException failure to regenerate session id
* @throws \UnexpectedValueException failed to get new session id
* @throws \RuntimeException failed to set new sesson id
* @throws \UnexpectedValueException new session id generated does not match the new set one
*/
private function sessionRegenerateSessionId()
{
// never
if ($this->regenerate == 'never') {
return;
}
// regenerate
if (
!(
// is not session obsolete
empty($_SESSION['SESSION_REGENERATE_OBSOLETE']) &&
(
(
// random
$this->regenerate == 'random' &&
mt_rand(1, $this->regenerate_interval) == 1
) || (
// interval type
$this->regenerate == 'interval' &&
($_SESSION['SESSION_REGENERATE_TIMESTAMP'] ?? 0) + $this->regenerate_interval < time()
)
)
)
) {
return;
}
// Set current session to expire in 1 minute
$_SESSION['SESSION_REGENERATE_OBSOLETE'] = true;
$_SESSION['SESSION_REGENERATE_EXPIRES'] = time() + 60;
$_SESSION['SESSION_REGENERATE_TIMESTAMP'] = time();
// Create new session without destroying the old one
if (session_regenerate_id(false) === false) {
throw new \RuntimeException('[SESSION] Session id regeneration failed', 1);
}
// Grab current session ID and close both sessions to allow other scripts to use them
if (false === ($new_session_id = $this->getSessionIdCall())) {
throw new \UnexpectedValueException('[SESSION] getSessionIdCall did not return a session id', 2);
}
$this->writeClose();
// Set session ID to the new one, and start it back up again
if (($get_new_session_id = session_id($new_session_id)) === false) {
throw new \RuntimeException('[SESSION] set session_id failed', 3);
}
if ($get_new_session_id != $new_session_id) {
throw new \UnexpectedValueException('[SESSION] new session id does not match the new set one', 4);
}
$this->session_id = $new_session_id;
$this->startSessionCall();
// Don't want this one to expire
unset($_SESSION['SESSION_REGENERATE_OBSOLETE']);
unset($_SESSION['SESSION_REGENERATE_EXPIRES']);
}
// MARK: session validation
/** /**
* check if session name is valid * check if session name is valid
* *
@@ -151,6 +303,13 @@ class Session
if (!$this->checkActiveSession()) { if (!$this->checkActiveSession()) {
throw new \RuntimeException('[SESSION] Failed to activate session', 5); throw new \RuntimeException('[SESSION] Failed to activate session', 5);
} }
if (
!empty($_SESSION['SESSION_REGENERATE_OBSOLETE']) &&
!empty($_SESSION['SESSION_REGENERATE_EXPIRES']) && $_SESSION['SESSION_REGENERATE_EXPIRES'] < time()
) {
$this->sessionDestroy();
throw new \RuntimeException('[SESSION] Expired session found', 6);
}
} elseif ($session_name != $this->getSessionName()) { } elseif ($session_name != $this->getSessionName()) {
throw new \UnexpectedValueException( throw new \UnexpectedValueException(
'[SESSION] Another session exists with a different name: ' . $this->getSessionName(), '[SESSION] Another session exists with a different name: ' . $this->getSessionName(),
@@ -159,10 +318,12 @@ class Session
} }
// check session id // check session id
if (false === ($session_id = $this->getSessionIdCall())) { if (false === ($session_id = $this->getSessionIdCall())) {
throw new \UnexpectedValueException('[SESSION] getSessionId did not return a session id', 6); throw new \UnexpectedValueException('[SESSION] getSessionIdCall did not return a session id', 7);
} }
// set session id // set session id
$this->session_id = $session_id; $this->session_id = $session_id;
// run session id re-create from time to time
$this->sessionRegenerateSessionId();
// if flagged auto close, write close session // if flagged auto close, write close session
if ($this->auto_write_close) { if ($this->auto_write_close) {
$this->writeClose(); $this->writeClose();