PHP Classes

File: src/abstractclass/UserCredentialAbstract.php

Recommend this page to a friend!
  Classes of Cyril Ogana   PHP User Credentials   src/abstractclass/UserCredentialAbstract.php   Download  
File: src/abstractclass/UserCredentialAbstract.php
Role: Example script
Content type: text/plain
Description: Activate Github sync
Class: PHP User Credentials
Implement password authentication policies
Author: By
Last change: Updated phpass to v7.00
Require minimum PHP 7.2
Date: 4 years ago
Size: 45,799 bytes
 

Contents

Class file image Download
<?php namespace cymapgt\core\application\authentication\UserCredential\abstractclass; use cymapgt\Exception\UserCredentialException; use Phpass\Strength; /** * This package implements user password policy and credential management as well as * 2 factor authentication using TOTP * * @category security * @package cymapgt.core.application.authentication.UserCredential * @copyright Copyright (c) 2015 Cymap * @author Cyril Ogana <cogana@gmail.com> * @abstract * Exception@1025 * * - See http://www.owasp.org/images/0/08/OWASP_SCP_Quick_Reference_Guide_v2.pdf * (authentication section) */ abstract class UserCredentialAbstract { private $_userProfile = array(); //Array containing user information to use in the class private $_baseEntropySetting = array(); //This is the default entropy setting private $_baseEntropyOverride = false; //A flag to turn off base entropy enforcement private $_udfEntropySetting = array(); //A variable to store the user defined entropy private $_basePasswordPolicy = array(); //Base password policy maintained by UserCredential class private $_udfPasswordPolicy = array(); //Udf password policy input by the user /* * Constructor method * Cyril Ogana <cogana@gmail.com> - 2015-07-18 * * @param array userProfile - array of user credential information */ public function __construct(iterable $userProfile) { $this->_initialize($userProfile); } /** * Initialize the classes default settings (base entropy) * Cyril Ogana <cogana@gmail.com> - 2015-07-18 * * @param array /ArrayAccess userProfile * * @access private */ private function _initialize(iterable $userProfile) { $this->_initializeProfile($userProfile); $this->_initializeBaseEntropy(); $this->_initializeBasePasswordPolicy(); } /** * initializes the user profiles data as per the user credentials provided to the constructor method * * Cyril Ogana <cogana@gmail.com> - 2015-07-18 * * @param iterable $userProfile * * @access private */ private function _initializeProfile(iterable $userProfile) { //validate that user profile has the correct information for password validation if (!is_array($userProfile) || !isset($userProfile['username']) || !isset($userProfile['password']) || !isset($userProfile['fullname']) || !isset($userProfile['passhash']) || !is_string($userProfile['passhash']) || !isset($userProfile['passhist']) || !is_array($userProfile['passhist']) || !isset($userProfile['account_state']) || !isset($userProfile['policyinfo']) || !is_array($userProfile['policyinfo']) || !isset($userProfile['platforminfo']) || !is_array($userProfile['platforminfo']) ) { throw new UserCredentialException('The user profile is not properly initialized', 1000); } //validate tenancy is a datetime if (array_key_exists('tenancy_expiry', $userProfile['policyinfo'])) { $tenancyExpiry = $userProfile['policyinfo']['tenancy_expiry']; if (($tenancyExpiry instanceof \DateTime) === false) { throw new UserCredentialException('The user profile is not properly initialized', 1000); } } //set a blank TOTP profile if not set if (!isset($userProfile['totpinfo'])) { $userProfile['totpinfo'] = array(); } $this->_userProfile = $userProfile; } /** * Initialize entopy requirements to recommended default base entropy as per OWASP * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @access private */ private function _initializeBaseEntropy() { //initialize if not already initialized as array if (!is_array($this->_baseEntropySetting)) { $this->_baseEntropySetting = array(); } $this->_baseEntropySetting['min_pass_len'] = 8; //minimum password length $this->_baseEntropySetting['max_consecutive_chars'] = 2; //minimum characters to repeat consecutively $this->_baseEntropySetting['max_consecutive_chars_of_same_class'] = 10; $this->_baseEntropySetting['uppercase'] = array ( //requirement and length for various character types 'toggle' => true, 'min_len' => 2 ); $this->_baseEntropySetting['numeric'] = array( 'toggle' => true, 'min_len' => 1 ); $this->_baseEntropySetting['lowercase'] = array( 'toggle' => true, 'min_len' => 2 ); $this->_baseEntropySetting['special'] = array( 'toggle' => true, 'min_len' => 1 ); $this->_baseEntropySetting['multi_factor_on'] = false; //whether multi-factor auth is on or off $this->_baseEntropySetting['multi_factor_enc_key_length'] = 20; //length of encryption key generated when verifying token $this->_baseEntropyOverride = false; //override the reommended settings? $this->_setUdfEntropy($this->_baseEntropySetting); } /** * Initialize policy requirements to recommended default base entropy as per OWASP * Cyril Ogana <cogana@gmail.com> * 2014-02-11 * * @access private */ private function _initializeBasePasswordPolicy() { $this->_basePasswordPolicy['illegal_attempts_limit'] = 3; //count $this->_basePasswordPolicy['password_reset_frequency'] = 45; //days $this->_basePasswordPolicy['password_repeat_minimum'] = 5; //count $this->_basePasswordPolicy['illegal_attempts_penalty_seconds'] = 600; //seconds $this->_setUdfPasswordPolicy($this->_basePasswordPolicy); } /** * Get the base entropy data structure * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @return array * * @access protected * @final */ final protected function _getBaseEntropy(): array { if (isset($this->_baseEntropySetting)) { return $this->_baseEntropySetting; } } /** * Get the base password policy * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @return array * * @access protected * @final */ final protected function _getBasePasswordPolicy(): array { if (isset($this->_basePasswordPolicy)) { return $this->_basePasswordPolicy; } } /** * Set the user defined entropy setting * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @param iterable entropyObj - array or object implementing ArrayAccess * * @access protected * @final */ final protected function _setUdfEntropy(iterable $entropyObj) { //initialize if not already initialized as array if (!is_array($this->_udfEntropySetting) || (!is_object($this->_udfEntropySetting) && $this->_udfEntropySetting instanceof \ArrayAccess) ) { $this->_udfEntropySetting = array(); } //validate the array / object if (!is_array($entropyObj)) { if (!is_object($entropyObj) || !($entropyObj instanceof \ArrayAccess) ) { throw new UserCredentialException('The entropy object should be an array or implement ArrayAccess interface', 1001); } } //validate that minimum password len has the correct indices, then set it if (!isset($entropyObj['min_pass_len']) || !is_int($entropyObj['min_pass_len']) ) { throw new UserCredentialException('The minimum password length hasn\'t been set', 1002); } $this->_udfEntropySetting['min_pass_len'] = $entropyObj['min_pass_len']; //validate that minimum allowed password characters to repeat has been set if (!isset($entropyObj['max_consecutive_chars']) || !is_int($entropyObj['max_consecutive_chars']) ) { throw new UserCredentialException('The maximum allowed consecutive character repetition hasn\'t been set', 1003); } $this->_udfEntropySetting['max_consecutive_chars'] = $entropyObj['max_consecutive_chars']; //validate that minimum allowed password characters of same class to repeat has been set if (!isset($entropyObj['max_consecutive_chars_of_same_class']) || !is_int($entropyObj['max_consecutive_chars_of_same_class']) ) { throw new UserCredentialException('The maximum allowed consecutive character repetition for characters of the same class hasn\'t been set', 1026); } $this->_udfEntropySetting['max_consecutive_chars_of_same_class'] = $entropyObj['max_consecutive_chars_of_same_class']; //validate that uppercase snippet has correct indices, then set it if (!isset($entropyObj['uppercase']) || !is_array($entropyObj['uppercase']) || !isset($entropyObj['uppercase']['toggle']) || !is_bool($entropyObj['uppercase']['toggle']) || !isset($entropyObj['uppercase']['min_len']) || !is_int($entropyObj['uppercase']['min_len']) ) { throw new UserCredentialException('The uppercase settings must be an array containing toggle and min upper length', 1004); } $this->_udfEntropySetting['uppercase'] = $entropyObj['uppercase']; //validate that lowercase snippet has correct indices, then set it if (!isset($entropyObj['lowercase']) || !is_array($entropyObj['lowercase']) || !isset($entropyObj['lowercase']['toggle']) || !is_bool($entropyObj['lowercase']['toggle']) || !isset($entropyObj['lowercase']['min_len']) || !is_int($entropyObj['lowercase']['min_len']) ) { throw new UserCredentialException('The lowercase settings must be an array containing toggle and min lower length', 1005); } $this->_udfEntropySetting['lowercase'] = $entropyObj['lowercase']; //validate that numeric chars snippet has correct indices, then set it if (!isset($entropyObj['numeric']) || !is_array($entropyObj['numeric']) || !isset($entropyObj['numeric']['toggle']) || !is_bool($entropyObj['numeric']['toggle']) || !isset($entropyObj['numeric']['min_len']) || !is_int($entropyObj['numeric']['min_len']) ) { throw new UserCredentialException('The numeric settings must be an array containing toggle and min lower length', 1006); } $this->_udfEntropySetting['numeric'] = $entropyObj['numeric']; //validate that special chars snippet has correct indices, then set it if (!isset($entropyObj['special']) || !is_array($entropyObj['special']) || !isset($entropyObj['special']['toggle']) || !is_bool($entropyObj['special']['toggle']) || !isset($entropyObj['special']['min_len']) || !is_int($entropyObj['special']['min_len']) ) { throw new UserCredentialException('the special character settings must be an array containing toggle and min length', 1007); } $this->_udfEntropySetting['special'] = $entropyObj['special']; //in case we are using multi-factor, validate that all options are set if ( isset($entropyObj['multi_factor_on']) && $entropyObj['multi_factor_on'] === true ) { //encryption key length if ( !isset($entropyObj['multi_factor_enc_key_length']) || !(is_int($entropyObj['multi_factor_enc_key_length'])) || !($entropyObj['multi_factor_enc_key_length'] >= 16) ) { throw new UserCredentialException('Multi factor auth is flagged on, but the encryption key length is not properly initialized!', 1023); } $this->_udfEntropySetting['multi_factor_on'] = $entropyObj['multi_factor_on']; $this->_udfEntropySetting['multi_factor_enc_key_length'] = $entropyObj['multi_factor_enc_key_length']; } } /** * Set the user defined password policy * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @param iterable entropyObj - array or object implementing ArrayAccess * * @access protected * @final */ final protected function _setUdfPasswordPolicy(iterable $entropyObj) { //initialize if not already initialized as array if (!is_array($this->_udfPasswordPolicy) || (!is_object($this->_udfPasswordPolicy) && $this->_udfPasswordPolicy instanceof \ArrayAccess) ) { $this->_udfPasswordPolicy = array(); } //validate the array / object if (!is_array($entropyObj)) { if(!is_object($entropyObj) || !($entropyObj instanceof \ArrayAccess) ) { throw new UserCredentialException('The entropy object should be an array or implement ArrayAccess interface', 1001); } } //validate that illegal attempts limit has correct indices, then set it if (!isset($entropyObj['illegal_attempts_limit']) || !is_int($entropyObj['illegal_attempts_limit']) ) { throw new UserCredentialException('The illegal attempts limit hasn\'t been set', 1009); } $this->_udfPasswordPolicy['illegal_attempts_limit'] = $entropyObj['illegal_attempts_limit']; //validate that password reset frequency has correct indices, then set it if (!isset($entropyObj['password_reset_frequency']) || !is_int($entropyObj['password_reset_frequency']) ) { throw new UserCredentialException('The password reset frequency hasn\'t been set', 1010); } $this->_udfPasswordPolicy['password_reset_frequency'] = $entropyObj['password_reset_frequency']; //validate that passwordd repeat minimum has correct indices, then set it if (!isset($entropyObj['password_repeat_minimum']) || !is_int($entropyObj['password_repeat_minimum']) ) { throw new UserCredentialException('The password repeat minimum has not been set', 1011); } $this->_udfPasswordPolicy['password_repeat_minimum'] = $entropyObj['password_repeat_minimum']; //validate that password repeat minimum has correct indices, then set it if(!isset($entropyObj['illegal_attempts_penalty_seconds']) || !is_int($entropyObj['illegal_attempts_penalty_seconds']) ) { throw new UserCredentialException('The illegal attempts penalty seconds has not been set', 1012); } $this->_udfPasswordPolicy['illegal_attempts_penalty_seconds'] = $entropyObj['illegal_attempts_penalty_seconds']; } /** * Get the udf entropy data structure * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @return array * * @access protected * @final */ final protected function _getUdfEntropy(): array { if (isset($this->_udfEntropySetting)) { return $this->_udfEntropySetting; } } /** * Get the udf password policy * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @return iterable * * @access protected * @final */ final protected function _getUdfPasswordPolicy() { if (isset($this->_udfPasswordPolicy)) { return $this->_udfPasswordPolicy; } } /** * Set the value of base entropy toggle flag * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @param bool to toggle the attribute * * @return void * * @access protected * @final */ final protected function _setBaseEntropyOverride(bool $toggle) { if (isset($this->_baseEntropyOverride) && is_bool($toggle) ) { $this->_baseEntropyOverride = $toggle; } } /** * Get the current value of base entropy override attrib * Cyril Ogana <cogana@gmail.com> * 2013-07-18 * * @return bool * * @access protected * @final */ final protected function _getBaseEntropyOverride(): bool { if (isset($this->_baseEntropyOverride)) { return $this->_baseEntropyOverride; } } /** * build simple regex patterns based on particular entropy settings * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @param int patternCode - integer representing defined constants for variable code * @param int matchCount - integer representing the count of matched transactions * * @return string * * @access private */ private function _regexBuildPattern(int $patternCode, int $matchCount): string { $patternRegex = ''; switch ($patternCode) { case 1: $patternRegex = "(?=(?:.*[A-Z]){{$matchCount}})"; break; case 2: $patternRegex = "(?=(?:.*[a-z]){{$matchCount}})"; break; case 3: $patternRegex = "(?=(?:.*[0-9]){{$matchCount}})"; break; case 4: $patternRegex = '(?=(?:.*([-@%+\/\'!&#$^*?:.)(}{\[\]~_])){'.$matchCount.'})'; break; case 5: $patternRegex = '((.)\2}?(\2{'.$matchCount.'}))'; break; case 6: $patternRegex = '(([a-z]{'.$matchCount.'}|[A-Z]{'.$matchCount.'}|[0-9]{'.$matchCount.'}|[-@%+\/\'!#$&^*?:.)(}{\[\]~_]{'.$matchCount.'}))'; default: break; } if (!(isset($patternRegex))) { throw new UserCredentialException('The regex pattern is not set', 1013); } return $patternRegex; } /** * Get a description of the required password entropy * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @return string * * @access protected * @final */ final protected function _getPasswordEntropyDescription(): string { $entropyObj = $this->_getUdfEntropy(); $description = 'The password entropy requires at minimum, the following: '; $hasEntropy = false; $isFirstEntropy = false; $concatenator = ''; if ($entropyObj['lowercase']['min_len'] > 0) { $lowercaseLen = $entropyObj['lowercase']['min_len']; $description .= " at least $lowercaseLen lowercase characters"; $hasEntropy = true; $isFirstEntropy = true; } if ($entropyObj['uppercase']['min_len'] > 0) { $isFirstEntropy = $isFirstEntropy === true? false : true; $concatenator = $isFirstEntropy === true ? '' : ','; $uppercaseLen = $entropyObj['uppercase']['min_len']; $description .= "$concatenator at least $uppercaseLen uppercase characters"; $hasEntropy = true; } if ($entropyObj['numeric']['min_len'] > 0) { $isFirstEntropy = $isFirstEntropy === true ? false : true; $concatenator = $isFirstEntropy === true ? '' : ','; $numericLen = $entropyObj['numeric']['min_len']; $description .= "$concatenator at least $numericLen numeric characters"; $hasEntropy = true; } if ($entropyObj['special']['min_len'] > 0) { $isFirstEntropy = $isFirstEntropy === true ? false : true; $concatenator = $isFirstEntropy === true ? '' : ','; $specialLen = $entropyObj['special']['min_len']; $description .= "$concatenator at least $specialLen special characters"; $hasEntropy = true; } if (!($hasEntropy === true)) { $description = 'There is no minimum password entropy policy in place'; } return $description; } /** * Get a description of the required password entropy * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @return string * * @access protected * @final */ final protected function _getPasswordLengthDescription(): string{ $entropyObj = $this->_getUdfEntropy(); if ($entropyObj['min_pass_len'] > 0) { return "The minimum password length is {$entropyObj['min_pass_len']} characters"; } else { return 'There is no minimum password length policy in place'; } } /** * Get a description for the entropy policy regarding repeating a character consecutively * Cyril Ogana<cogana@gmail.com> * 2015-07-18 * * @return string * * @access protected * @final */ final protected function _getPasswordCharacterRepeatDescription(): string { $entropyObj = $this->_getUdfEntropy(); if ($entropyObj['max_consecutive_chars'] > 0) { return "The maximum allowed number of repeated characters in password of same type (e.g. aaa) is {$entropyObj['max_consecutive_chars']}"; } else { return "There is no maximum allowed number of repeated characters in password of the same type (e.g. aaa)"; } } /** * Get a description for the entropy policy regarding repeating a character class consecutively * Cyril Ogana<cogana@gmail.com> * 2016-11-30 * * @return string * * @access protected * @final */ final protected function _getPasswordCharacterClassRepeatDescription(): string { $entropyObj = $this->_getUdfEntropy(); if ($entropyObj['max_consecutive_chars_of_same_class'] > 0) { return "The maximum allowed number of repeated characters of the same class in password e.g aaaaBBBB1234 is {$entropyObj['max_consecutive_chars_of_same_class']}"; } else { return "There is no maximum allowed number of repeated characters of the same class e.g (aaaaBBBB1234"; } } /** * Get a description of the required password policy * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @param string policyType - The policy type for which we want a string description * * @return string * * @access protected * @final */ final protected function _getPasswordPolicyDescription(string $policyType): string{ $policyObj = $this->_getUdfPasswordPolicy(); switch ($policyType) { case 'illegal_attempts_limit': if ($policyObj['illegal_attempts_limit'] > 0) { return 'The illegal login attempts limit is '.$policyObj['illegal_attempts_limit']; } break; case 'password_reset_frequency': if ($policyObj['password_reset_frequency'] > 0) { return 'The password reset frequency is '.$policyObj['password_reset_frequency'].' days'; } break; case 'password_repeat_minimum': if ($policyObj['password_repeat_minimum'] > 0) { return 'A user is not allowed to repeat any of their last '.$policyObj['password_repeat_minimum'].' passwords'; } break; case 'illegal_attempts_penalty_seconds': if ($policyObj['illegal_attempts_penalty_seconds'] > 0) { return 'A user account will be temporarily locked out after the illegal login attempts limit for '.$policyObj['illegal_attempts_penalty_seconds'].' seconds; and will require admin intervention if the offense is repeated'; } break; default: break; } } /** * validate the entropy of the password in the userprofile * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @return bool * * @access protected * @final */ final protected function _validateEntropy(): bool{ //validate that required indices exist if (!isset($this->_userProfile['username']) || !isset($this->_userProfile['password']) || !isset($this->_userProfile['fullname']) || !isset($this->_userProfile['passhist']) ) { throw new UserCredentialException('The username and password are not set', 1016); } //validate that user is not using part of username as password (or reverse of it either) $namePartsArr = array(); $namePartsArr[] = strtolower($this->_userProfile['username']); $namePartsArr[] = strrev(strtolower($this->_userProfile['username'])); $namePartsArr[] = strtolower($this->_userProfile['fullname']); $namePartsArr[] = strrev(strtolower($this->_userProfile['fullname'])); $namePartsArr[] = strtolower(str_replace(' ', '', $this->_userProfile['fullname'])); $namePartsArr[] = strrev(strtolower(str_replace(' ', '', $this->_userProfile['fullname']))); $fullNameExploded = explode(' ', $this->_userProfile['fullname']); foreach ($fullNameExploded as $nameItem) { $namePartsArr[] = strtolower($nameItem); } $fullNameExplodedRev = explode(' ', strrev($this->_userProfile['fullname'])); foreach ($fullNameExplodedRev as $nameItemRev) { $namePartsArr[] = strtolower($nameItemRev); } //iterate and search for occurrences of name parts foreach ($namePartsArr as $namePart) { $namePartCast = (string) $namePart; if ((strpos(strtolower($this->_userProfile['password']), $namePartCast)) !== false) { throw new UserCredentialException('Password cannot contain username or any of your names (or reverse of either)', \USERCREDENTIAL_ACCOUNTPOLICY_NAMEINPASSWD); } } //set which entropy to use (base or udf) $entropyObj = $this->_udfEntropySetting; $validateCaseRegex = ''; $upperCaseRegex = ''; //build the password entropy regex uppercase if ($entropyObj['uppercase']['toggle'] === true) { //@TODO: Implement as constants the patterns $pattern = 1; $matchCount = ($entropyObj['uppercase']['min_len'] ? $entropyObj['uppercase']['min_len'] : 1); $upperCaseRegex = $this->_regexBuildPattern($pattern, $matchCount); } $lowerCaseRegex = ''; //build the password entropy regex lowercase if ($entropyObj['lowercase']['toggle'] === true) { $pattern = 2; $matchCount = ($entropyObj['lowercase']['min_len'] ? $entropyObj['lowercase']['min_len'] : 1); $lowerCaseRegex = $this->_regexBuildPattern($pattern,$matchCount); } $numericRegex = ''; //build the password entropy regex numbers if ($entropyObj['numeric']['toggle'] === true) { $pattern = 3; $matchCount = ($entropyObj['numeric']['min_len'] ? $entropyObj['numeric']['min_len'] : 1); $numericRegex = $this->_regexBuildPattern($pattern, $matchCount); } $specialRegex = ''; //build the password entropy regex special if ($entropyObj['special']['toggle'] === true) { $pattern = 4; $matchCount = ($entropyObj['special']['min_len'] ? $entropyObj['special']['min_len'] : 1); $specialRegex = $this->_regexBuildPattern($pattern, $matchCount); } //regex entropy string $validateCaseRegex = '/^'.$upperCaseRegex.$lowerCaseRegex.$numericRegex.$specialRegex.'/'; $testVal = preg_match($validateCaseRegex,$this->_userProfile['password']); if ($testVal === false) { throw new UserCredentialException('A fatal error occured in the password validation', 1015); } elseif ($testVal == false) { throw new UserCredentialException('The password does not meet the minimum entropy. '. $this->_getPasswordEntropyDescription(), \USERCREDENTIAL_ACCOUNTPOLICY_WEAKPASSWD); } else { return true; } } /** * Validate entropy of TOTP parameters in the profile * Cyril Ogana <cogana@gmail.com> * 2015-07-24 * * @return bool * * @access protected * @final */ final protected function _validateEntropyTotp(): bool { //in case we are using multi-factor, validate that all options are set if ( isset($this->_udfEntropySetting['multi_factor_on']) && $this->_udfEntropySetting['multi_factor_on'] === true ) { //encryption key length if ( !(count($this->_userProfile['totpinfo'])) || !(isset($this->_userProfile['totpinfo']['enc_key'])) ) { throw new UserCredentialException('TOTP info is not set in the users profile', 1024); } //validate length of encryption key $encKeyLength = $this->_udfEntropySetting['multi_factor_enc_key_length']; if (!strlen($this->_userProfile['totpinfo']['enc_key']) >= $encKeyLength) { throw new UserCredentialException('The encryption key string length for TOTP hashing is too short', 1025); } } return true; } /** * validate the password length of the users credentials * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @return bool * * @access protected * @final */ final protected function _validateLength(): bool { //validate that required indices exist if (!isset($this->_userProfile['username']) || !isset($this->_userProfile['password']) || !isset($this->_userProfile['fullname']) || !isset($this->_userProfile['passhist']) ) { throw new UserCredentialException('The username and password are not set', 1016); } //determine which entropy to use (base or udf) $entropyObj = $this->_udfEntropySetting; //perform length check if (strlen($this->_userProfile['password']) < $entropyObj['min_pass_len']) { throw new UserCredentialException('The password does not meet required length. '.$this->_getPasswordLengthDescription(), \USERCREDENTIAL_ACCOUNTPOLICY_WEAKPASSWD); } return true; } /** * validate that there are no instances of consecutive character repetitions beyond allowed number * in the users password string * * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @return bool * * @access protected * @final */ final protected function _validateConsecutiveCharacterRepeat(): bool { //validate that required indices exist if (!isset($this->_userProfile['username']) || !isset($this->_userProfile['password']) || !isset($this->_userProfile['fullname']) || !isset($this->_userProfile['passhist']) ) { throw new UserCredentialException('The username and password are not set', 1016); } //FOR CHARACTER REPETITION //determine which entropy to use (base or udf) $entropyObj = $this->_udfEntropySetting; $maxConsecutiveChars = (int) ($entropyObj['max_consecutive_chars']); //because we offset by -2 when doing regex, if the limit is not greater or equal to 2, default to 2 if (!($maxConsecutiveChars >= 2)) { $maxConsecutiveChars = 2; } //offset for purposes of matching (TODO: fix?) $maxConsecutiveCharsRegexOffset = ++$maxConsecutiveChars - 2; //build regex $maxConsecutiveCharsRegex = '/' . $this->_regexBuildPattern(5, $maxConsecutiveCharsRegexOffset) . '/'; $testVal = preg_match($maxConsecutiveCharsRegex,$this->_userProfile['password']); if ($testVal === false) { throw new UserCredentialException('A fatal error occured in the password validation', 1015); } elseif ($testVal == true) { throw new UserCredentialException('The password violates policy about consecutive character repetitions. '. $this->_getPasswordCharacterRepeatDescription(), \USERCREDENTIAL_ACCOUNTPOLICY_WEAKPASSWD); } else {/*Do nothing*/} //FOR CHARACTER CLASS REPETITION //determine which entropy to use (base or udf) $maxConsecutiveCharsSameClass = (int) ($entropyObj['max_consecutive_chars_of_same_class']); //because we offset by -2 when doing regex, if the limit is not greater or equal to 2, default to 2 if (!($maxConsecutiveCharsSameClass >= 2)) { $maxConsecutiveCharsSameClass = 2; } //offset for purposes of matching (TODO: fix?) $maxConsecutiveCharsSameClassRegexOffset = ++$maxConsecutiveCharsSameClass; //build regex $maxConsecutiveCharsSameClassRegex = '/' . $this->_regexBuildPattern(6, $maxConsecutiveCharsSameClassRegexOffset) . '/'; $testValSameClass = preg_match($maxConsecutiveCharsSameClassRegex,$this->_userProfile['password']); if ($testValSameClass === false) { throw new UserCredentialException('A fatal error occured in the password validation', 1015); } elseif ($testValSameClass == true) { throw new UserCredentialException('The password violates policy about consecutive repetition of characters of the same class. '. $this->_getPasswordCharacterClassRepeatDescription(), \USERCREDENTIAL_ACCOUNTPOLICY_WEAKPASSWD); } else { return true; } return true; } /** * validate the password policy during authentication * * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @return bool * * @access protected * @final */ final protected function _validatePolicy(): bool { //validate that required indices exist if (!isset($this->_userProfile['username']) || !isset($this->_userProfile['password']) || !isset($this->_userProfile['fullname']) || !isset($this->_userProfile['passhist']) ) { throw new UserCredentialException('The username and password are not set', 1016); } //determine which entropy to use (base or udf) $policyObj = $this->_udfPasswordPolicy; //check attempt limits if ($this->_userProfile['account_state'] == \USERCREDENTIAL_ACCOUNTSTATE_AUTHFAILED) { if ($this->_userProfile['policyinfo']['failed_attempt_count'] > $policyObj['illegal_attempts_limit']) { throw new UserCredentialException('The account has exceeded login attempts and is locked. Contact admin', \USERCREDENTIAL_ACCOUNTPOLICY_ATTEMPTLIMIT2); } elseif ($this->_userProfile['policyinfo']['failed_attempt_count'] == $policyObj['illegal_attempts_limit']) { throw new UserCredentialException('The account has failed login '.(++$policyObj['illegal_attempts_limit']).' times in a row and is temporarily locked. Any further wrong passwords will lead to your account being locked fully. You will be automatically unlocked in '.(($policyObj['illegal_attempts_penalty_seconds']) / 60).' minutes or contact admin to unlock immediately', \USERCREDENTIAL_ACCOUNTPOLICY_ATTEMPTLIMIT1); } else { throw new UserCredentialException('Login failed. Wrong username or password', \USERCREDENTIAL_ACCOUNTPOLICY_VALID); } } //check needs reset $currDateTimeObj = new \DateTime(); $passChangeDaysElapsedObj = $currDateTimeObj->diff($this->_userProfile['policyinfo']['password_last_changed_datetime']); $passChangeDaysElapsed = $passChangeDaysElapsedObj->format('%a'); if ($passChangeDaysElapsed > $policyObj['password_reset_frequency']) { throw new UserCredentialException('The password has expired and must be changed', \USERCREDENTIAL_ACCOUNTPOLICY_EXPIRED); } return true; } /** * validate the password policy during process of making a password change * * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @return bool * * @access protected * @final */ final protected function _validatePolicyAtChange(): bool { //validate that required indices exist if (!isset($this->_userProfile['username']) || !isset($this->_userProfile['password']) || !isset($this->_userProfile['fullname']) || !isset($this->_userProfile['passhist']) ) { throw new UserCredentialException('The username and password are not set', 1016); } //determine which entropy to use (base or udf) $policyObj = $this->_udfPasswordPolicy; //check password repeat $passHistory = $this->_userProfile['passhist']; $passHistoryRequired = array_slice($passHistory, 0, ((int) $policyObj['password_repeat_minimum'])); //iterate and verify foreach ($passHistoryRequired as $passHistoryItem) { if (password_verify($this->_userProfile['password'], $passHistoryItem)) { throw new UserCredentialException('User cannot repeat any of their ' . $policyObj['password_repeat_minimum'] . ' last passwords', \USERCREDENTIAL_ACCOUNTPOLICY_REPEATERROR); } } return true; } /** * Check that a user can change password in case you want to implement limits on changing passwords * only once in 24 hours * * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * * @return bool * * @access protected * @final */ final protected function _canChangePassword(): bool { //validate that required indices exist if (!isset($this->_userProfile['username']) || !isset($this->_userProfile['password']) || !isset($this->_userProfile['fullname']) || !isset($this->_userProfile['passhist']) ) { throw new UserCredentialException('The username and password are not set', 1016); } //Verify if the password was changed today or server has been futuredated $currDateTimeObj = new \DateTime(); //Password was changed today or in the future if ($currDateTimeObj <= $this->_userProfile['policyinfo']['password_last_changed_datetime']) { return false; } else { return true; } } /** * validate the tenancy of an account. This can be preset by the system admin so that accounts that * are past tenancy date are automatically not allowed to authenticate. Tenancy should be validated * after other policies to avoid farming of accounts by testing which ones are still in tenancy * * Cyril Ogana <cogana@gmail.com> * 2018-04-25 * * @return bool * * @access protected * @final */ final protected function _validateTenancy(): bool { $userProfile = $this->_userProfile; //Verify if the password was changed today or server has been futuredated $currDateTimeObj = new \DateTime(); //if account has tenancy expiry, deny login if user account tenancy is past if (array_key_exists('tenancy_expiry', $userProfile['policyinfo'])) { $tenancyExpiry = $userProfile['policyinfo']['tenancy_expiry']; if ($currDateTimeObj > $tenancyExpiry) { throw new UserCredentialException('Tenancy problem with your account. Please contact your Administrator'); } } return true; } /** * Check password strength using NIST Or Wolfram adapter (default NIST) * See https://github.com/rchouinard/phpass * Many thanks to Ryan Chouinard for the phpass package * Cyril Ogana <cogana@gmail.com> * 2015-07-18 * @param string $passwordString - The password string to evaluate * @param int $strengthAdapter - Named constant representing adapter to use (default NIST) * * @return int * * @access public * @static */ public static function passwordStrength(string $passwordString, int $strengthAdapter = \PHPASS_PASSWORDSTRENGTHADAPTER_NIST): int { if ($strengthAdapter == \PHPASS_PASSWORDSTRENGTHADAPTER_WOLFRAM) { $strengthAdapter = new Strength\Adapter\Wolfram; } elseif ($strengthAdapter == \PHPASS_PASSWORDSTRENGTHADAPTER_NIST) { $strengthAdapter = new Strength\Adapter\Nist; } else { throw new UserCredentialException('Phpass strength adapter calculator must be NIST or Wolfram. Wrong Flag provivded.', 1022); } $phpassStrength = new Strength($strengthAdapter); return $phpassStrength->calculate($passwordString); } /** * Return a cyprographically strong random string of required length * Cyril Ogana <cogana@gmail.com> * 2015-07-24 * * @param int keyLength - Length of the random key * * @return string * * @access public * @static */ public static function generateRandomKey(int $keyLength): string { if ( !(is_int($keyLength)) && !($keyLength > 0) ) { throw new UserCredentialException('Key length for random key must be a positive integer', 1017); } return openssl_random_pseudo_bytes($keyLength); } /** * Generate a 6 digit SMS token * Cyril Ogana <cogana@gmail.com> * 2015-07-24 * * @return string * * @param string $userName - The username * @param string $keyString - String to use as a salt * * @access public * @static */ public static function generateToken(string $userName, string $keyString): string { $userNameCast = (string) $userName; $keyStringCast = (string) $keyString; $multiOtpObj = new MultiotpWrapper($keyStringCast); return $multiOtpObj->GenerateSmsToken($userNameCast); } /** * Abstract methods for concrete implementation */ abstract public function getBaseEntropy(): array; abstract public function getBaseEntropyOverride(): bool; abstract public function getBasePasswordPolicy(): array; abstract public function getPasswordEntropyDescription(): string; abstract public function getPasswordLengthDescription(): string; abstract public function getPasswordPolicyDescription(string $policyType): string; abstract public function getUdfEntropy(): array; abstract public function getUdfPasswordPolicy(): array; abstract public function setBaseEntropyOverride(bool $toggle); abstract public function setUdfEntropy(array $entropyObj); abstract public function setUdfPasswordPolicy(array $entropyObj); abstract public function validateEntropy(): bool; abstract public function validateEntropyTotp(): bool; abstract public function validateLength(): bool; abstract public function validateConsecutiveCharacterRepeat(): bool; abstract public function validatePolicy(): bool; abstract public function validatePolicyAtChange(): bool; abstract public function validateTenancy(): bool; abstract public function canChangePassword(): bool; }