TimeTrex/classes/modules/core/Authentication.class.php

2186 lines
73 KiB
PHP
Raw Permalink Normal View History

2022-12-13 07:10:06 +01:00
<?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;
}
}
?>