2186 lines
73 KiB
PHP
2186 lines
73 KiB
PHP
<?php
|
|
/*********************************************************************************
|
|
*
|
|
* TimeTrex is a Workforce Management program developed by
|
|
* TimeTrex Software Inc. Copyright (C) 2003 - 2021 TimeTrex Software Inc.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU Affero General Public License version 3 as published by
|
|
* the Free Software Foundation with the addition of the following permission
|
|
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
|
|
* WORK IN WHICH THE COPYRIGHT IS OWNED BY TIMETREX, TIMETREX DISCLAIMS THE
|
|
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
|
|
*
|
|
* This program 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 Affero General Public License for more
|
|
* details.
|
|
*
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License along
|
|
* with this program; if not, see http://www.gnu.org/licenses or write to the Free
|
|
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
* 02110-1301 USA.
|
|
*
|
|
*
|
|
* You can contact TimeTrex headquarters at Unit 22 - 2475 Dobbin Rd. Suite
|
|
* #292 West Kelowna, BC V4T 2E9, Canada or at email address info@timetrex.com.
|
|
*
|
|
*
|
|
* The interactive user interfaces in modified source and object code versions
|
|
* of this program must display Appropriate Legal Notices, as required under
|
|
* Section 5 of the GNU Affero General Public License version 3.
|
|
*
|
|
*
|
|
* In accordance with Section 7(b) of the GNU Affero General Public License
|
|
* version 3, these Appropriate Legal Notices must retain the display of the
|
|
* "Powered by TimeTrex" logo. If the display of the logo is not reasonably
|
|
* feasible for technical reasons, the Appropriate Legal Notices must display
|
|
* the words "Powered by TimeTrex".
|
|
*
|
|
********************************************************************************/
|
|
|
|
|
|
/**
|
|
* @package Core
|
|
*/
|
|
class Authentication {
|
|
protected $name = 'SessionID';
|
|
protected $idle_timeout = null; //Max IDLE time
|
|
protected $expire_session; //When TRUE, cookie is expired when browser closes.
|
|
protected $type_id = 0; //Default Pre-Login rather than 800 or something, as it has the potential to accidently promote sessions to something higher than it should be.
|
|
protected $mfa_type_id = 0;
|
|
protected $end_point_id = null;
|
|
protected $client_id = null;
|
|
protected $object_id = null;
|
|
protected $session_id = null;
|
|
protected $ip_address = null;
|
|
protected $user_agent = null;
|
|
protected $flags = null;
|
|
protected $created_date = null;
|
|
protected $updated_date = null;
|
|
protected $reauthenticated_date = null;
|
|
protected $other_json = [];
|
|
protected $mfa_start_listen = false;
|
|
public $mfa_timeout_seconds = 900; //15 minutes
|
|
|
|
//SmallINT datatype, max of 32767
|
|
protected $session_type_map = [
|
|
'pending_authentication' => 0,
|
|
//
|
|
//Non-Users.
|
|
//
|
|
'job_applicant' => 100,
|
|
'client_contact' => 110,
|
|
|
|
//
|
|
//Users
|
|
//
|
|
|
|
//Other hardware.
|
|
'ibutton' => 500,
|
|
'barcode' => 510,
|
|
'finger_print' => 520,
|
|
|
|
//QuickPunch
|
|
'quick_punch_id' => 600,
|
|
'phone_id' => 605, //This used to have to be 800 otherwise the Desktop PC app and touch-tone AGI scripts would fail, however that should be resolved now with changes to soap/server.php
|
|
'client_pc' => 610,
|
|
|
|
'api_key' => 700, //API key created after user_name/password authentication. This should be below any methods that use user_name/password to authenticate each time they login.
|
|
|
|
//SSO or alternative methods
|
|
'http_auth' => 705,
|
|
'sso' => 710,
|
|
|
|
//Username/Passwords including multifactor.
|
|
'user_name' => 800,
|
|
'user_name_multi_factor' => 810,
|
|
];
|
|
|
|
protected $obj = null;
|
|
protected $rl = null;
|
|
|
|
/**
|
|
* @var Cache_Lite_Function|Cache_Lite_Output
|
|
*/
|
|
protected $cache;
|
|
protected $db;
|
|
|
|
/**
|
|
* Authentication constructor.
|
|
*/
|
|
function __construct() {
|
|
global $db, $cache;
|
|
|
|
$this->db = $db;
|
|
$this->cache = $cache;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return mixed|Object|RateLimit
|
|
* @throws ReflectionException
|
|
*/
|
|
function getRateLimitObject() {
|
|
if ( is_object( $this->rl ) ) {
|
|
return $this->rl;
|
|
} else {
|
|
$this->rl = TTNew( 'RateLimit' );
|
|
$this->rl->setID( 'authentication_' . Misc::getRemoteIPAddress() );
|
|
$this->rl->setAllowedCalls( 20 );
|
|
$this->rl->setTimeFrame( 900 ); //15 minutes
|
|
|
|
return $this->rl;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param int $type_id
|
|
* @return bool|mixed
|
|
*/
|
|
function getNameByTypeId( $type_id ) {
|
|
if ( !is_numeric( $type_id ) ) {
|
|
$type_id = $this->getTypeIDByName( $type_id );
|
|
}
|
|
|
|
//Seperate session cookie names so if the user logs in with QuickPunch it doesn't log them out of the full interface for example.
|
|
$map = [
|
|
0 => 'SessionID',
|
|
100 => 'SessionID-JA', //Job Applicant
|
|
110 => 'SessionID-CC', //Client Contact
|
|
|
|
500 => 'SessionID-HW',
|
|
510 => 'SessionID-HW',
|
|
520 => 'SessionID-HW',
|
|
|
|
600 => 'SessionID-QP', //QuickPunch - Web Browser
|
|
605 => 'SessionID', //QuickPunch - Phone ID (Mobile App expects SessionID)
|
|
610 => 'SessionID-PC', //ClientPC
|
|
|
|
700 => 'SessionID',
|
|
705 => 'SessionID',
|
|
710 => 'SessionID',
|
|
800 => 'SessionID',
|
|
810 => 'SessionID',
|
|
];
|
|
|
|
if ( isset( $map[$type_id] ) ) {
|
|
return $map[$type_id];
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param bool $type_id
|
|
* @return bool|mixed
|
|
*/
|
|
function getName( $type_id = false ) {
|
|
if ( $type_id == '' ) {
|
|
$type_id = $this->getType();
|
|
}
|
|
|
|
return $this->getNameByTypeId( $type_id );
|
|
}
|
|
|
|
/**
|
|
* Determine if the session type is for an actual user, so we know if we can create audit logs.
|
|
* @param bool $type_id
|
|
* @return bool
|
|
*/
|
|
function isUser( $type_id = false ) {
|
|
if ( $type_id == '' ) {
|
|
$type_id = $this->getType();
|
|
}
|
|
|
|
//If this is updated, modify PurgeDatabase.class.php for authentication table as well.
|
|
if ( in_array( $type_id, [ 100, 110 ] ) ) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param $type
|
|
* @return bool|int
|
|
*/
|
|
function getTypeIDByName( $type ) {
|
|
$type = strtolower( $type );
|
|
|
|
if ( isset( $this->session_type_map[$type] ) ) {
|
|
return (int)$this->session_type_map[$type];
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return string|bool
|
|
*/
|
|
function getTypeName() {
|
|
$type_id_to_name_map = array_flip( $this->session_type_map );
|
|
if ( isset( $type_id_to_name_map[$this->getType()] ) ) {
|
|
return $type_id_to_name_map[$this->getType()];
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return int
|
|
*/
|
|
function getType() {
|
|
return $this->type_id;
|
|
}
|
|
|
|
/**
|
|
* @param int $type_id
|
|
* @return bool
|
|
*/
|
|
function setType( $type_id ) {
|
|
if ( !is_numeric( $type_id ) ) {
|
|
$type_id = $this->getTypeIDByName( $type_id );
|
|
}
|
|
|
|
if ( is_numeric( $type_id ) ) {
|
|
$this->type_id = (int)$type_id;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return int
|
|
*/
|
|
function getMFATypeId() {
|
|
return $this->mfa_type_id;
|
|
}
|
|
|
|
/**
|
|
* @param int $mfa_type_id
|
|
* @return bool
|
|
*/
|
|
function setMFATypeId( $mfa_type_id ) {
|
|
$this->mfa_type_id = (int)$mfa_type_id;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return null
|
|
*/
|
|
function getIPAddress() {
|
|
return $this->ip_address;
|
|
}
|
|
|
|
/**
|
|
* @param null $ip_address
|
|
* @return bool
|
|
*/
|
|
function setIPAddress( $ip_address = null ) {
|
|
if ( empty( $ip_address ) ) {
|
|
$ip_address = Misc::getRemoteIPAddress();
|
|
}
|
|
|
|
if ( !empty( $ip_address ) ) {
|
|
$this->ip_address = $ip_address;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return int
|
|
*/
|
|
function getIdleTimeout() {
|
|
if ( $this->idle_timeout == null ) {
|
|
global $config_vars;
|
|
if ( isset( $config_vars['other']['web_session_timeout'] ) && $config_vars['other']['web_session_timeout'] != '' ) {
|
|
$this->idle_timeout = (int)$config_vars['other']['web_session_timeout'];
|
|
} else {
|
|
$this->idle_timeout = 14400; //Default to 4-hours.
|
|
}
|
|
}
|
|
|
|
Debug::text( 'Idle Seconds Allowed: ' . $this->idle_timeout, __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
return $this->idle_timeout;
|
|
}
|
|
|
|
/**
|
|
* @param $secs
|
|
* @return bool
|
|
*/
|
|
function setIdleTimeout( $secs ) {
|
|
if ( $secs != '' && is_numeric( $secs ) ) {
|
|
$this->idle_timeout = (int)$secs;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param null $end_point_id
|
|
* @return mixed|string
|
|
*/
|
|
function parseEndPointID( $end_point_id = null ) {
|
|
if ( $end_point_id == null && isset( $_SERVER['SCRIPT_NAME'] ) && $_SERVER['SCRIPT_NAME'] != '' ) {
|
|
$end_point_id = $_SERVER['SCRIPT_NAME'];
|
|
}
|
|
|
|
$end_point_id = Environment::stripDuplicateSlashes( $end_point_id );
|
|
|
|
//If the SCRIPT_NAME is something like upload_file.php, or APIGlobal.js.php, assume its the JSON API
|
|
// soap/server.php is a SOAP end-point.
|
|
// This is also set in parseEndPointID() and getClientIDHeader()
|
|
// /api/json/api.php should be: json/api
|
|
// /api/soap/api.php should be: soap/api
|
|
// /api/report/api.php should be: report/api
|
|
// /soap/server.php should be: soap/server
|
|
// See MiscTest::testAuthenticationParseEndPoint() for unit tests.
|
|
if ( $end_point_id == '' || ( strpos( $end_point_id, 'api' ) === false && strpos( $end_point_id, 'soap/server.php' ) === false ) ) {
|
|
$retval = 'json/api';
|
|
} else {
|
|
//$retval = Environment::stripDuplicateSlashes( str_replace( [ dirname( Environment::getAPIBaseURL() ) . '/', '.php' ], '', $end_point_id ) );
|
|
if ( strpos( $end_point_id, 'api/json/api' ) !== false ) {
|
|
$retval = 'json/api';
|
|
} else if ( strpos( $end_point_id, 'api/soap/api' ) !== false ) {
|
|
$retval = 'soap/api';
|
|
} else if ( strpos( $end_point_id, 'api/report/api' ) !== false ) {
|
|
$retval = 'report/api';
|
|
} else if ( strpos( $end_point_id, 'soap/server.php' ) !== false ) {
|
|
$retval = 'soap/server';
|
|
} else if ( strpos( $end_point_id, 'api/time_clock/api' ) !== false ) {
|
|
$retval = 'time_clock/api';
|
|
} else {
|
|
$retval = 'json/api'; //Default to this.
|
|
}
|
|
}
|
|
|
|
$retval = strtolower( trim( $retval, '/' ) ); //Strip leading and trailing slashes.
|
|
//Debug::text('End Point: '. $retval .' Input: '. $end_point_id .' API Base URL: '. Environment::getAPIBaseURL(), __FILE__, __LINE__, __METHOD__, 10);
|
|
|
|
return $retval;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
function getEndPointID( $end_point_id = null ) {
|
|
if ( $end_point_id != '' ) {
|
|
$this->end_point_id = $end_point_id;
|
|
Debug::text('Forced End Point: '. $end_point_id, __FILE__, __LINE__, __METHOD__, 10);
|
|
}
|
|
|
|
if ( $this->end_point_id == null ) {
|
|
$this->end_point_id = $this->parseEndPointID( $end_point_id );
|
|
}
|
|
|
|
return $this->end_point_id;
|
|
}
|
|
|
|
/**
|
|
* @param $value
|
|
* @return bool
|
|
*/
|
|
function setEndPointID( $value ) {
|
|
if ( $value != '' ) {
|
|
$this->end_point_id = substr( $value, 0, 30 );
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
function getClientID() {
|
|
if ( $this->client_id == null ) {
|
|
$this->client_id = strtolower( $this->getClientIDHeader() );
|
|
}
|
|
|
|
return $this->client_id;
|
|
}
|
|
|
|
/**
|
|
* @param $value
|
|
* @return bool
|
|
*/
|
|
function setClientID( $value ) {
|
|
if ( $value != '' ) {
|
|
$this->client_id = strtolower( substr( $value, 0, 30 ) );
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
function getUserAgent() {
|
|
if ( $this->user_agent == null ) {
|
|
$this->user_agent = sha1( ( isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null ) . TTPassword::getPasswordSalt() ); //Hash the user agent so its not as long.
|
|
}
|
|
|
|
return $this->user_agent;
|
|
}
|
|
|
|
/**
|
|
* @param $value
|
|
* @param bool $hash
|
|
* @return bool
|
|
*/
|
|
function setUserAgent( $value, $hash = false ) {
|
|
if ( $value != '' ) {
|
|
if ( $hash == true ) {
|
|
$value = sha1( $value . TTPassword::getPasswordSalt() ); //Hash the user agent so its not as long.
|
|
}
|
|
|
|
$this->user_agent = substr( $value, 0, 40 );
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//Expire Session when browser is closed?
|
|
function getEnableExpireSession() {
|
|
return $this->expire_session;
|
|
}
|
|
|
|
/**
|
|
* @param $bool
|
|
* @return bool
|
|
*/
|
|
function setEnableExpireSession( $bool ) {
|
|
$this->expire_session = (bool)$bool;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
function setReauthenticatedSession( $force_update = false ) {
|
|
$json_data = $this->getOtherJSON();
|
|
$json_data['mfa']['one_time_auth'] = true;
|
|
|
|
$this->setOtherJSON( $json_data );
|
|
$this->setReauthenticatedDate( TTDate::getTime() );
|
|
|
|
if ( $force_update == true ) {
|
|
$this->Update();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks if the user has a valid one_time_auth flag set which allows them to perform a single action without reauthenticating.
|
|
* @return bool
|
|
*/
|
|
function isSessionReauthenticated() {
|
|
if ( $this->getReauthenticatedDate() == false || ( ( TTDate::getTime() - $this->getReauthenticatedDate() ) > $this->mfa_timeout_seconds ) ) {
|
|
return false;
|
|
}
|
|
|
|
$other_json = $this->getOtherJSON();
|
|
if ( isset( $other_json['mfa']['one_time_auth'] ) && $other_json['mfa']['one_time_auth'] == true ) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return null
|
|
*/
|
|
function getCreatedDate() {
|
|
return $this->created_date;
|
|
}
|
|
|
|
/**
|
|
* @param int $epoch EPOCH
|
|
* @return bool
|
|
*/
|
|
function setCreatedDate( $epoch = null ) {
|
|
if ( $epoch == '' ) {
|
|
$epoch = time();
|
|
}
|
|
|
|
if ( is_numeric( $epoch ) ) {
|
|
$this->created_date = $epoch;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return null
|
|
*/
|
|
function getUpdatedDate() {
|
|
return $this->updated_date;
|
|
}
|
|
|
|
/**
|
|
* @param int $epoch EPOCH
|
|
* @return bool
|
|
*/
|
|
function setUpdatedDate( $epoch = null ) {
|
|
if ( $epoch == '' ) {
|
|
$epoch = time();
|
|
}
|
|
|
|
if ( is_numeric( $epoch ) ) {
|
|
$this->updated_date = $epoch;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return null
|
|
*/
|
|
function getReauthenticatedDate() {
|
|
return $this->reauthenticated_date;
|
|
}
|
|
|
|
/**
|
|
* @param int $epoch EPOCH
|
|
* @return bool
|
|
*/
|
|
function setReauthenticatedDate( $epoch = null ) {
|
|
if ( is_numeric( $epoch ) ) {
|
|
$this->reauthenticated_date = $epoch;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return null
|
|
*/
|
|
function getOtherJSON() {
|
|
return $this->other_json ?? []; //Return empty array if not set.
|
|
}
|
|
|
|
/**
|
|
* @param array $data
|
|
* @return bool
|
|
*/
|
|
function setOtherJSON( $data ) {
|
|
if ( is_array( $data ) == false ) {
|
|
$data = [ $data ];
|
|
}
|
|
$this->other_json = $data;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Register permanent API key Session ID to be used for all subsequent API calls without needing a username/password.
|
|
* @param string $user_name
|
|
* @param string $password
|
|
* @return bool|string
|
|
* @throws DBError
|
|
*/
|
|
function registerAPIKey( $user_name, $password, $end_point = null ) {
|
|
$login_result = $this->Login( $user_name, $password, 'USER_NAME', false ); //Make sure login succeeds before generating API key. -- Make sure we don't set any cookies when registering an API key, as the cookie that is set will different from what is returned here anyways.
|
|
if ( $login_result === true ) {
|
|
return $this->generateAPIKey( $this->getObjectID(), $end_point );
|
|
}
|
|
|
|
Debug::text( 'Password match failed, unable to create API Key session for User ID: ' . $this->getObjectID() . ' Original SessionID: ' . $this->getSessionID(), __FILE__, __LINE__, __METHOD__, 10 );
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param $end_point
|
|
* @return string|null
|
|
*/
|
|
function registerAPIKeyForCurrentUser( $end_point ) {
|
|
return $this->generateAPIKey( $this->getObjectID(), $end_point );
|
|
}
|
|
|
|
/**
|
|
* @param $user_id
|
|
* @param $end_point
|
|
* @return string|null
|
|
*/
|
|
protected function generateAPIKey( $user_id, $end_point ) {
|
|
$authentication = new Authentication();
|
|
$authentication->setType( 700 ); //API Key
|
|
$authentication->setSessionID( 'API'. $this->genSessionID() );
|
|
$authentication->setIPAddress();
|
|
|
|
if ( $this->getEndPointID( $end_point ) == 'json/api' || $this->getEndPointID( $end_point ) == 'soap/api' || $this->getEndPointID( $end_point ) == 'report/api' ) {
|
|
$authentication->setEndPointID( $this->getEndPointID( $end_point ) ); //json/api, soap/api
|
|
}
|
|
$authentication->setClientID( 'api' );
|
|
$authentication->setUserAgent( 'API KEY' ); //Force the same user agent for all API keys, as its very likely could change across time as these are long-lived keys.
|
|
$authentication->setIdleTimeout( ( 90 * 86400 ) ); //90 Days of inactivity.
|
|
$authentication->setCreatedDate();
|
|
$authentication->setUpdatedDate();
|
|
$authentication->setObjectID( $user_id );
|
|
|
|
Debug::text( 'Creating API Key session for User ID: ' . $user_id . ' Original SessionID: ' . $this->getSessionID() .' New SessionID: '. $authentication->getSessionID() . ' DB: ' . $authentication->encryptSessionID( $authentication->getSessionID() ), __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
//Write data to db.
|
|
$authentication->Write();
|
|
|
|
TTLog::addEntry( $this->getObjectID(), 10, TTi18n::getText( 'Registered API Key' ) . ': ' . $authentication->getSecureSessionID() . ' ' . TTi18n::getText( 'End Point' ) . ': ' . $authentication->getEndPointID(), $this->getObjectID(), 'authentication' ); //Add
|
|
|
|
return $authentication->getSessionID();
|
|
}
|
|
|
|
/**
|
|
* Duplicates existing session with a new SessionID. Useful for multiple logins with the same or different users.
|
|
* @param string $object_id UUID
|
|
* @param string $ip_address
|
|
* @param string $user_agent
|
|
* @param string $client_id UUID
|
|
* @param string $end_point_id
|
|
* @param null $type_id
|
|
* @return null
|
|
* @throws DBError
|
|
*/
|
|
function newSession( $object_id = null, $ip_address = null, $user_agent = null, $client_id = null, $end_point_id = null, $type_id = null ) {
|
|
if ( $object_id == '' && $this->getObjectID() != '' ) {
|
|
$object_id = $this->getObjectID();
|
|
}
|
|
|
|
if ( $type_id == null ) {
|
|
$type_id = $this->getType();
|
|
}
|
|
|
|
//Allow switching from type_id=700 (API Key) to 800 (username/password) so we can impersonate across API key to browser. Only allow going from 810 to 810 (MFA).
|
|
if ( !( ( $this->getType() == 700 || $this->getType() == 800 || $this->getType() == 810 ) && ( $type_id == 700 || $type_id == 800 || $type_id == 810 ) ) ) {
|
|
Debug::text( ' ERROR: Invalid from/to Type IDs! From Type: ' . $this->getType() . ' To Type: '. $type_id, __FILE__, __LINE__, __METHOD__, 10 );
|
|
return false;
|
|
}
|
|
|
|
$new_session_id = $this->genSessionID();
|
|
Debug::text( 'Duplicating session to User ID: ' . $object_id . ' Original SessionID: ' . $this->getSessionID() . ' New Session ID: ' . $new_session_id . ' IP Address: ' . $ip_address . ' Type: ' . $type_id . ' End Point: ' . $end_point_id . ' Client ID: ' . $client_id . ' DB: ' . $this->encryptSessionID( $new_session_id ), __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
$authentication = new Authentication();
|
|
$authentication->setType( $type_id );
|
|
$authentication->setSessionID( $new_session_id );
|
|
$authentication->setIPAddress( $ip_address );
|
|
$authentication->setEndPointID( $end_point_id );
|
|
$authentication->setClientID( $client_id );
|
|
$authentication->setUserAgent( $user_agent, true ); //Force hash the user agent.
|
|
$authentication->setCreatedDate();
|
|
$authentication->setUpdatedDate();
|
|
$authentication->setObjectID( $object_id );
|
|
|
|
//Sets session cookie.
|
|
//$authentication->setCookie();
|
|
|
|
//Write data to db.
|
|
$authentication->Write();
|
|
|
|
//$authentication->UpdateLastLoginDate(); //Don't do this when switching users.
|
|
|
|
return $authentication->getSessionID();
|
|
}
|
|
|
|
/**
|
|
* @param string $object_id UUID
|
|
* @return bool
|
|
* @throws DBError
|
|
*/
|
|
function changeObject( $object_id ) {
|
|
$this->getObjectById( $object_id );
|
|
|
|
$ph = [
|
|
'object_id' => TTUUID::castUUID( $object_id ),
|
|
'session_id' => $this->encryptSessionID( $this->getSessionID() ),
|
|
];
|
|
|
|
try {
|
|
$query = 'UPDATE authentication SET object_id = ? WHERE session_id = ?';
|
|
$this->db->Execute( $query, $ph );
|
|
$this->cache->remove( $ph['session_id'], 'authentication' );
|
|
} catch ( Exception $e ) {
|
|
throw new DBError( $e );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param string $id UUID
|
|
* @return bool
|
|
*/
|
|
function getObjectByID( $id ) {
|
|
if ( empty( $id ) ) {
|
|
return false;
|
|
}
|
|
|
|
if ( $this->isUser() ) {
|
|
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
|
|
$ulf->getByID( $id );
|
|
if ( $ulf->getRecordCount() == 1 ) {
|
|
$retval = $ulf->getCurrent();
|
|
}
|
|
}
|
|
|
|
if ( $this->getType() === 100 ) {
|
|
$jalf = TTnew( 'JobApplicantListFactory' ); /** @var JobApplicantListFactory $jalf */
|
|
$jalf->getByID( $id );
|
|
if ( $jalf->getRecordCount() == 1 ) {
|
|
$retval = $jalf->getCurrent();
|
|
}
|
|
}
|
|
|
|
if ( isset( $retval ) && is_object( $retval ) ) {
|
|
return $retval;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return bool|null
|
|
*/
|
|
function getObject() {
|
|
if ( is_object( $this->obj ) ) {
|
|
return $this->obj;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param $object
|
|
* @return bool
|
|
*/
|
|
function setObject( $object ) {
|
|
if ( is_object( $object ) ) {
|
|
$this->obj = $object;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return null
|
|
*/
|
|
function getObjectID() {
|
|
return $this->object_id;
|
|
}
|
|
|
|
/**
|
|
* @param string $id UUID
|
|
* @return bool
|
|
*/
|
|
function setObjectID( $id ) {
|
|
$id = TTUUID::castUUID( $id );
|
|
if ( $id != '' ) {
|
|
$this->object_id = $id;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return mixed
|
|
*/
|
|
function getSecureSessionID() {
|
|
return substr_replace( $this->getSessionID(), '...', 7, (int)( strlen( $this->getSessionID() ) - 11 ) );
|
|
}
|
|
|
|
/**
|
|
* #2238 - Encrypt SessionID with private SALT before writing/reading SessionID in database.
|
|
* This adds an additional protection layer against session stealing if a SQL injection attack is ever discovered.
|
|
* It prevents someone from being able to enumerate over the SessionIDs in the table and use them for nafarious purposes.
|
|
* @param string $session_id UUID
|
|
* @return string
|
|
*/
|
|
function encryptSessionID( $session_id ) {
|
|
$retval = sha1( $session_id . TTPassword::getPasswordSalt() );
|
|
|
|
return $retval;
|
|
}
|
|
|
|
/**
|
|
* @return string|null
|
|
*/
|
|
function getSessionID() {
|
|
return $this->session_id;
|
|
}
|
|
|
|
/**
|
|
* @param string $session_id UUID
|
|
* @return bool
|
|
*/
|
|
function setSessionID( $session_id ) {
|
|
$validator = new Validator;
|
|
$session_id = $validator->stripNonAlphaNumeric( $session_id );
|
|
|
|
if ( !empty( $session_id ) ) {
|
|
$this->session_id = $session_id;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
function genSessionID() {
|
|
return sha1( Misc::getUniqueID() );
|
|
}
|
|
|
|
/**
|
|
* @param bool $type_id
|
|
* @return bool
|
|
*/
|
|
private function setCookie( $type_id = false, $company_obj = null ) {
|
|
if ( $this->getSessionID() != '' ) {
|
|
$cookie_expires = ( time() + 7776000 ); //90 Days
|
|
if ( $this->getEnableExpireSession() === true ) {
|
|
$cookie_expires = 0; //Expire when browser closes.
|
|
}
|
|
Debug::text( 'Cookie Expires: ' . $cookie_expires . ' Path: ' . Environment::getCookieBaseURL(), __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
//15-Jun-2016: This should be not be needed anymore as it has been around for several years now.
|
|
//setcookie( $this->getName(), NULL, ( time() + 9999999 ), Environment::getBaseURL(), NULL, Misc::isSSL( TRUE ) ); //Delete old directory cookie as it can cause a conflict if it stills exists.
|
|
|
|
//Upon successful login to a cloud hosted server, set the URL to a cookie that can be read from the upper domain to help get the user back to the proper login URL later.
|
|
if ( DEPLOYMENT_ON_DEMAND == true && DEMO_MODE == false ) {
|
|
setcookie( 'LoginURL', Misc::getURLProtocol() . '://' . Misc::getHostName() . Environment::getBaseURL(), ( time() + 9999999 ), '/', '.' . Misc::getHostNameWithoutSubDomain( Misc::getHostName( false ) ), false ); //Delete old directory cookie as it can cause a conflict if it stills exists.
|
|
}
|
|
|
|
//Set cookie in root directory so other interfaces can access it.
|
|
setcookie( $this->getName(), $this->getSessionID(), $cookie_expires, Environment::getCookieBaseURL(), '', Misc::isSSL( true ) );
|
|
if ( is_object( $company_obj ) ) {
|
|
setcookie( 'ShortName', $company_obj->getShortName(), ( time() + 9999999 ), '/', '.' . Misc::getHostNameWithoutSubDomain( Misc::getHostName( false ) ), false ); //Delete old directory cookie as it can cause a conflict if it stills exists.
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
private function destroyCookie() {
|
|
setcookie( $this->getName(), '', ( time() + 9999999 ), Environment::getCookieBaseURL(), '', Misc::isSSL( true ) );
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param $object_id
|
|
* @return bool
|
|
* @throws DBError
|
|
*/
|
|
function UpdateLastLoginDate( $object_id = null ) {
|
|
$ph = [
|
|
'last_login_date' => time(),
|
|
'object_id' => TTUUID::castUUID( $object_id ?? $this->getObjectID() ),
|
|
];
|
|
|
|
$query = 'UPDATE users SET last_login_date = ? WHERE id = ?';
|
|
|
|
try {
|
|
$this->db->Execute( $query, $ph );
|
|
} catch ( Exception $e ) {
|
|
throw new DBError( $e );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
public function Update() {
|
|
try {
|
|
$ph = [
|
|
'updated_date' => time(),
|
|
'reauthenticated_date' => $this->getReauthenticatedDate(),
|
|
'type_id' => $this->getType(),
|
|
'other_json' => json_encode( $this->getOtherJSON() ),
|
|
'session_id' => $this->encryptSessionID( $this->getSessionID() ),
|
|
];
|
|
|
|
Debug::Arr( $ph, 'Updating Session Data for Session ID: '. $this->getSessionID() .' Encrypted Session ID: '. $this->encryptSessionID( $this->getSessionID() ), __FILE__, __LINE__, __METHOD__, 10 );
|
|
$query = 'UPDATE authentication SET updated_date = ?, reauthenticated_date = ?, type_id = ?, other_json = ? WHERE session_id = ?';
|
|
$this->db->Execute( $query, $ph ); //This can cause SQL error: "could not serialize access due to concurrent update" when in READ COMMITTED mode.
|
|
|
|
$clear_cache = function() {
|
|
if ( get_class( $this->cache ) == 'Redis_Cache_Lite' ) {
|
|
Debug::Text( ' Clearing cached session data...', __FILE__, __LINE__, __METHOD__, 10 );
|
|
$this->cache->_unlink( $this->cache->_file, true ); //This removes the cache from all read-only (slave) servers, so during MFA there isn't stale cache sitting on a different server causing race conditions and random MFA failures.
|
|
}
|
|
};
|
|
|
|
$cached_session = $this->cache->get( $ph['session_id'], 'authentication' );
|
|
if ( is_array( $cached_session ) ) {
|
|
Debug::Text( ' Updating cached session data...', __FILE__, __LINE__, __METHOD__, 10 );
|
|
$cached_session['updated_date'] = $ph['updated_date'];
|
|
$cached_session['reauthenticated_date'] = $ph['reauthenticated_date'];
|
|
$cached_session['type_id'] = $ph['type_id'];
|
|
$cached_session['other_json'] = $ph['other_json'];
|
|
|
|
$clear_cache();
|
|
$this->cache->save( $cached_session, $ph['session_id'], 'authentication' );
|
|
} else {
|
|
//Make sure we clear the cache on all *other* servers, even if the current session data is not cached on our current server.
|
|
$clear_cache();
|
|
}
|
|
unset( $cached_session );
|
|
} catch ( Exception $e ) {
|
|
//Ignore any serialization errors, as its not a big deal anyways.
|
|
Debug::text( 'WARNING: SQL query failed, likely due to transaction isolotion: ' . $e->getMessage(), __FILE__, __LINE__, __METHOD__, 10 );
|
|
//throw new DBError($e);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
* @throws DBError
|
|
*/
|
|
private function Delete() {
|
|
try {
|
|
$ph = [
|
|
'session_id' => $this->encryptSessionID( $this->getSessionID() ),
|
|
];
|
|
|
|
$query = 'DELETE FROM authentication WHERE session_id = ? OR (' . time() . ' - updated_date) > idle_timeout';
|
|
$this->db->Execute( $query, $ph );
|
|
$this->cache->remove( $ph['session_id'], 'authentication' );
|
|
} catch ( Exception $e ) {
|
|
throw new DBError( $e );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
* @throws DBError
|
|
*/
|
|
private function Write() {
|
|
$ph = [
|
|
'session_id' => $this->encryptSessionID( $this->getSessionID() ),
|
|
'type_id' => (int)$this->getType(),
|
|
'object_id' => TTUUID::castUUID( $this->getObjectID() ),
|
|
'ip_address' => $this->getIPAddress(),
|
|
'idle_timeout' => $this->getIdleTimeout(),
|
|
'end_point_id' => $this->getEndPointID(),
|
|
'client_id' => $this->getClientID(),
|
|
'user_agent' => $this->getUserAgent(),
|
|
'created_date' => $this->getCreatedDate(),
|
|
'updated_date' => $this->getUpdatedDate(),
|
|
'reauthenticated_date' => $this->getReauthenticatedDate(),
|
|
'other_json' => json_encode( $this->getOtherJSON() ),
|
|
];
|
|
|
|
try {
|
|
$query = 'INSERT INTO authentication (session_id, type_id, object_id, ip_address, idle_timeout, end_point_id, client_id, user_agent, created_date, updated_date, reauthenticated_date, other_json ) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )';
|
|
$this->db->Execute( $query, $ph );
|
|
$this->cache->save( $ph, $ph['session_id'], 'authentication' );
|
|
} catch ( Exception $e ) {
|
|
throw new DBError( $e );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Authentication during API calls does not log in users for pending (0 type_id) sessions and the mobile app will be logged into a different session than the one being authorized.
|
|
* Therefore, to utilize Read() and Update() we need to populate $this with the correct session_id data related to the session being authenticated and not necessarily the one that is currently logged in.
|
|
* @param $session_id
|
|
* @return bool
|
|
*/
|
|
public function setAndReadForMultiFactor( $session_id = null ) {
|
|
global $config_vars, $db;
|
|
|
|
//Check if we are using load balancing or not. If we are force the main $db connection to just the master/write server, otherwise we could be reading old data if the replication is delayed.
|
|
//Every SQL call after this will go to the master server.
|
|
// The 2nd time this is called, it would no longer be a ADODBLoadBalancer object, so we need to make sure getConnection method always exists first.
|
|
if ( strpos( $config_vars['database']['host'], ',' ) !== false && method_exists( $db, 'getConnection') == true ) {
|
|
$db = $db->getConnection( 'write' );
|
|
}
|
|
|
|
//Session ID and EndPoint must set so $this->read() knows which data to read. setObjectFromArray() sets the same data again though, but they should match anyways.
|
|
$this->setSessionID( $session_id ?? $this->getCurrentSessionID( 810 ) );
|
|
$this->setEndPointID( 'json/api' );
|
|
|
|
//When the app is authenticating a multifactor request, the session being authenticated is different from the session the app is logged into.
|
|
//Because of this we do not know what client_id it is being authenticated and need to check all the options.
|
|
//If the app is authenticating a browser session, the app only knows the session_id it is trying to authenticate and not the other data.
|
|
$result = $this->Read( [ 0, 800, 810 ], [ 'browser-timetrex', 'app-timetrex', 'app-timetrex-kiosk' ], true );
|
|
if ( is_array( $result ) == true ) {
|
|
return $this->setObjectFromArray( $result );
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param $result
|
|
* @return bool
|
|
*/
|
|
private function setObjectFromArray( $result ) {
|
|
$this->setType( $result['type_id'] );
|
|
$this->setIdleTimeout( $result['idle_timeout'] );
|
|
$this->setEndPointID( $result['end_point_id'] );
|
|
$this->setClientID( $result['client_id'] );
|
|
$this->setUserAgent( $result['user_agent'] );
|
|
$this->setSessionID( $this->getSessionID() ); //Make sure this is *not* the encrypted session_id
|
|
$this->setIPAddress( $result['ip_address'] );
|
|
$this->setCreatedDate( $result['created_date'] );
|
|
$this->setUpdatedDate( $result['updated_date'] );
|
|
$this->setReauthenticatedDate( $result['reauthenticated_date'] ?? null );
|
|
$this->setObjectID( $result['object_id'] );
|
|
$this->setOtherJSON( json_decode( ( $result['other_json'] ?? '[]' ), true ) );
|
|
if ( $this->setObject( $this->getObjectById( $this->getObjectID() ) ) ) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param $type array
|
|
* @param $client array|null
|
|
* @return bool
|
|
*/
|
|
private function Read( $type, $client = null, $skip_cache = false ) {
|
|
$ph = [
|
|
'session_id' => $this->encryptSessionID( $this->getSessionID() ),
|
|
'end_point_id' => $this->getEndPointID(),
|
|
'updated_date' => time(),
|
|
];
|
|
|
|
if ( $client === null ) {
|
|
$client = [ $this->getClientID() ];
|
|
}
|
|
|
|
$result = false;
|
|
|
|
//By using caching here, we don't actually save that much time overall,
|
|
// but in cases where SQL calls are otherwise never executed during a request (ie: isLoggedIn(), ping() ), we can save a connection to the database.
|
|
if ( $skip_cache == false && is_object( $this->cache ) ) {
|
|
$result = $this->cache->get( $ph['session_id'], 'authentication' );
|
|
if ( is_array( $result ) && count( $result ) > 1 ) {
|
|
//Check to make sure cache result matches what we expect.
|
|
if ( in_array( $result['type_id'], $type ) == true && in_array( $result['client_id'], $client ) == true && $result['end_point_id'] == $ph['end_point_id'] && $result['updated_date'] >= ( $ph['updated_date'] - $result['idle_timeout'] ) ) {
|
|
Debug::text( ' Using cached authentication record...', __FILE__, __LINE__, __METHOD__, 10 );
|
|
} else {
|
|
Debug::Arr( [ $ph, $result ], ' Cached authentication record does not match filter, falling back to SQL! Cache ID: ' . $ph['session_id'], __FILE__, __LINE__, __METHOD__, 10 );
|
|
$result = false;
|
|
}
|
|
} elseif ( is_object( $result ) && get_class( $result ) == 'PEAR_Error' ) {
|
|
Debug::Arr( $result, 'WARNING: Unable to read cache file, likely due to permissions or locking! Cache ID: ' . $ph['session_id'] . ' File: ' . $this->cache->_file, __FILE__, __LINE__, __METHOD__, 10 );
|
|
}
|
|
}
|
|
|
|
if ( empty( $result ) ) {
|
|
//Need to handle IP addresses changing during the session.
|
|
//When using SSL, don't check for IP address changing at all as we use secure cookies.
|
|
//When *not* using SSL, always require the same IP address for the session.
|
|
//However we need to still allow multiple sessions for the same user, using different IPs.
|
|
|
|
$type_ph = [];
|
|
$client_ph = [];
|
|
foreach ( $type as $type_id ) {
|
|
$ph[] = $type_id;
|
|
$type_ph[] = '?';
|
|
}
|
|
|
|
foreach ( $client as $client_id ) {
|
|
$ph[] = $client_id;
|
|
$client_ph[] = '?';
|
|
}
|
|
|
|
$query = 'SELECT type_id, session_id, object_id, ip_address, idle_timeout, end_point_id, client_id, user_agent, created_date, updated_date, reauthenticated_date, other_json FROM authentication WHERE session_id = ? AND end_point_id = ? AND updated_date >= ( ? - idle_timeout ) AND type_id in (' . implode( ',', $type_ph ) . ') AND client_id in (' . implode( ',', $client_ph ) . ')';
|
|
$result = $this->db->GetRow( $query, $ph );
|
|
//Debug::Query($query, $ph, __FILE__, __LINE__, __METHOD__, 10);
|
|
if ( $skip_cache == false && !empty( $result ) ) {
|
|
Debug::Text( ' Caching Session Data: '. $ph['session_id'], __FILE__, __LINE__, __METHOD__, 10 );
|
|
$this->cache->save( $result, $ph['session_id'], 'authentication' );
|
|
}
|
|
}
|
|
|
|
if ( count( $result ) > 0 ) {
|
|
return $result;
|
|
} else {
|
|
Debug::text( 'Session ID not found in the DB... End Point: ' . $this->getEndPointID() . ' Client ID: ' . $this->getClientID() . ' Type: ' . implode( ',', $type ), __FILE__, __LINE__, __METHOD__, 10 );
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param $user_name
|
|
* @param $password
|
|
* @param $type
|
|
* @param $enable_cookie
|
|
* @param $reauthenticate_only
|
|
* @param $mfa_type_id
|
|
* @return bool
|
|
* @throws DBError
|
|
*/
|
|
function Login( $user_name, $password, $type = 'USER_NAME', $enable_cookie = true, $reauthenticate_only = false, $mfa_type_id = 0 ) {
|
|
//DO NOT lowercase username, because iButton values are case sensitive.
|
|
$user_name = html_entity_decode( trim( $user_name ) );
|
|
$password = html_entity_decode( trim( $password ) );
|
|
|
|
//Checks user_name/password. However password is blank for iButton/Fingerprints often so we can't check that.
|
|
if ( $user_name == '' ) {
|
|
return false;
|
|
}
|
|
|
|
$this->setMFATypeId( $mfa_type_id );
|
|
|
|
$type = strtolower( $type );
|
|
Debug::text( 'Login Type: ' . $type, __FILE__, __LINE__, __METHOD__, 10 );
|
|
try {
|
|
//Prevent brute force attacks by IP address.
|
|
//Allowed up to 20 attempts in a 30 min period.
|
|
if ( $this->getRateLimitObject()->check() == false ) {
|
|
Debug::Text( 'Excessive failed password attempts... Preventing login from: ' . Misc::getRemoteIPAddress() . ' for up to 15 minutes...', __FILE__, __LINE__, __METHOD__, 10 );
|
|
sleep( 5 ); //Excessive password attempts, sleep longer.
|
|
|
|
return false;
|
|
}
|
|
|
|
$uf = new UserFactory();
|
|
if ( preg_match( $uf->username_validator_regex, $user_name ) === 0 ) { //This helps prevent invalid byte sequences on unicode strings.
|
|
Debug::Text( 'Username doesnt match regex: ' . $user_name, __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
return false; //No company by that user name.
|
|
}
|
|
unset( $uf );
|
|
|
|
$bypass_multi_factor = false;
|
|
|
|
switch ( $type ) {
|
|
case 'user_name_multi_factor':
|
|
case 'user_name':
|
|
$company_obj = $this->getCompanyObject( $user_name, 'USER' );
|
|
|
|
if ( $password == '' ) {
|
|
return false;
|
|
}
|
|
|
|
if ( $this->checkCompanyStatus( $user_name ) == 10 ) { //Active
|
|
//Lowercase regular user_names here only.
|
|
$password_result = $this->checkPassword( $user_name, $password );
|
|
} else {
|
|
$password_result = false; //No company by that user name.
|
|
}
|
|
|
|
if ( $password_result === true ) {
|
|
if ( $type === 'user_name_multi_factor' ) {
|
|
|
|
//reauthenticate_only always does full multifactor authentication process and ignores trusted device.
|
|
if ( $reauthenticate_only == true ) {
|
|
$type = 'pending_authentication'; //Set type to 0 until multifactor authentication is verified.
|
|
$this->setMfaStartListen( true ); //Reauthenticate always starte and listens for multifactor authentication.
|
|
} else {
|
|
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
|
|
$ulf->getByUserNameAndCompanyId( $user_name, $company_obj->getId() );
|
|
if ( $ulf->getRecordCount() == 1 ) {
|
|
$user_obj = $ulf->getCurrent();
|
|
if ( $this->isUserAllowedBypassMFA( $user_obj->getId(), $_COOKIE['TrustedDevice'] ?? '' ) ) {
|
|
Debug::Text( 'Bypassing MFA for user: ' . $user_name, __FILE__, __LINE__, __METHOD__, 10 );
|
|
$bypass_multi_factor = true;
|
|
$this->setMfaStartListen( false );
|
|
} else {
|
|
Debug::Text( 'NOT Bypassing MFA for user: ' . $user_name, __FILE__, __LINE__, __METHOD__, 10 );
|
|
$type = 'pending_authentication'; //Set type to 0 until multifactor authentication is verified.
|
|
$this->setMfaStartListen( true );
|
|
}
|
|
} else {
|
|
Debug::Text( 'Cannot find user, NOT Bypassing MFA for user: ' . $user_name, __FILE__, __LINE__, __METHOD__, 10 );
|
|
$type = 'pending_authentication'; //Set type to 0 until multifactor authentication is verified.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 'phone_id': //QuickPunch ID/Password
|
|
case 'quick_punch_id':
|
|
$company_obj = $this->getCompanyObject( $user_name, 'QUICK_PUNCH' );
|
|
if ( is_object( $company_obj ) && getTTProductEdition() > 10 && $company_obj->getProductEdition() > 10 ) {
|
|
$password_result = $this->checkPhonePassword( $user_name, $password );
|
|
} else {
|
|
Debug::text( 'ERROR: Company not found or not active...', __FILE__, __LINE__, __METHOD__, 10 );
|
|
$password_result = false; //No company by that quick punch ID
|
|
}
|
|
unset( $company_obj );
|
|
break;
|
|
case 'ibutton':
|
|
$password_result = $this->checkIButton( $user_name );
|
|
break;
|
|
case 'barcode':
|
|
$password_result = $this->checkBarcode( $user_name, $password );
|
|
break;
|
|
case 'finger_print':
|
|
$password_result = $this->checkFingerPrint( $user_name );
|
|
break;
|
|
case 'client_pc':
|
|
//This is for client application persistent connections, use:
|
|
//Login Type: client_pc
|
|
//Station Type: PC
|
|
|
|
$password_result = false;
|
|
|
|
//StationID must be set on the URL
|
|
if ( isset( $_GET['StationID'] ) && $_GET['StationID'] != '' ) {
|
|
$slf = new StationListFactory();
|
|
$slf->getByStationID( $_GET['StationID'] );
|
|
if ( $slf->getRecordCount() == 1 ) {
|
|
$station_obj = $slf->getCurrent();
|
|
if ( $station_obj->getStatus() == 20 ) { //Enabled
|
|
$uilf = new UserIdentificationListFactory();
|
|
$uilf->getByCompanyIdAndTypeId( $station_obj->getCompany(), [ 1 ] ); //1=Employee Sequence number.
|
|
if ( $uilf->getRecordCount() > 0 ) {
|
|
foreach ( $uilf as $ui_obj ) {
|
|
if ( (int)$ui_obj->getValue() == (int)$user_name ) {
|
|
//$password_result = $this->checkClientPC( $user_name );
|
|
$password_result = $this->checkBarcode( $ui_obj->getUser(), $password );
|
|
}
|
|
}
|
|
} else {
|
|
Debug::text( 'UserIdentification match failed: ' . $user_name, __FILE__, __LINE__, __METHOD__, 10 );
|
|
}
|
|
} else {
|
|
Debug::text( 'Station is DISABLED... UUID: ' . $station_obj->getId(), __FILE__, __LINE__, __METHOD__, 10 );
|
|
}
|
|
} else {
|
|
Debug::text( 'StationID not specifed on URL or not found...', __FILE__, __LINE__, __METHOD__, 10 );
|
|
}
|
|
}
|
|
break;
|
|
case 'http_auth':
|
|
if ( $this->checkCompanyStatus( $user_name ) == 10 ) { //Active
|
|
//Lowercase regular user_names here only.
|
|
$password_result = $this->checkUsername( $user_name );
|
|
} else {
|
|
$password_result = false; //No company by that user name.
|
|
}
|
|
break;
|
|
case 'job_applicant':
|
|
$company_obj = $this->getCompanyObject( $user_name, 'JOB_APPLICANT' );
|
|
if ( is_object( $company_obj ) && $company_obj->getProductEdition() == 25 && $company_obj->getStatus() == 10 ) { //Active
|
|
$password_result = $this->checkApplicantPassword( $user_name, $password );
|
|
} else {
|
|
Debug::text( 'ERROR: Company not found or not active...', __FILE__, __LINE__, __METHOD__, 10 );
|
|
$password_result = false; //No company by that user name.
|
|
}
|
|
unset( $company_obj );
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
if ( $password_result === true ) {
|
|
if ( $reauthenticate_only === true ) {
|
|
//We are only reauthenticating and do not want to save this as a new session.
|
|
return true;
|
|
}
|
|
|
|
$this->setType( $type );
|
|
$this->setSessionID( $this->genSessionID() );
|
|
$this->setIPAddress();
|
|
|
|
if ( $this->getClientIDHeader() == 'App-TimeTrex' && Misc::getMobileAppClientVersion() != '' && version_compare( Misc::getMobileAppClientVersion(), '5.0.0', '>=' ) ) {
|
|
$this->setIdleTimeout( ( 60 * 86400 ) ); //Login from Mobile app, use longer (60 day) idle timeouts so they don't have to login so often.
|
|
} else if ( $this->getClientIDHeader() == 'App-TimeTrex-Kiosk' ) {
|
|
$this->setIdleTimeout( ( 15 * 60 ) ); //Login from Mobile app in kiosk mode, use 15 min session timeout.
|
|
}
|
|
|
|
$this->setCreatedDate();
|
|
$this->setUpdatedDate();
|
|
|
|
//Sets session cookie.
|
|
if ( $enable_cookie !== false ) {
|
|
$this->setCookie( false, $company_obj ?? null );
|
|
} else {
|
|
Debug::text( ' Not setting session cookie...', __FILE__, __LINE__, __METHOD__, 10 );
|
|
}
|
|
|
|
//Write data to db.
|
|
$this->Write();
|
|
|
|
Debug::text( 'Login Succesful for User Name: ' . $user_name . ' End Point ID: ' . $this->getEndPointID() . ' Client ID: ' . $this->getClientID() . ' Type: ' . $type . ' Session ID: Cookie: ' . $this->getSessionID() . ' DB: ' . $this->encryptSessionID( $this->getSessionID() ), __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
//Only update last_login_date when using user_name to login to the web interface.
|
|
if ( $type == 'user_name' || ( $type == 'user_name_multi_factor' && $bypass_multi_factor == true ) ) {
|
|
$this->UpdateLastLoginDate();
|
|
}
|
|
|
|
if ( $this->isUser() == true ) {
|
|
//Truncate SessionID for security reasons, so someone with access to the audit log can't steal sessions.
|
|
if ( $type == 'pending_authentication' ) { //Pending authentication has not completed the full login process yet, so don't add a "login" audit log entry until login is fully successfull.
|
|
TTLog::addEntry( $this->getObjectID(), 98, TTi18n::getText( 'From' ) . ': ' . Misc::getLocationOfIPAddress( Misc::getRemoteIPAddress() ) . ' (' . Misc::getRemoteIPAddress() . ') ' . TTi18n::getText( 'Type' ) . ': ' . $type . ' ' . TTi18n::getText( 'SessionID' ) . ': ' . $this->getSecureSessionID() . ' ' . TTi18n::getText( 'Client' ) . ': ' . $this->getClientID() . ' ' . TTi18n::getText( 'End Point' ) . ': ' . $this->getEndPointID() . ' ' . TTi18n::getText( 'ObjectID' ) . ': ' . $this->getObjectID(), $this->getObjectID(), 'authentication' ); //Login
|
|
} else {
|
|
TTLog::addEntry( $this->getObjectID(), 100, TTi18n::getText( 'From' ) . ': ' . Misc::getLocationOfIPAddress( Misc::getRemoteIPAddress() ) . ' (' . Misc::getRemoteIPAddress() . ') ' . TTi18n::getText( 'Type' ) . ': ' . $type . ' ' . TTi18n::getText( 'SessionID' ) . ': ' . $this->getSecureSessionID() . ' ' . TTi18n::getText( 'Client' ) . ': ' . $this->getClientID() . ' ' . TTi18n::getText( 'End Point' ) . ': ' . $this->getEndPointID() . ' ' . TTi18n::getText( 'ObjectID' ) . ': ' . $this->getObjectID(), $this->getObjectID(), 'authentication' ); //Login
|
|
}
|
|
}
|
|
|
|
$this->getRateLimitObject()->delete(); //Clear failed password rate limit upon successful login.
|
|
|
|
return true;
|
|
}
|
|
|
|
Debug::text( 'Login Failed! Attempt: ' . $this->getRateLimitObject()->getAttempts(), __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
sleep( ceil( $this->getRateLimitObject()->getAttempts() * 0.5 ) ); //If password is incorrect, sleep for some time to slow down brute force attacks.
|
|
} catch ( Exception $e ) {
|
|
//Database not initialized, or some error, redirect to Install page.
|
|
throw new DBError( $e, 'DBInitialize' );
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
function Logout() {
|
|
$this->destroyCookie();
|
|
$this->Delete();
|
|
|
|
if ( $this->isUser() == true ) {
|
|
TTLog::addEntry( $this->getObjectID(), 110, TTi18n::getText( 'From' ) . ': ' . Misc::getLocationOfIPAddress( Misc::getRemoteIPAddress() ) . ' (' . Misc::getRemoteIPAddress() . ') ' . TTi18n::getText( 'SessionID' ) . ': ' . $this->getSecureSessionID() . ' ' . TTi18n::getText( 'ObjectID' ) . ': ' . $this->getObjectID(), $this->getObjectID(), 'authentication' );
|
|
}
|
|
|
|
global $current_user, $current_company;
|
|
$current_user = null; //This helps subsequent functions from thinking we are still logged in. Unset does not work on global variables.
|
|
$current_company = null;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Gets the current session ID from the COOKIE, POST or GET variables.
|
|
* @param string $type
|
|
* @return string|bool
|
|
*/
|
|
function getCurrentSessionID( $type ) {
|
|
$session_name = $this->getName( $type );
|
|
|
|
if ( isset( $_COOKIE[$session_name] ) && $_COOKIE[$session_name] != '' ) {
|
|
$session_id = (string)$_COOKIE[$session_name];
|
|
} else if ( isset( $_SERVER[$session_name] ) && $_SERVER[$session_name] != '' ) {
|
|
$session_id = (string)$_SERVER[$session_name];
|
|
} else if ( isset( $_POST[$session_name] ) && $_POST[$session_name] != '' ) {
|
|
$session_id = (string)$_POST[$session_name];
|
|
} else if ( isset( $_GET[$session_name] ) && $_GET[$session_name] != '' ) {
|
|
$session_id = (string)$_GET[$session_name];
|
|
} else {
|
|
$session_id = false;
|
|
}
|
|
|
|
Debug::text( 'Session ID: ' . $session_id . ' Encrypted Session ID: '. $this->encryptSessionID( $session_id ) .' IP Address: ' . Misc::getRemoteIPAddress() . ' URL: ' . $_SERVER['REQUEST_URI'], __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
return $session_id;
|
|
}
|
|
|
|
/**
|
|
* @param $session_id
|
|
* @return bool
|
|
*/
|
|
function isSessionIDAPIKey( $session_id ) {
|
|
if ( $session_id != '' && substr( $session_id, 0, 3 ) == 'API' ) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return array|false
|
|
*/
|
|
function getMultiFactorData() {
|
|
$other_json = $this->getOtherJSON();
|
|
|
|
//If key or login_approved exists then we know that the MFA data has not been fully used yet.
|
|
if ( isset( $other_json['mfa']['key'] ) == false && isset( $other_json['mfa']['login_approved'] ) == false ) {
|
|
Debug::Text( 'ERROR: Cannot retrieve MFA key or login_approved flag, MFA data may have already been consumed.', __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
return false;
|
|
}
|
|
|
|
$key = $other_json['mfa']['key'] ?? false; //Secret key used to encrypt multifactor key (This may be consumed and no longer exist)
|
|
|
|
return [
|
|
'type_id' => $this->getType(),
|
|
'user_id' => $this->getObjectID(),
|
|
'session_id' => $this->getSessionID(),
|
|
'key' => $key,
|
|
'reauthenticated_date' => $this->getReauthenticatedDate(),
|
|
'expected_key' => $key ? sha1( $this->getSessionID() . $this->getObjectID() . $key ) : false,
|
|
'time' => $other_json['mfa']['time'] ?? false,
|
|
'is_reauthentication_request' => $other_json['mfa']['is_reauthentication_request'] ?? false,
|
|
'one_time_auth' => $other_json['mfa']['one_time_auth'] ?? false,
|
|
'login_approved' => $other_json['mfa']['login_approved'] ?? false,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param $is_reauthentication bool
|
|
* @return bool
|
|
*/
|
|
function generateMultiFactorAuthKey( $is_reauthentication ) {
|
|
$secret_key = $this->genSessionID();
|
|
|
|
$other_json = array_merge( $this->getOtherJSON(), [ 'mfa' => [
|
|
'time' => TTDate::getTime(),
|
|
'key' => $secret_key,
|
|
'is_reauthentication_request' => $is_reauthentication,
|
|
] ] );
|
|
|
|
$this->setOtherJSON( $other_json );
|
|
|
|
Debug::Text( 'Generating MFA key for user: ' . $this->getObjectID(), __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
$this->Update();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param string $session_id UUID
|
|
* @param string|array $type
|
|
* @param bool $touch_updated_date
|
|
* @return bool
|
|
* @throws DBError
|
|
*/
|
|
function Check( $session_id = null, $type = null, $touch_updated_date = true ) {
|
|
global $profiler;
|
|
$profiler->startTimer( "Authentication::Check()" );
|
|
|
|
if ( $type == '' ) {
|
|
$type = [ 'USER_NAME', 'USER_NAME_MULTI_FACTOR' ];
|
|
}
|
|
|
|
if ( is_array( $type ) == false ) {
|
|
$type = [ $type ];
|
|
}
|
|
|
|
//Support session_ids passed by cookie, post, and get.
|
|
if ( $session_id == '' ) {
|
|
$session_id = $this->getCurrentSessionID( $type[0] );
|
|
}
|
|
|
|
Debug::text( 'Session ID: ' . $session_id . ' Type: ' . implode( ',', $type ) . ' IP Address: ' . Misc::getRemoteIPAddress() . ' URL: ' . $_SERVER['REQUEST_URI'] . ' Touch Updated Date: ' . (int)$touch_updated_date, __FILE__, __LINE__, __METHOD__, 10 );
|
|
//Checks session cookie, returns object_id;
|
|
if ( isset( $session_id ) ) {
|
|
/*
|
|
Bind session ID to IP address to aid in preventing session ID theft,
|
|
if this starts to cause problems
|
|
for users behind load balancing proxies, allow them to choose to
|
|
bind session IDs to just the first 1-3 quads of their IP address
|
|
as well as the SHA1 of their user-agent string.
|
|
Could also use "behind proxy IP address" if one is supplied.
|
|
*/
|
|
try {
|
|
$this->setSessionID( $session_id );
|
|
$this->setIPAddress();
|
|
|
|
foreach ( $type as $key => $type_id ) {
|
|
if ( is_numeric( $type_id ) == false ) {
|
|
$type[$key] = $this->getTypeIDByName( $type_id );
|
|
}
|
|
}
|
|
|
|
$result = $this->Read( $type );
|
|
|
|
if ( is_array( $result ) === true ) {
|
|
if ( PRODUCTION == true && $result['ip_address'] != $this->getIPAddress() ) {
|
|
Debug::text( 'NOTICE: IP Address has changed for existing session... Original IP: ' . $result['ip_address'] . ' Current IP: ' . $this->getIPAddress() . ' isSSL: ' . (int)Misc::isSSL( true ), __FILE__, __LINE__, __METHOD__, 10 );
|
|
//When using SSL, we don't care if the IP address has changed, as the session should still be secure.
|
|
//This allows sessions to work across load balancing routers, or between mobile/wifi connections, which can change 100% of the IP address (so close matches are useless anyways)
|
|
if ( Misc::isSSL( true ) != true ) {
|
|
//When not using SSL there is no 100% method of preventing session hijacking, so just insist that IP addresses match exactly as its as close as we can get.
|
|
Debug::text( 'Not using SSL, IP addresses must match exactly...', __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
//Only check user agent if we know its a web-browser, and definitely not when its an API or Mobile App, as the user agent may change between SOAP/REST libraries or App versions.
|
|
if ( PRODUCTION == true && $result['client_id'] == 'browser-timetrex' && $result['user_agent'] != $this->getUserAgent() ) {
|
|
Debug::text( 'WARNING: User Agent changed! Original: ' . $result['user_agent'] . ' Current: ' . $this->getUserAgent(), __FILE__, __LINE__, __METHOD__, 10 );
|
|
return false;
|
|
}
|
|
|
|
if( $this->setObjectFromArray( $result ) === true ) {
|
|
//touch UpdatedDate in most cases, however when calling PING() we don't want to do this.
|
|
if ( $touch_updated_date !== false ) {
|
|
//Reduce contention and traffic on the session table by only touching the updated_date every 120 +/- rand( 0, 60 ) seconds.
|
|
//Especially helpful for things like the dashboard that trigger many async calls.
|
|
if ( ( time() - $this->getUpdatedDate() ) > ( 120 + rand( 0, 60 ) ) ) {
|
|
Debug::text( ' Touching updated date due to more than 120s...', __FILE__, __LINE__, __METHOD__, 10 );
|
|
$this->Update();
|
|
}
|
|
}
|
|
|
|
$profiler->stopTimer( "Authentication::Check()" );
|
|
|
|
return true;
|
|
}
|
|
}
|
|
} catch ( Exception $e ) {
|
|
//Database not initialized, or some error, redirect to Install page.
|
|
throw new DBError( $e, 'DBInitialize' );
|
|
}
|
|
}
|
|
|
|
$profiler->stopTimer( "Authentication::Check()" );
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* When company status changes, logout all users for the company.
|
|
* @param string $company_id UUID
|
|
* @return bool
|
|
* @throws DBError
|
|
*/
|
|
function logoutCompany( $company_id, $client_id = null ) {
|
|
$ph = [
|
|
'company_id' => TTUUID::castUUID( $company_id ),
|
|
'type_id' => (int)$this->getTypeIDByName( 'USER_NAME' ),
|
|
];
|
|
|
|
try {
|
|
Debug::text( 'Logging out entire company ID: ' . $company_id .' Client ID: '. $client_id, __FILE__, __LINE__, __METHOD__, 10 );
|
|
$query = 'DELETE FROM authentication as a USING users as b WHERE a.object_id = b.id AND b.company_id = ? AND a.type_id = ? ';
|
|
if ( isset( $client_id ) && !empty( $client_id ) ) {
|
|
$ph[] = (string)$client_id;
|
|
$query .= ' AND a.client_id = ? ';
|
|
}
|
|
|
|
$this->db->Execute( $query, $ph );
|
|
$this->cache->clean( 'authentication' );
|
|
} catch ( Exception $e ) {
|
|
throw new DBError( $e );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* When user resets or changes their password, logout all sessions for that user.
|
|
* @param string $object_id UUID
|
|
* @param string $type_id
|
|
* @param bool $ignore_current_session Avoid logging out existing session, for example when the user is changing their own password.
|
|
* @return bool
|
|
* @throws DBError
|
|
*/
|
|
function logoutUser( $object_id, $type_id = [ 800, 810, 0 ], $client_id = [ 'browser-timetrex', 'app-timetrex' ], $ignore_current_session = true ) {
|
|
if ( $type_id !== null && is_array( $type_id ) === false ) {
|
|
$type_id = [ $type_id ];
|
|
}
|
|
|
|
if ( $client_id !== null && is_array( $client_id ) === false ) {
|
|
$client_id = [ $client_id ];
|
|
}
|
|
|
|
$delete_all_sessions = $type_id === null && $client_id === null;
|
|
|
|
$session_id = [];
|
|
|
|
if ( $ignore_current_session == true ) {
|
|
//logoutUser() is generally called outside the context/scope of global $authentication and acts as a static method.
|
|
//This is because supervisors can change other users passwords, and then we need to log out the target user.
|
|
//Because of that we need to see if the user being logged out is the currently logged-in user.
|
|
global $current_user;
|
|
if ( is_object( $current_user ) && $current_user->getId() == $object_id ) {
|
|
//If the user being logged out is the currently logged-in user, do not log out their current session or the app session that recently authenticated this session.
|
|
$current_session_id = $this->getCurrentSessionID( $type_id[0] ?? 800 );
|
|
$session_id[] = $this->encryptSessionID( $current_session_id );
|
|
|
|
$authentication = new Authentication();
|
|
$authentication->setAndReadForMultiFactor( $current_session_id );
|
|
$other_json = $authentication->getOtherJSON();
|
|
//Make sure we do not log them out if their most recent authenticated app session.
|
|
if ( isset( $other_json['mfa']['recent_authenticated_device_session_id'] ) ) {
|
|
$session_id[] = $other_json['mfa']['recent_authenticated_device_session_id'];
|
|
}
|
|
} else {
|
|
//If the user being logged out is NOT the currently logged-in user, then this is probably a supervisor changing the password of another user.
|
|
//We do not want to log out the app in this scenario if the user has mfa enabled, otherwise we would lock them out.
|
|
if ( is_array( $client_id ) == true && in_array( 'app-timetrex', $client_id ) ) {
|
|
$uflf = TTnew( 'UserListFactory' ); /** @var UserListFactory $uflf */
|
|
$uflf->getById( $object_id );
|
|
if ( $uflf->getRecordCount() > 0 ) {
|
|
$user_obj = $uflf->getCurrent(); /** @var UserFactory $user_obj */
|
|
if ( $user_obj->getMultiFactorType() > 0 ) {
|
|
$array_key = array_search( 'app-timetrex', $client_id );
|
|
if ( $array_key !== false ) {
|
|
unset( $client_id[$array_key] );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$ph = [
|
|
'object_id' => TTUUID::castUUID( $object_id )
|
|
];
|
|
|
|
try {
|
|
$query = 'DELETE FROM authentication WHERE object_id = ?';
|
|
|
|
if ( $delete_all_sessions === false ) { //Delete all deletes but the currently logged in browser and recently authenticated app session.
|
|
if ( is_array( $type_id ) && count( $type_id ) > 0 ) {
|
|
$type_ph = [];
|
|
foreach ( $type_id as $type ) {
|
|
$ph[] = $type;
|
|
$type_ph[] = '?';
|
|
}
|
|
|
|
$query .= ' AND type_id in (' . implode( ',', $type_ph ) . ')';
|
|
}
|
|
|
|
if ( is_array( $client_id ) && count( $client_id ) > 0 ) {
|
|
$client_ph = [];
|
|
foreach ( $client_id as $client ) {
|
|
$ph[] = $client;
|
|
$client_ph[] = '?';
|
|
}
|
|
|
|
$query .= ' AND client_id in (' . implode( ',', $client_ph ) . ')';
|
|
}
|
|
}
|
|
|
|
if ( is_array( $session_id ) && count( $session_id ) > 0 ) {
|
|
$session_ph = [];
|
|
|
|
foreach ( $session_id as $current_session ) {
|
|
$ph[] = $current_session;
|
|
$session_ph[] = '?';
|
|
}
|
|
$query .= ' AND session_id not in (' . implode( ',', $session_ph ) . ')';
|
|
}
|
|
|
|
$this->db->Execute( $query, $ph );
|
|
//Debug::Query( $query, $ph, __FILE__, __LINE__, __METHOD__, 10);
|
|
$this->cache->clean( 'authentication' );
|
|
Debug::text( 'Logging out all sessions for User ID: ' . $object_id . ' Affected Rows: ' . $this->db->Affected_Rows(), __FILE__, __LINE__, __METHOD__, 10 );
|
|
} catch ( Exception $e ) {
|
|
throw new DBError( $e );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//
|
|
//Functions to help check crendentials.
|
|
//
|
|
|
|
/**
|
|
* @param $user_name
|
|
* @param string $type
|
|
* @return bool|mixed
|
|
*/
|
|
function getCompanyObject( $user_name, $type = 'USER' ) {
|
|
$type = strtoupper( $type );
|
|
if ( $type == 'USER' ) {
|
|
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
|
|
$ulf->getByUserName( TTi18n::strtolower( $user_name ) );
|
|
} else if ( $type == 'USER_ID' ) {
|
|
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
|
|
$ulf->getById( $user_name );
|
|
} else if ( $type == 'QUICK_PUNCH' ) {
|
|
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
|
|
$ulf->getByPhoneIdAndStatus( TTi18n::strtolower( $user_name ), 10 );
|
|
} else if ( $type == 'JOB_APPLICANT' ) {
|
|
$ulf = TTnew( 'JobApplicantListFactory' ); /** @var JobApplicantListFactory $ulf */
|
|
$ulf->getByUserName( TTi18n::strtolower( $user_name ) );
|
|
}
|
|
|
|
if ( $ulf->getRecordCount() == 1 ) {
|
|
$u_obj = $ulf->getCurrent();
|
|
if ( is_object( $u_obj ) ) {
|
|
$clf = TTnew( 'CompanyListFactory' ); /** @var CompanyListFactory $clf */
|
|
$clf->getById( $u_obj->getCompany() );
|
|
if ( $clf->getRecordCount() == 1 ) {
|
|
return $clf->getCurrent();
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param $user_name
|
|
* @return bool
|
|
*/
|
|
function checkCompanyStatus( $user_name ) {
|
|
$company_obj = $this->getCompanyObject( $user_name, 'USER' );
|
|
if ( is_object( $company_obj ) ) {
|
|
//Return the actual status so we can do multiple checks.
|
|
Debug::text( 'Company Status: ' . $company_obj->getStatus(), __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
return $company_obj->getStatus();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks just the username, used in conjunction with HTTP Authentication/SSO.
|
|
* @param $user_name
|
|
* @return bool
|
|
*/
|
|
function checkUsername( $user_name ) {
|
|
//Use UserFactory to set name.
|
|
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
|
|
$ulf->getByUserNameAndEnableLogin( $user_name, true ); //Login Enabled
|
|
foreach ( $ulf as $user ) {
|
|
if ( TTi18n::strtolower( $user->getUsername() ) == TTi18n::strtolower( trim( $user_name ) ) ) {
|
|
$this->setObjectID( $user->getID() );
|
|
$this->setObject( $user );
|
|
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param $user_name
|
|
* @param $password
|
|
* @return bool
|
|
*/
|
|
function checkPassword( $user_name, $password ) {
|
|
//Use UserFactory to set name.
|
|
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
|
|
$ulf->getByUserNameAndEnableLogin( $user_name, true ); //Login Enabled
|
|
foreach ( $ulf as $user ) {
|
|
/** @var UserFactory $user */
|
|
if ( $user->checkPassword( $password ) ) {
|
|
$this->setObjectID( $user->getID() );
|
|
$this->setObject( $user );
|
|
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param int $phone_id
|
|
* @param $password
|
|
* @return bool
|
|
*/
|
|
function checkPhonePassword( $phone_id, $password ) {
|
|
//Use UserFactory to set name.
|
|
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
|
|
|
|
$ulf->getByPhoneIdAndStatus( $phone_id, 10 );
|
|
|
|
foreach ( $ulf as $user ) {
|
|
if ( $user->checkPhonePassword( $password ) ) {
|
|
$this->setObjectID( $user->getID() );
|
|
$this->setObject( $user );
|
|
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param $user_name
|
|
* @param $password
|
|
* @return bool
|
|
*/
|
|
function checkApplicantPassword( $user_name, $password ) {
|
|
$ulf = TTnew( 'JobApplicantListFactory' ); /** @var JobApplicantListFactory $ulf */
|
|
|
|
$ulf->getByUserName( $user_name );
|
|
|
|
foreach ( $ulf as $user ) {
|
|
if ( $user->checkPassword( $password ) ) {
|
|
$this->setObjectID( $user->getID() );
|
|
$this->setObject( $user );
|
|
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param string $id UUID
|
|
* @return bool
|
|
*/
|
|
function checkIButton( $id ) {
|
|
$uilf = TTnew( 'UserIdentificationListFactory' ); /** @var UserIdentificationListFactory $uilf */
|
|
$uilf->getByTypeIdAndValue( 10, $id );
|
|
if ( $uilf->getRecordCount() > 0 ) {
|
|
foreach ( $uilf as $ui_obj ) {
|
|
if ( is_object( $ui_obj->getUserObject() ) && $ui_obj->getUserObject()->getStatus() == 10 ) {
|
|
$this->setObjectID( $ui_obj->getUser() );
|
|
$this->setObject( $ui_obj->getUserObject() );
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param string $object_id UUID
|
|
* @param $employee_number
|
|
* @return bool
|
|
*/
|
|
function checkBarcode( $object_id, $employee_number ) {
|
|
//Use UserFactory to set name.
|
|
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
|
|
|
|
$ulf->getByIdAndStatus( $object_id, 10 );
|
|
|
|
foreach ( $ulf as $user ) {
|
|
if ( $user->checkEmployeeNumber( $employee_number ) ) {
|
|
$this->setObjectID( $user->getID() );
|
|
$this->setObject( $user );
|
|
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param string $id UUID
|
|
* @return bool
|
|
*/
|
|
function checkFingerPrint( $id ) {
|
|
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
|
|
|
|
$ulf->getByIdAndStatus( $id, 10 );
|
|
|
|
foreach ( $ulf as $user ) {
|
|
if ( $user->getId() == $id ) {
|
|
$this->setObjectID( $user->getID() );
|
|
$this->setObject( $user );
|
|
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param $user_name
|
|
* @return bool
|
|
*/
|
|
function checkClientPC( $user_name ) {
|
|
//Use UserFactory to set name.
|
|
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
|
|
|
|
$ulf->getByUserNameAndStatus( TTi18n::strtolower( $user_name ), 10 );
|
|
|
|
foreach ( $ulf as $user ) {
|
|
if ( $user->getUserName() == $user_name ) {
|
|
$this->setObjectID( $user->getID() );
|
|
$this->setObject( $user );
|
|
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns the value of the X-Client-ID HTTP header so we can determine what type of front-end we are using and if CSRF checks should be enabled or not.
|
|
* @return bool|string
|
|
*/
|
|
function getClientIDHeader() {
|
|
if ( isset( $_SERVER['HTTP_X_CLIENT_ID'] ) && $_SERVER['HTTP_X_CLIENT_ID'] != '' ) {
|
|
return trim( $_SERVER['HTTP_X_CLIENT_ID'] );
|
|
} else if ( isset( $_POST['X-Client-ID'] ) && $_POST['X-Client-ID'] != '' ) { //Need to read X-Client-ID from POST variables so Global.APIFileDownload() works.
|
|
return trim( $_POST['X-Client-ID'] );
|
|
} else if ( Misc::isMobileAppUserAgent() == true ) {
|
|
//Check if its Kiosk or Single Employee.
|
|
$parsed_user_agent = Misc::parseMobileAppUserAgent();
|
|
if ( isset( $parsed_user_agent['station_type'] ) && (int)$parsed_user_agent['station_type'] == 65 ) { //65=Kiosk
|
|
return 'App-TimeTrex-Kiosk'; //Kiosk Mode.
|
|
}
|
|
|
|
return 'App-TimeTrex'; //Single Employee
|
|
} else {
|
|
if ( isset( $_SERVER['SCRIPT_NAME'] ) && $_SERVER['SCRIPT_NAME'] != '' ) {
|
|
$script_name = $_SERVER['SCRIPT_NAME'];
|
|
|
|
//If the SCRIPT_NAME is something like upload_file.php, or APIGlobal.js.php, assume its the JSON API
|
|
// This is also set in parseEndPointID() and getClientIDHeader()
|
|
if ( $script_name == '' || ( strpos( $script_name, 'api' ) === false && strpos( $script_name, 'soap/server.php' ) === false ) ) {
|
|
return 'Browser-TimeTrex';
|
|
}
|
|
}
|
|
}
|
|
|
|
return 'API'; //Default to API Client-ID
|
|
}
|
|
|
|
/**
|
|
* Checks that the CSRF token header matches the CSRF token cookie that was originally sent.
|
|
* This uses the Cookie-To-Header method explained here: https://en.wikipedia.org/w/index.php?title=Cross-site_request_forgery#Cookie-to-header_token
|
|
* Also explained further here: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html -- "Double Submit Cookie" method.
|
|
* @return bool
|
|
*/
|
|
function checkValidCSRFToken() {
|
|
global $config_vars;
|
|
|
|
$client_id_header = $this->getClientIDHeader();
|
|
|
|
if ( $client_id_header != 'API' && $client_id_header != 'App-TimeTrex' && $client_id_header != 'App-TimeTrex-Kiosk' && $client_id_header != 'App-TimeTrex-AGI'
|
|
&& ( !isset( $config_vars['other']['enable_csrf_validation'] ) || ( isset( $config_vars['other']['enable_csrf_validation'] ) && $config_vars['other']['enable_csrf_validation'] == true ) )
|
|
&& ( !isset( $config_vars['other']['installer_enabled'] ) || ( isset( $config_vars['other']['installer_enabled'] ) && $config_vars['other']['installer_enabled'] != true ) ) //Disable CSRF if installer is enabled, because TTPassword::getPasswordSalt() has the potential to change at anytime.
|
|
) {
|
|
if ( isset( $_SERVER['HTTP_X_CSRF_TOKEN'] ) && $_SERVER['HTTP_X_CSRF_TOKEN'] != '' ) {
|
|
$csrf_token_header = trim( $_SERVER['HTTP_X_CSRF_TOKEN'] );
|
|
} else {
|
|
if ( isset( $_POST['X-CSRF-Token'] ) && $_POST['X-CSRF-Token'] != '' ) { //Global.APIFileDownload() needs to be able to send the token by POST or GET.
|
|
$csrf_token_header = trim( $_POST['X-CSRF-Token'] );
|
|
} else if ( isset( $_GET['X-CSRF-Token'] ) && $_GET['X-CSRF-Token'] != '' ) { //Some send_file.php calls need to be able to send the token by GET.
|
|
$csrf_token_header = trim( $_GET['X-CSRF-Token'] );
|
|
} else {
|
|
$csrf_token_header = false;
|
|
}
|
|
}
|
|
|
|
if ( isset( $_COOKIE['CSRF-Token'] ) && $_COOKIE['CSRF-Token'] != '' ) {
|
|
$csrf_token_cookie = trim( $_COOKIE['CSRF-Token'] );
|
|
} else {
|
|
$csrf_token_cookie = false;
|
|
}
|
|
|
|
if ( $csrf_token_header != '' && $csrf_token_header == $csrf_token_cookie ) {
|
|
//CSRF token is hashed with a secret key, so full token is: <TOKEN>-<HASHED WITH SECRET KEY TOKEN> -- Therefore make sure that the hashed token matches with our secret key.
|
|
$split_csrf_token = explode( '-', $csrf_token_header ); //0=Token value, 1=Salted token value.
|
|
if ( is_array( $split_csrf_token ) && count( $split_csrf_token ) == 2 && $split_csrf_token[1] == sha1( $split_csrf_token[0] . TTPassword::getPasswordSalt() ) ) {
|
|
return true;
|
|
} else {
|
|
Debug::Text( ' CSRF token value does not match hashed value! Client-ID: ' . $client_id_header . ' CSRF Token: Header: ' . $csrf_token_header . ' Cookie: ' . $csrf_token_cookie, __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
return false;
|
|
}
|
|
} else {
|
|
Debug::Text( ' CSRF token does not match! Client-ID: ' . $client_id_header . ' CSRF Token: Header: ' . $csrf_token_header . ' Cookie: ' . $csrf_token_cookie .' Total Cookies: '. count( $_COOKIE ), __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
return false;
|
|
}
|
|
} else {
|
|
return true; //Not a CSRF vulnerable end-point
|
|
}
|
|
}
|
|
|
|
/**
|
|
* One time auth is used to verify single actions that require re-authentication and needs to be removed after used.
|
|
* @return bool
|
|
*/
|
|
function reauthenticationActionCompleted() {
|
|
return $this->clearMultiFactorDataFlags( [ 'one_time_auth' ] );
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
function clearMultiFactorDataFlags( $flags_to_clear = [] ) {
|
|
//Clearing multifactor data after it has been used to prevent replay attacks and remove one time flags.
|
|
|
|
$other_json = $this->getOtherJSON();
|
|
|
|
//Remove all data from $other_json other than flags to clear keys
|
|
if ( isset( $other_json['mfa'] ) == true && is_array( $other_json['mfa'] ) == true && is_array( $flags_to_clear ) == true ) {
|
|
$other_json['mfa'] = array_diff_key( $other_json['mfa'], array_flip( $flags_to_clear ) );
|
|
$this->setOtherJSON( $other_json );
|
|
|
|
$this->Update();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param $user_id
|
|
* @param $device_id
|
|
* @return bool
|
|
*/
|
|
function isUserAllowedBypassMFA( $user_id, $device_id ) {
|
|
global $config_vars;
|
|
if ( isset( $config_vars['other']['disable_mfa'] ) && $config_vars['other']['disable_mfa'] == true ) {
|
|
Debug::Text( ' MFA is disabled, allowing user to bypass MFA...', __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
return true;
|
|
}
|
|
|
|
$atdlf = TTnew( 'AuthenticationTrustedDeviceListFactory' ); /** @var AuthenticationTrustedDeviceListFactory $atdlf */
|
|
$atdlf->getByUserIdAndDeviceId( $user_id, $this->encryptSessionID( $device_id ) );
|
|
if ( $atdlf->getRecordCount() == 1 ) { /** @var AuthenticationTrustedDeviceFactory $atd_obj */
|
|
$atd_obj = $atdlf->getCurrent();
|
|
if ( $atd_obj->getIPAddress() == Misc::getRemoteIPAddress() ) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param $enable
|
|
* @return void
|
|
*/
|
|
function setMfaStartListen( $enable ) {
|
|
$this->mfa_start_listen = (bool)$enable;
|
|
}
|
|
|
|
/**
|
|
* @return bool|mixed
|
|
*/
|
|
function getMfaStartListen() {
|
|
return $this->mfa_start_listen;
|
|
}
|
|
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
function setTrustedDevice() {
|
|
//If the TrustedDevice cookie is already set and valid for this user, don't do anything.
|
|
if ( isset( $_COOKIE['TrustedDevice'] ) ) {
|
|
$atdlf = TTnew( 'AuthenticationTrustedDeviceListFactory' ); /** @var AuthenticationTrustedDeviceListFactory $atdlf */
|
|
$atdlf->getByUserIdAndDeviceId( $this->getObjectID(), $this->encryptSessionID( (string)$_COOKIE['TrustedDevice'] ) );
|
|
if ( $atdlf->getRecordCount() > 0 ) {
|
|
$atd_obj = $atdlf->getCurrent();
|
|
if ( ( TTDate::getTime() - $atd_obj->getCreatedDate() ) < ( 86400 * 30 ) ) { //30 days
|
|
Debug::Text( 'Trusted device cookie already set and valid for this user...', __FILE__, __LINE__, __METHOD__, 10 );
|
|
return true;
|
|
}
|
|
} else {
|
|
//Delete expired cookie.
|
|
setcookie( 'TrustedDevice', '', TTDate::getTime() - 3600, Environment::getCookieBaseURL(), '', Misc::isSSL( true ) );
|
|
}
|
|
}
|
|
|
|
//User doesn't have a TrustedDevice cookie set, and we need to create one.
|
|
$device_id = $this->genSessionID();
|
|
|
|
$atd_obj = TTnew( 'AuthenticationTrustedDeviceFactory' ); /** @var AuthenticationTrustedDeviceFactory $atd_obj */
|
|
$atd_obj->setUser( $this->getObjectID() );
|
|
$atd_obj->setDeviceID( $this->encryptSessionID( $device_id ) );
|
|
|
|
if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
|
|
require_once( Environment::getBasePath() . 'vendor' . DIRECTORY_SEPARATOR . 'cbschuld' . DIRECTORY_SEPARATOR . 'browser.php' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Browser.php' );
|
|
$browser = new Browser( $_SERVER['HTTP_USER_AGENT'] );
|
|
$user_agent = $browser->getBrowser();
|
|
if ( $user_agent === 'unknown' ) {
|
|
$atd_obj->setDeviceUserAgent( $_SERVER['HTTP_USER_AGENT'] );
|
|
} else {
|
|
$atd_obj->setDeviceUserAgent( $browser->getBrowser() . $browser->getPlatform() );
|
|
}
|
|
}
|
|
|
|
$atd_obj->setIPAddress( Misc::getRemoteIPAddress() );
|
|
$atd_obj->setLocation( Misc::getLocationOfIPAddress() );
|
|
|
|
if ( $atd_obj->isValid() ) {
|
|
$atd_obj->Save();
|
|
//Valid for 30 days.
|
|
setcookie( 'TrustedDevice', $device_id, ( TTDate::getTime() + ( 86400 * 30 ) ), Environment::getCookieBaseURL(), '', Misc::isSSL( true ) );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns data formatted for the API response after a successful login.
|
|
* @param $user_action_message
|
|
* @return array
|
|
*/
|
|
function getAuthenticationResponseData( $user_action_message = '' )
|
|
{
|
|
return [
|
|
'status' => true,
|
|
'session_id' => $this->getSessionId(),
|
|
'session_type' => $this->getTypeName(),
|
|
'mfa' => [
|
|
'step' => $this->getMFAStep(),
|
|
'type_id' => $this->getMFATypeID(),
|
|
'user_action_message' => TTi18n::getText( 'Please check your device to verify your identity' ),
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
function getMFAStep() {
|
|
return $this->getMFAStartListen() ? 'start_listen' : false;
|
|
}
|
|
}
|
|
?>
|