TEMPLATE_CLASS_USERS, META_SESSION_DAEMON => 1 // todo -- hook this up to something! ]; } elseif (!isset($_meta[META_TEMPLATE])) { // client didn't submit a template; let's fix that for them $_meta[META_TEMPLATE] = TEMPLATE_CLASS_USERS; } elseif ($_meta[META_TEMPLATE] != TEMPLATE_CLASS_USERS) { // there's a difference between unset and being set to the wrong class // todo -- system event for a hack attempt $_meta[META_TEMPLATE] = TEMPLATE_CLASS_USERS; } try { parent::__construct($_meta, $_id); if (!$this->isServiceLocal(ENV_TERCERO)) return; $this->myToken = ''; $this->myAPIKey = ''; $this->myUserType = ''; $this->securityConfig = gasConfig::$settings[CONFIG_SECURITY]; $this->emailConfig = gasConfig::$settings[CONFIG_EMAIL]; $this->sessionConfig = gasConfig::$settings[CONFIG_SESSIONS]; } catch (Throwable $t) { $hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__); $this->eventMessages[] = ERROR_EXCEPTION; $msg = ERROR_FAILED_TO_INSTANTIATE . TEMPLATE_CLASS_USERS; $this->logger->fatal($hdr . $msg); consoleLog($this->res, CON_ERROR, $msg); $this->state = STATE_FRAMEWORK_FAIL; $this->status = false; return; } } /** * registerNewUser() -- public method * * This method is called when we're adding a new user to the system. The array of request data, as processed by * the broker, is the only input parameter. * * The function is of type void -- processing results are reflected in the class member settings. * * The method process the input data, ensuring that the minimally-required fields are present. The method then * performs the following validation checks and storage actions: * * 1. Validate the user's email address and domain * 2. Create the user record * 3. Create the user's session, linking the user record * 4. Create a System Event for the timer (expiry) event on Admin * 5. Publish an Admin request to register the session with AT(1) * * In the event of a system error, where we fail to instantiate a class or save a record, since we're working * with three different class objects in this method, and if the error is encountered in the session or system- * event classes, then we'll copy the eventMessages stack on that object over to the user object before * returning control to the calling client. * * Again, as there are no explicit or implicit returns via the parameters, the calling client has to check the * user-class data members for the results. * * * @author mike@givingassistant.org * @version 1.0 * * @param array $_data * * * HISTORY: * ======== * 09-14-20 mks DB-168: Original coding * */ public function registerNewUser(array $_data):void { $error = false; $this->state = STATE_VALIDATION_ERROR; $this->status = false; // we minimally need the user email and hashed password to proceed $minFields = [ USER_PII_EMAIL . $this->ext, USER_PASSWORD . $this->ext ]; // add fields as necessary // first, we're only going to allow the creation of one record per request... // ...so make sure $data is an array... if (!is_array($_data[BROKER_DATA])) { $this->eventMessages[] = ERROR_DATA_ARRAY_NOT_ARRAY; return; } // ...and that the array only has one record if (count($_data[BROKER_DATA]) != 1) { $this->eventMessages[] = sprintf(ERROR_DATA_ARRAY_COUNT, 1, STRING_DATA, count($_data[BROKER_DATA])); return; } $data = $_data[BROKER_DATA][0]; // make sure the minimally-required fields are present: foreach ($minFields as $field) { if (!array_key_exists($field, $data)) { $this->eventMessages[] = ERROR_DATA_KEY_404 . $field; $error = true; } } if ($error) return; // email validation: proper email, wblist, email does not already exist $this->validateUserEmail($data[USER_PII_EMAIL . $this->ext]); if (!$this->status) return; // grab the partnerID, if it exists // (if no partner ID, the assumption is that we're creating an internal user) if (array_key_exists(CLIENT_AUTH_TOKEN, $_data[BROKER_META_DATA])) $data[USER_PARTNER_API_KEY] = $_data[BROKER_META_DATA][CLIENT_AUTH_TOKEN]; // create the new-user record and start gathering session data $this->_createRecord([$data]); if (!$this->status) return; // calculate the session duration based on the XML configuration $duration = (gasConfig::$settings[CONFIG_SESSIONS][CONFIG_SESSIONS_DURATION_DAYS]) ? gasConfig::$settings[CONFIG_SESSIONS][CONFIG_SESSIONS_DURATION_DAYS] * NUMBER_ONE_DAY : gasConfig::$settings[CONFIG_SESSIONS][CONFIG_SESSIONS_DURATION_HOURS] * NUMBER_ONE_HOUR_SEC; // build the session-record payload $sessionData = [ SESSION_EXPIRES => time() + $duration, SESSION_DURATION => $duration, SESSION_LEVEL => SESSION_LEVEL_USER, SESSION_FK_USER => $this->getColumn(DB_TOKEN) // todo: other fields are legacy and we need to learn what to fill them with... ]; // instantiate a new session object $metaCopy = $this->metaPayload; $metaCopy[META_TEMPLATE] = TEMPLATE_CLASS_SESSIONS; $errors = []; if (is_null($objSession = grabWidget($metaCopy, '', $errors))) { $this->eventMessages = [...$this->eventMessages, $errors]; $this->state = STATE_FRAMEWORK_WARNING; $hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__); $msg = ERROR_TEMPLATE_INSTANTIATE . $metaCopy[META_TEMPLATE]; $this->logger->warn($hdr . $msg); consoleLog($this->res, CON_ERROR, $msg); return; } // create the session record $objSession->_createRecord([$sessionData], DATA_USER); if (!$objSession->status) { $this->eventMessages = [ ...$this->eventMessages, ...$objSession->eventMessages ]; if (is_object($objSession)) $objSession->__destruct(); unset($objSession); $this->state = STATE_FRAMEWORK_FAIL; return; } // theses two values will be harvested back up at the broker level and used to create the return payload $this->sessionGUID = $objSession->getColumn(DB_TOKEN); $this->userGUID = $this->getColumn(DB_TOKEN); // create the system-event data and publish the event to admin (or save locally if admin is local) $eventData = [ SYSTEM_EVENT_NAME => EVENT_NAME_SESSION_EXPIRY, SYSTEM_EVENT_STATUS => STATUS_ACTIVE, SYSTEM_EVENT_TYPE => EVENT_TYPE_SESSION, SYSTEM_EVENT_FK_SESSION_GUID => $this->sessionGUID, SYSTEM_EVENT_FK_USER_GUID => $this->userGUID, SYSTEM_EVENT_CLASS => get_class($this), SYSTEM_EVENT_DURATION => $duration, SYSTEM_EVENT_BROKER_EVENT => $_data[BROKER_REQUEST], SYSTEM_EVENT_OGUID => $metaCopy[META_EVENT_GUID], SYSTEM_EVENT_CODE_LOC => basename(__FILE__) . AT . __LINE__, SYSTEM_EVENT_META_DATA => $metaCopy, SYSTEM_EVENT_NOTES => basename(__METHOD__) ]; // invoke the function to publish the system event request and register the session with AT(1) on Admin: if (!postSystemEvent($eventData, $metaCopy[META_EVENT_GUID], $this->logger)) { $hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__); @handleExceptionMessaging($hdr, ERROR_MDB_SYS_EVENT_SAVE, $this->eventMessages, true); } // call admin to fetch the token for the system event record we just created // next, register the event with the admin broker AT(1) service $request = [ BROKER_REQUEST => BROKER_REQUEST_NEW_SESSION, BROKER_DATA => [ SYSTEM_EVENT_DURATION => $duration ], BROKER_META_DATA => [ META_TEMPLATE => TEMPLATE_CLASS_SYS_EVENTS, META_SESSION_GUID => $this->sessionGUID, META_CLIENT => CLIENT_SYSTEM ] ]; // get a copy of the record we just created // $dataSystemEvent = $objSession->getData(); // $dataSystemEvent = $dataSystemEvent[0]; // create the broker client and publish the BROKER_REQUEST_NEW_SESSION event to AdminInBroker to register // the session expiry with AT(1) /** @var gacWorkQueueClient $tmpObj */ $tmpObj = new gacWorkQueueClient(basename(__METHOD__) . AT . __LINE__); if (is_null($tmpObj) or !$tmpObj->status) { $hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__); $msg = ERROR_TEMPLATE_INSTANTIATE . sprintf(ERROR_BROKER_CLIENT_INSTANTIATION, STRING_WORK_QUEUE_CLIENT); @handleExceptionMessaging($hdr, $msg, $this->eventMessages, true); return; } // fire-n-forget queue $tmpObj->call(gzcompress(json_encode($request))); // todo: validation email // clean-up and return a success condition if (is_object($objSession)) $objSession->__destruct(); if (is_object($tmpObj)) $tmpObj->__destruct(); unset($objSession, $tmpObj); $this->state = STATE_SUCCESS; $this->status = true; } /** * validateUserEmail() -- public method * * this method is the single entry-point for all email checks. * * this method checks a submitted email address: * * 1. sanitize the email removing invalid characters * 2. validate that the email is in the correct format * 3. validate the email domain * 4. validate the the email is unique * 5. validate against the WBL list * * If case 1 or case 2 fails, then a STATE_VALIDATION_ERROR is returned -- the email address itself is invalid * * Next, check to see if the email address exists in the database. We're going to check both the primary * and alternate email addresses. * * if the query comes back success and the data count is true, that's implicit indication that the * email is in-use. What we want to do is make an additional check on the status of the email. * * If the status is false, (from the query), then we want to check for a 404-state -- indicating that no records * were found for that email and it's ok to use. * * any other state/status combination defaults to a framework warning and a check-logs diagnostic is generated. * * Return States: * -------------- * STATE_SUCCESS -- email address is valid, not already in-use, and is white-listed * STATE_NOT_WHITE_LIST -- email does not appear on the white list and white-list checks are enabled * STATE_BLACK_LIST -- email has been black-listed * STATE_VALIDATION_ERROR -- email address as submitted was empty or malformed * STATE_ALREADY_EXISTS -- email address is in-use and cannot be reused * STATE_MAIL_FAIL -- processing error within the framework * STATE_FRAMEWORK_WARNING -- some random bad thing happened and needs investigation * * Note that the $_email parameter is a call-by-reference who's contents may be altered by this method. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_email -- the email to validate/check * @param Boolean $_skipCheck -- if set to true, skip email-already-exists check (for logins) * * HISTORY: * ======== * 08-18-20 mks DB-168: original coding * */ public function validateUserEmail(string &$_email, bool $_skipCheck = false):void { $oldData = null; $whiteList = true; $blackList = true; $oldCount = 0; $this->state = STATE_VALIDATION_ERROR; $this->status = false; $backup = false; if (empty($this->metaPayload)) { $hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__); $this->logger->warn($hdr . ERROR_DATA_META_404); $this->eventMessages[] = ERROR_DATA_META_404; $this->status = false; $this->state = STATE_META_ERROR; return; } $metaCopy = $this->metaPayload; $metaCopy[META_TEMPLATE] = TEMPLATE_CLASS_WBL; if ($this->count and !empty($this->data)) { $oldData = $this->getData(); $oldCount = $this->count; $this->data = []; $this->count = 0; $backup = true; } // ensure we have input parameters if (empty($_email)) { $hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__); $msg = ERROR_DATA_KEY_404 . USER_PII_EMAIL; $this->eventMessages[] = $msg; $this->logger->data($hdr . $msg); if ($backup) { $this->count = $oldCount; $this->data = $oldData; } return; } if (empty($this->emailConfig) or !is_array($this->emailConfig)) { $hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__); $msg = ERROR_CONFIG_RESOURCE_404 . RESOURCE_EMAIL; $this->eventMessages[] = $msg; $this->logger->data($hdr . $msg); if ($backup) { $this->count = $oldCount; $this->data = $oldData; } return; } // force the user email to lowercase $_email = trim(mb_strtolower($_email)); // validate the email and email domain if (!checkEmailAndDomain($_email)) { $hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__); $msg = ERROR_DIAG_EMAIL_MALFORMED . COLON . $_email; @handleExceptionMessaging($hdr, $msg, $this->eventMessages); $this->state = STATE_MAIL_FAIL; return; } // check for duplicate emails if (!$_skipCheck) { $state = $this->emailSearch($_email); switch ($state) { case STATE_FRAMEWORK_WARNING : case STATE_ALREADY_EXISTS : $this->state = $state; $this->status = false; if ($backup) { $this->count = $oldCount; $this->data = $oldData; } return; break; case STATE_DOES_NOT_EXIST : // do nothing: optimal return break; default : $this->status = false; $msg = ERROR_UNKNOWN_STATE . $state; $this->eventMessages[] = $msg; $this->logger->warn($msg); if ($backup) { $this->count = $oldCount; $this->data = $oldData; } return; break; } } // if either whitelisting or blacklisting are enabled for the client, then check the wbl if ($whiteList or $blackList) { try { $this->checkWBL($_email); if ($this->debug) { $this->logger->debug('email checked: ' . $_email); $this->logger->debug('objWBL state: ' . $this->state); $this->logger->debug('objWBL status: ' . (($this->status) ? STRING_TRUE : STRING_FALSE)); } } catch (Throwable $t) { $hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__); $msg = $t->getMessage(); $this->eventMessages[] = ERROR_EXCEPTION; @handleExceptionMessaging($hdr, $msg, $this->eventMessages); $this->eventMessages = array_pop($this->eventMessages); return; } } else { $this->state = STATE_SUCCESS; $this->status = true; } // reset the current class data if ($backup) { $this->count = $oldCount; $this->data = $oldData; } } /** * emailSearch() -- private method * * this method accepts an email address as it's only input parameter and generates a query to check to * see if the email already exists in the user collection. * * the method returns a state (string) determined by the following conditions: * * STATE_FRAMEWORK_WARNING - processing or db error * STATE_ALREADY_EXISTS - email is already in-use in the db * STATE_DOES_NOT_EXIST - email is not in-use (desired return) * * The calling client should evaluate the return state accordingly as this method does not change the class * state/status params. * * The calling client should also reset the data payload as, if a record is found, then the found record will * be added to the current data member. * * This method was written to reduce the code footprint of the validateUserEmail method. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_email * @return string * * HISTORY: * ======== * 08-18-20 mks DB-169: original coding * */ private function emailSearch(string $_email): string { $return = STATE_FRAMEWORK_WARNING; // check if tercero is not a "local" service if (!gasConfig::$settings[ENV_TERCERO][CONFIG_IS_LOCAL]) return $return; // set-up the email query $query = [ STRING_QUERY_DATA => [ USER_PII_EMAIL => [ OPERAND_NULL => [ OPERATOR_EQ => [ $_email ]]], USER_PII_SECONDARY_EMAIL => [ OPERAND_NULL => [ OPERATOR_EQ => [ $_email]]], OPERAND_OR => null ], STRING_RETURN_DATA => [ CM_TOKEN ] ]; // query the db $this->_fetchRecords($query); switch ($this->status) { case true : if ($this->count) { $hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__); $msg = sprintf(ERROR_EMAIL_DUPLICATE, $_email); $this->eventMessages[] = $msg; $this->logger->data($hdr . $msg); $return = STATE_ALREADY_EXISTS; } elseif ($this->state == STATE_NOT_FOUND) { return STATE_DOES_NOT_EXIST; } break; case false : $hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__); $this->logger->warn($hdr . $this->strQuery); $this->logger->warn($hdr . ERROR_CHECK_LOGS); $this->eventMessages[] = ERROR_CHECK_LOGS; break; } return($return); } /** * checkWBL() -- public method * * this method checks the submitted email and domain against the White/Black list data. * * there is but one input parameter required for the method: * * $_email -- a string containing the email to be evaluated * * the method begins by looping through the email - the address is processed by first evaluating the * entire email (for a match in the WBL table) and, if not found, then we continue with the domain-part of * the email (right-side of the '@') and continue to remove sub-domains until we get to the TLD. If we * get to the TLD, then the user is neither black-listed or white-listed. * * if we get a domain match, or if we match the entire email, then we look at the WBL record "type" (a boolean) * to determine if the WBL record is a black (false) or white (true) listed email. * * The following states are assigned to the class under the following: * * STATE_DB_ERROR -- the WBL record was found, but no value was stored in the "type_wbl" column * -- more than one WBL record was found for the email/domain * -- the search query failed to execute successfully * STATE_SUCCESS -- the email/domain is white-listed * STATE_BLACK_LIST -- the email/domain is black-listed * STATE_NOT_FOUND -- the email/domain is neither white-listed or black-listed, or wbl is disabled * * the method return is the class STATE variable which should be evaluated by the calling client upon return. * * Programmer's Notes: * ------------------- * This method is a member of the user class, as opposed to the WBList class, for efficiency - we can check a user's * email in this class without resorting to instantiating another data class (WBList) for the check. * * PENDING WORK: * ------------- * todo: code the security event when a black-listed return is encountered * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_email * * HISTORY: * ======== * 08-19-20 mks DB-168: original coding * */ public function checkWBL(string $_email):void { // lvar init $wblState = null; $emailBits = explode(AT, $_email); $domain = $emailBits[1]; $diminishingDomain = $domain; $method = basename(__METHOD__); $domainBits = explode(DOT, $domain); $counter = 0; $errorList = []; $this->status = false; $this->state = STATE_VALIDATION_ERROR; $blEnabled = boolval(gasConfig::$settings[CONFIG_SECURITY][CONFIG_SECURITY_BANNED_LIST]); $wlEnabled = boolval(gasConfig::$settings[CONFIG_SECURITY][CONFIG_SECURITY_RESTRICTED_LIST]); // return immediately if wbl is disabled system-wide if (!$blEnabled and !$wlEnabled) { $this->state = STATE_NOT_SUPPORTED; $this->status = true; return; } $tmpMeta = $this->metaPayload; $tmpMeta[META_TEMPLATE] = TEMPLATE_CLASS_WBL; /** @var gacMongoDB $widget */ if (is_null($widget = grabWidget($tmpMeta, '', $errorList))) { foreach ($errorList as $error) $this->logger->error($error); $this->eventMessages = [...$this->eventMessages, ...$errorList]; return; } /* * we start with all of the domain. If there are sub-domains embedded in the domain, * start by searching with the left-most sub-domain and each iteration, remove a sub-domain * until we either find an entry in the collection, or we run out of domain. * * This technique allows us to validate an email submitted such as: mike@backend.engineering.givva.com * to one of the following: * * 1. mike@backend.engineering.givva.com * 2. mike@engineering.givva.com * 3. mike@givva.com * */ // first, check to see if the email USER is explicitly listed in the WBL collection: $query = [ USER_PII_EMAIL => [ OPERAND_NULL => [ OPERATOR_EQ => [ $_email ]]]]; $widget->_fetchRecords([STRING_QUERY_DATA => $query]); if ($widget->status and $widget->count == 1) { // email is either white or black-listed if (is_null($wbl = $widget->getColumn(MONGO_WBL_TYPE))) { // edge case: there is no WBL Type setting so ... this is a system event! Why would a user be listed // in the table but not have a setting?!? Database error... // todo: system event for incomplete data record found in the database $hdr = sprintf(INFO_LOC, $method, __LINE__); $this->state = STATE_DATA_ERROR; $this->eventMessages[] = ERROR_GENERIC_CUSTOMER; $this->logger->warn($hdr . ERROR_BAD_DATA_RECORD . 'wbl-type is not populated'); if (is_object($widget)) $widget->__destruct(); unset($widget); return; } // ... otherwise, if there we found a record for the email address, check to see if it's a white or black-listed entry $this->state = (boolval($wbl)) ? STATE_SUCCESS : STATE_BLACK_LIST; $this->status = ($this->state == STATE_SUCCESS) ? true : false; if (is_object($widget)) $widget->__destruct(); unset($widget); return; } elseif ($widget->status and $widget->state == STATE_NOT_FOUND) { // record was not found - which is not a problem unless whitelisting is enabled $this->status = true; $this->state = ($wlEnabled) ? STATE_NOT_WHITE_LIST : STATE_SUCCESS; if (is_object($widget)) $widget->__destruct(); unset($widget); return; } elseif ($widget->status and $widget->count > 1 and $widget->state != STATE_NOT_FOUND) { // more than one record was found $msg = sprintf(MONGO_FAILED_TOO_MANY_RECS, 1) . $widget->count; $hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__); @handleExceptionMessaging($hdr, $msg, $this->eventMessages, true); $this->state = STATE_DB_ERROR; if (is_object($widget)) $widget->__destruct(); unset($widget); return; } elseif (!$widget->status) { // query fail $this->eventMessages[] = ERROR_CHECK_LOGS; $this->state = STATE_FAIL; if (is_object($widget)) $widget->__destruct(); unset($widget); return; } // process the email DOMAIN until we find an entry in the wbl table or we run out of domain for ($index = count($domainBits); $index > 1; $index--) { $query = [USER_PII_EMAIL => [OPERAND_NULL => [OPERATOR_EQ => [(AT . $diminishingDomain)]]]]; $widget->_fetchRecords([STRING_QUERY_DATA => $query]); $wblState = $widget->getColumn(MONGO_WBL_TYPE); if ($widget->status and $widget->count == 1) { // a wbl record exists for the domain if (false === boolval($wblState)) { // domain has been explicitly black listed $this->state = STATE_BLACK_LIST; $this->status = true; if (is_object($widget)) $widget->__destruct(); unset($widget); return; // todo -- system event } elseif (true === boolval($wblState)) { // domain has been explicitly white listed $this->state = STATE_SUCCESS; $this->status = true; if (is_object($widget)) $widget->__destruct(); unset($widget); return; } elseif (is_null($wblState)) { // record exists but type is not defined $this->state = STATE_DATA_ERROR; if (is_object($widget)) $widget->__destruct(); unset($widget); return; } } elseif ($widget->status and $widget->state == STATE_NOT_FOUND) { // no records returned -- shrink the domain $diminishingDomain = ltrim($diminishingDomain, ($domainBits[$counter++] . DOT)); } } // if we're done processing the domain and we land here, then the check the $wblType for null if (is_null($wblState) and $wlEnabled) { // none of the domain was found and white-listing is enabled $this->state = STATE_NOT_WHITE_LIST; $this->status = false; } else { // wl is not enabled and no wbl record was found for any of the domain $this->state = STATE_SUCCESS; $this->status = true; } if (is_object($widget)) $widget->__destruct(); unset($widget); } /** * hashText() -- protected method * * This method requires a single input parameter, the text to be hashed: * * $_text -- string containing the text to be hashed * * The method will check the XML configuration for the hashing algorithm to use. If the security section was * not properly loaded during instantiation, or if the calling client did not provide input text, then an error * message will be generated and a null value returned. * * Otherwise, we'll use the password_hash() function to generate a hash of the request text. If the function * returns a Boolean(false), then we'll generate an error message and return a null to the requesting client. * * Otherwise, return the hashed string. * * Programmer's Notes: * ------------------- * I've marked this as protected so that it cannot be invoked (generate a hash) outside of the user instantiation * stack. This is to limit access to the Namaste hashing algorithm to only the user class. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_text * @return string|null * * * HISTORY: * ======== * 08-31-20 mks DB-168: original coding * */ protected function hashText(string $_text):?string { $method = basename(__METHOD__); if (empty($this->securityConfig)) { $hdr = sprintf(INFO_LOC, $method, __LINE__); $msg = ERROR_CONFIG_RESOURCE_404 . CONFIG_SECURITY; @handleExceptionMessaging($hdr, $msg, $this->eventMessages); return null; } if (empty($_text)) { $hdr = sprintf(INFO_LOC, $method, __LINE__); $msg = ERROR_DATA_404; @handleExceptionMessaging($hdr, $msg, $this->eventMessages); return null; } try { // if set, pull the hash algorithm from namaste config, o/w set to default and call hash function $hash = password_hash($_text, (!isset($this->securityConfig[CONFIG_SECURITY_HASH_ALGO])) ? PASSWORD_ARGON2I : $this->securityConfig[CONFIG_SECURITY_HASH_ALGO]); if (false === $hash) { $hdr = sprintf(INFO_LOC, $method, __LINE__); $msg = ERROR_PASSWORD_HASH_GENERATION_FAILED; @handleExceptionMessaging($hdr, $msg, $this->eventMessages); return null; } return $hash; } catch (TypeError | Throwable $t) { $hdr = sprintf(INFO_LOC, $method, __LINE__); @handleExceptionMessaging($hdr, $t->getMessage(), $this->eventMessages); return null; } } /** * hashFetch() -- private method * * This method has two input parameters: * * $_searchValue - this should be either a 36-character GUID value, or an email address * $_retData -- call-by-reference parameter that, if submitted, will contain the user's type and api-key in * addition to the password hash and account token * * We'll test to see if the input value is either a GUID or an email address and will structure the query * to match. If the searchValue is not either, then generate error messages, return a null, and exit. * * If the query generated an error, or returns a not found, we'll generate appropriate messaging and return * a null value to the calling client preserving class state/status and query results data. * * Otherwise, we'll return the password hash to the calling client. * * Programmer's Notes: * ------------------- * This function is private so as to limit access to the user table for the purposes of accessing the hash key * to this class only. * * * @author mike@givingassistant.org * @version 1.0 * * @param string|null $_searchValue -- the email or guid value of the target record * @param array|null $_retData -- call by reference param to return additional record data * @return string|null * * * HISTORY: * ======== * 08-31-20 mks DB-168: original coding * */ private function hashFetch(?string $_searchValue = null, ?array &$_retData = null):?string { $method = basename(__METHOD__); // search key can be either a token or an email address - figure out which if (empty($_searchValue)) { $hdr = sprintf(INFO_LOC, $method, __LINE__); $msg = ERROR_PARAM_404 . STRING_TOKEN; @handleExceptionMessaging($hdr, $msg, $this->eventMessages); return null; } elseif (validateGUID($_searchValue)) $searchKey = STRING_TOKEN; elseif (false !== filter_var($_searchValue, FILTER_VALIDATE_EMAIL)) $searchKey = USER_PII_EMAIL; else { $hdr = sprintf(INFO_LOC, $method, __LINE__); $msg = ERROR_DATA_INVALID_KEY . $_searchValue; @handleExceptionMessaging($hdr, $msg, $this->eventMessages); return null; } // build the query to fetch the user's hash based on the record token $query = [ $searchKey => [ STRING_TOKEN => [ OPERAND_NULL => [ OPERATOR_EQ => [ $_searchValue]]]], STRING_RETURN_DATA => [ USER_PASSWORD, USER_TYPE, USER_PARTNER_API_KEY, DB_STATUS ] ]; $this->_fetchRecords($query); if (!$this->status) { $hdr = sprintf(INFO_LOC, $method, __LINE__); $msg = sprintf(ERROR_MDB_QUERY_FAIL, STRING_SEARCH); @handleExceptionMessaging($hdr, $msg, $this->eventMessages); return null; } elseif ($this->state == STATE_NOT_FOUND) { $this->eventMessages[] = ERROR_DATA_404; return null; } // class data gets reset on successful search and return $_retData = $this->getData(); $_retData = $_retData[0]; if (isset($_retData[STRING_PASSWORD . $this->ext])) unset($_retData[STRING_PASSWORD . $this->ext]); $hash = $this->getColumn(USER_PASSWORD); $this->removeData(); return $hash; } /** * hashCheck() -- public function * * This is the public function, access point, for validating a user's password hash. The method has the * following input parameters to the method: * * $_searchValue -- this can be either a GUID or an email address and will be used as search key * $_hashText -- this is the hash text as generated by the client that we'll compare to the stored hash * * There are no explicit parameters returned. However, we make use of the class state/status members to pass-back * the state and status of the request which should be processed by the calling client. * If either parameter is passed empty, then generate messaging and return. * * The method invokes the hashFetch() method, passing in the search-value to fetch the password hash from the * database record and, in the same line, invokes the password_verify() function to validate the user * submitted pre-hash value against the stored hash. * * Programmer's Notes: * ------------------- * Anytime you want to add layered-validation, like ensuring that partner account belongs to a partner, you'll want * to add those checks to this method. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_searchValue * @param $_hashText * * * HISTORY: * ======== * 08-31-20 mks DB-168: Original coding * */ public function hashCheck(string $_searchValue, $_hashText):void { $this->status = false; $this->state = STATE_AUTH_ERROR; $userData = null; $method = basename(__METHOD__); if (empty($_searchValue) or empty($_hashText)) { $this->eventMessages[] = ERROR_DATA_404; return; } if (password_verify($_hashText, $this->hashFetch($_searchValue, $userData))) { // if the hash verification was successful... // check the account status for non-active states: if ($userData[DB_STATUS . $this->ext] != STATUS_ACTIVE) { switch ($userData[DB_STATUS . $this->ext]) { case STATUS_LOCKED : case STATUS_CLOSED : case STATUS_SUSPENDED : case STATUS_REVOKED : case STATUS_INACTIVE : // account needs CSR intervention break; case STATUS_ABANDONED : // account needs to have status updated and password change forced break; case STATUS_PENDING : // account needs to validate their email break; } } if (isset($userData[USER_TYPE . $this->ext]) and ($userData[USER_TYPE . $this->ext] == USER_TYPE_PARTNER)) { // ...and we have a userType and that type is equal to "partner"... if (isset($userData[USER_PARTNER_API_KEY . $this->ext]) and validateGUID($userData[USER_PARTNER_API_KEY . $this->ext])) { // ...and we have an partner API key in the user record... if (isset($this->metaPayload[CLIENT_AUTH_TOKEN])) { // ...and we have the XPI-Key set in the meta payload... if ($this->metaPayload[CLIENT_AUTH_TOKEN] == $userData[USER_PARTNER_API_KEY . $this->ext]) { // ...and the meta-payload X-API-Key matches the API-Key stored in the user record... // then we have a successful partner-user login! $this->myAPIKey = $userData[USER_PARTNER_API_KEY . $this->ext]; $this->myUserType = $userData[USER_TYPE . $this->ext]; if (isset($userData[STRING_TOKEN . $this->ext])) $this->myToken = $userData[STRING_TOKEN . $this->ext]; $this->state = STATE_SUCCESS; $this->status = true; return; } else { // client auth token in meta payload does not match xpi-key in user record $this->eventMessages[] = ERROR_PARTNER_API_KEY_MISMATCH; return; // todo security system event } } else { // Event request was from a partner, but user is not associated with a partner account $this->eventMessages[] = ERROR_PARTNER_USER_NOT_MEMBER; // todo security system event return; } } else { // either the api-key is not set or is an invalid guid (and the user is partner'd) if (!isset($userData[USER_PARTNER_API_KEY . $this->ext])) { // partner key was not set in the user record $hdr = sprintf(INFO_LOC, $method, __LINE__); $this->logger->warn($hdr . ERROR_PARTNER_USER_NOT_REGISTERED); $this->eventMessages[] = ERROR_PARTNER_USER_NOT_MEMBER; $this->state = STATE_DB_ERROR; return; } else { // the partner key guid stored in the user record is bad $hdr = sprintf(INFO_LOC, $method, __LINE__); $this->eventMessages[] = ERROR_PARTNER_USER_DATA; $msg = sprintf(ERROR_PARTNER_USER_HAS_BAD_GUID, $userData[USER_PARTNER_API_KEY . $this->ext], USER_PARTNER_API_KEY); $this->logger->warn($hdr . $msg); $this->logger->warn(ERROR_INVALID_GUID . $userData[USER_PARTNER_API_KEY . $this->ext]); $this->state = STATE_DATA_ERROR; return; } } } // END - check to see if user belongs to a partner } else { $this->eventMessages[] = ERROR_PASSWORD_MISMATCH; } } }