Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @since 13 May 2017 * @version SVN_VERSION_NUMBER */ namespace IPS; /* To prevent PHP errors (extending class does not exist) revealing path */ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) ) { header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' ); exit; } /** * Login Handler */ class _Login { const LOGIN_FRONT = 1; const LOGIN_ACP = 2; const LOGIN_REGISTRATION_FORM = 3; const LOGIN_REAUTHENTICATE = 4; const LOGIN_UCP = 5; const AUTH_TYPE_USERNAME = 1; const AUTH_TYPE_EMAIL = 2; const TYPE_USERNAME_PASSWORD = 1; const TYPE_BUTTON = 2; /** * @brief URL */ public $url; /** * @brief Login form type */ public $type; /** * @brief Reauthenticating member */ public $reauthenticateAs; /** * Constructor * * @param \IPS\Http\Url $url The URL page for the login screen * @param \IPS\Http\Url $returnUrl The URL to send the user back to after login * @param int $type One of the LOGIN_* constants * @return void */ public function __construct( \IPS\Http\Url $url = NULL, $type=1 ) { $this->url = $url; $this->type = $type; if ( $type === static::LOGIN_REAUTHENTICATE or $type === static::LOGIN_UCP ) { $this->reauthenticateAs = \IPS\Member::loggedIn(); } } /* !Methods */ /** * Get methods * * @return array */ public static function methods() { if ( !isset( \IPS\Data\Store::i()->loginMethods ) ) { \IPS\Data\Store::i()->loginMethods = iterator_to_array( \IPS\Db::i()->select( '*', 'core_login_methods', array( 'login_enabled=1' ) )->setKeyField('login_id') ); } $return = array(); foreach ( \IPS\Data\Store::i()->loginMethods as $row ) { try { $return[ $row['login_id'] ] = \IPS\Login\Handler::constructFromData( $row ); } catch ( \OutOfRangeException $e ) { } } return $return; } /** * Get methods * * @return array */ protected function _methods() { $methods = array(); foreach ( static::methods() as $k => $method ) { if ( $this->type === static::LOGIN_FRONT or $this->type === static::LOGIN_UCP or ( $this->type === static::LOGIN_ACP and $method->acp ) or ( $this->type === static::LOGIN_REGISTRATION_FORM and $method->register ) or ( $this->type === static::LOGIN_REAUTHENTICATE and $method->canProcess( $this->reauthenticateAs ) and !( $method instanceof \IPS\Login\LoginAbstract ) ) ) { $methods[ $k ] = $method; } } return $methods; } /** * Get methods which use a username and password * * @return array */ public function usernamePasswordMethods() { $return = array(); foreach ( $this->_methods() as $method ) { if ( $method->type() === static::TYPE_USERNAME_PASSWORD ) { $return[ $method->id ] = $method; } } return $return; } /** * Should the username/password form ask for username or email address? * * @return array */ public function authType() { $authType = 0; foreach ( $this->usernamePasswordMethods() as $method ) { $authType = $authType | $method->authType(); } return $authType; } /** * Get methods which use a button * * @return array */ public function buttonMethods() { $return = array(); foreach ( $this->_methods() as $method ) { if ( $method->type() === static::TYPE_BUTTON ) { $return[ $method->id ] = $method; } } return $return; } /* !Authentication */ /** * Authenticate * * @param \IPS\Login\Handler\NULL $onlyCheck If provided, will only check the given method * @return \IPS\Login\Success|NULL * @throws \IPS\Login\Exception */ public function authenticate( \IPS\Login\Handler $onlyCheck = NULL ) { try { if ( isset( \IPS\Request::i()->_processLogin ) ) { \IPS\Session::i()->csrfCheck(); /* Username/Password */ if ( \IPS\Request::i()->_processLogin === 'usernamepassword' ) { $leastOffensiveException = NULL; $success = NULL; $fails = array(); foreach ( $this->usernamePasswordMethods() as $method ) { if ( !$onlyCheck or $method->id == $onlyCheck->id ) { try { if ( $this->type === static::LOGIN_REAUTHENTICATE ) { if ( $method->authenticatePasswordForMember( $this->reauthenticateAs, \IPS\Request::i()->password ) ) { $member = $this->reauthenticateAs; } else { throw new \IPS\Login\Exception( 'login_err_bad_password', \IPS\Login\Exception::BAD_PASSWORD, NULL, $this->reauthenticateAs ); } } else { $member = $method->authenticateUsernamePassword( $this, \IPS\Request::i()->auth, \IPS\Request::i()->password ); if ( $member === TRUE ) { $member = $this->reauthenticateAs; } } if ( $member ) { static::checkIfAccountIsLocked( $member, TRUE ); $success = new Login\Success( $member, $method, isset( \IPS\Request::i()->remember_me ), isset( \IPS\Request::i()->anonymous ) ); break; } } catch ( \IPS\Login\Exception $e ) { if ( $e->getCode() === \IPS\Login\Exception::BAD_PASSWORD and $e->member ) { $fails[ $e->member->member_id ] = $e->member; } if ( $leastOffensiveException === NULL or $leastOffensiveException->getCode() < $e->getCode() ) { $leastOffensiveException = $e; } } } } foreach ( $fails as $failedMember ) { if ( !$success or $success->member->member_id != $failedMember->member_id ) { $failedLogins = is_array( $failedMember->failed_logins ) ? $failedMember->failed_logins : array(); $failedLogins[ \IPS\Request::i()->ipAddress() ][] = time(); $failedMember->failed_logins = $failedLogins; $failedMember->save(); } } if ( $success ) { return $success; } elseif ( $leastOffensiveException ) { throw $leastOffensiveException; } else { throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::NO_ACCOUNT ); } } /* Buttons */ elseif ( isset( $this->buttonMethods()[ \IPS\Request::i()->_processLogin ] ) and ( !$onlyCheck or $onlyCheck->id == $this->buttonMethods()[ \IPS\Request::i()->_processLogin ]->id ) ) { $method = $this->_methods()[ \IPS\Request::i()->_processLogin ]; if ( $member = $method->authenticateButton( $this ) ) { if ( $this->type === static::LOGIN_REAUTHENTICATE and $member !== $this->reauthenticateAs ) { throw new \IPS\Login\Exception( 'login_err_wrong_account', \IPS\Login\Exception::BAD_PASSWORD ); } static::checkIfAccountIsLocked( $member, TRUE ); return new Login\Success( $member, $method ); } } } /* Backwards Compatibility for login handlers created before 4.3 */ elseif ( isset( \IPS\Request::i()->loginProcess ) ) { foreach ( $this->buttonMethods() as $method ) { if ( $method instanceof \IPS\Login\LoginAbstract and get_class( $method ) === 'IPS\Login\\' . mb_ucfirst( \IPS\Request::i()->loginProcess ) and ( !$onlyCheck or $method->id == $onlyCheck->id ) ) { if ( $member = $method->authenticateButton( $this ) ) { static::checkIfAccountIsLocked( $member, TRUE ); return new Login\Success( $member, $method, isset( \IPS\Request::i()->remember_me ), isset( \IPS\Request::i()->anonymous ) ); } } } } } catch ( \IPS\Login\Exception $e ) { /* If we're about to say the password is incorrect, check if the account is locked and throw that error rather than a bad password error first */ if ( $e->getCode() === \IPS\Login\Exception::BAD_PASSWORD and $e->member ) { static::checkIfAccountIsLocked( $e->member ); } /* If we're still here, throw the error we got */ throw $e; } } /* !Account Management Utility Methods */ /** * After authentication (successful or failed) but before * processing the login, check if the account is locked * * @param \IPS\Member $member The account * @param bool $succcess Boolean value indicating if the login was successfu. If TRUE, and the account is not locked, failed logins will be removed * @return void * @throws \Exception */ public static function checkIfAccountIsLocked( $member, $success = FALSE ) { $unlockTime = $member->unlockTime(); if ( $unlockTime !== FALSE ) { /* Notify the member if they've been locked */ if( count( $member->failed_logins[ \IPS\Request::i()->ipAddress() ] ) == \IPS\Settings::i()->ipb_bruteforce_attempts ) { /* Can we get a physical location */ try { $location = \IPS\GeoLocation::getByIp( \IPS\Request::i()->ipAddress() ); } catch ( \Exception $e ) { $location = \IPS\Request::i()->ipAddress(); } \IPS\Email::buildFromTemplate( 'core', 'account_locked', array( $member, $location, isset( $unlockTime ) ? $unlockTime : NULL ), \IPS\Email::TYPE_TRANSACTIONAL )->send( $member ); $member->logHistory( 'core', 'login', array( 'type' => 'lock', 'count' => count( $member->failed_logins[ \IPS\Request::i()->ipAddress() ] ), 'unlockTime' => isset( $unlockTime ) ? $unlockTime->getTimestamp() : NULL ) ); } if ( \IPS\Settings::i()->ipb_bruteforce_period and \IPS\Settings::i()->ipb_bruteforce_unlock ) { throw new \IPS\Login\Exception( \IPS\Member::loggedIn()->language()->addToStack( 'login_err_locked_unlock', FALSE, array( 'pluralize' => array( $unlockTime->diff( new DateTime() )->format('%i') ) ) ), \IPS\Login\Exception::ACCOUNT_LOCKED ); } else { throw new \IPS\Login\Exception( 'login_err_locked_nounlock', \IPS\Login\Exception::ACCOUNT_LOCKED ); } } elseif ( $success ) { $failedLogins = is_array( $member->failed_logins ) ? $member->failed_logins : array(); unset( $failedLogins[ \IPS\Request::i()->ipAddress() ] ); $member->failed_logins = $failedLogins; $member->save(); } } /** * Check if a given username is in use * Returns string with error message or FALSE if not in use * * @param string $username Desired username * @param \IPS\Member $exclude If provided, that member will be excluded from the check * @param \IPS\Member $admin Boolean value indicating if error message can include details about which login method has claimed it * @return string|false */ public static function usernameIsInUse( $username, $exclude = NULL, $admin = FALSE ) { /* Check locally */ $existingMember = \IPS\Member::load( $username, 'name' ); if ( $existingMember->member_id and ( !$exclude or $exclude->member_id != $existingMember->member_id ) ) { return \IPS\Member::loggedIn()->language()->addToStack('member_name_exists'); } /* Check each handler */ foreach( static::methods() as $k => $handler ) { if( $handler->usernameIsInUse( $username, $exclude ) === TRUE ) { if( $admin ) { return \IPS\Member::loggedIn()->language()->addToStack( 'member_name_exists_admin', FALSE, array('sprintf' => array( $handler->_title ) ) ); } else { return \IPS\Member::loggedIn()->language()->addToStack('member_name_exists'); } } } /* Still here? We're good */ return FALSE; } /** * Check if a given email address is in use * Returns string with error message or FALSE if not in use * * @param string $email Desired email address * @param \IPS\Member $exclude If provided, that member will be excluded from the check * @param \IPS\Member $admin Boolean value indicating if error message can include details about which login method has claimed it * @return string|false */ public static function emailIsInUse( $email, $exclude = NULL, $admin = FALSE ) { /* Check locally */ $existingMember = \IPS\Member::load( $email, 'email' ); if ( $existingMember->member_id and ( !$exclude or $exclude->member_id != $existingMember->member_id ) ) { return \IPS\Member::loggedIn()->language()->addToStack('member_email_exists'); } /* Check each handler */ foreach( static::methods() as $k => $handler ) { if( $handler->emailIsInUse( $email, $exclude ) === TRUE ) { if( $admin ) { return \IPS\Member::loggedIn()->language()->addToStack( 'member_email_exists_admin', FALSE, array('sprintf' => array( $handler->_title ) ) ); } else { return \IPS\Member::loggedIn()->language()->addToStack('member_email_exists'); } } } /* Still here? We're good */ return FALSE; } /* !Misc Utility Methods */ /** * Compare hashes in fixed length, time constant manner. * * @param string $expected The expected hash * @param string $provided The provided input * @return boolean */ public static function compareHashes( $expected, $provided ) { if ( !is_string( $expected ) || !is_string( $provided ) || $expected === '*0' || $expected === '*1' || $provided === '*0' || $provided === '*1' ) // *0 and *1 are failures from crypt() - if we have ended up with an invalid hash anywhere, we will reject it to prevent a possible vulnerability from deliberately generating invalid hashes { return FALSE; } $len = \strlen( $expected ); if ( $len !== \strlen( $provided ) ) { return FALSE; } $status = 0; for ( $i = 0; $i < $len; $i++ ) { $status |= ord( $expected[ $i ] ) ^ ord( $provided[ $i ] ); } return $status === 0; } /** * Return a random string * * @param int $length The length of the final string * @return string */ public static function generateRandomString( $length=32 ) { $return = ''; if ( function_exists( 'random_bytes' ) ) { $return = \substr( bin2hex( random_bytes( $length ) ), 0, $length ); } elseif( function_exists( 'openssl_random_pseudo_bytes' ) ) { $return = \substr( bin2hex( openssl_random_pseudo_bytes( ceil( $length / 2 ) ) ), 0, $length ); } /* Fallback JUST IN CASE */ if( !$return OR \strlen( $return ) != $length ) { $return = \substr( md5( uniqid( '', true ) ) . md5( uniqid( '', true ) ), 0, $length ); } return $return; } protected static $_registrationType = NULL; /** * Registration Type * * @return string */ public static function registrationType() { if ( static::$_registrationType === NULL ) { static::$_registrationType = \IPS\Settings::i()->allow_reg; if ( static::$_registrationType == 1 ) { static::$_registrationType = \IPS\Settings::i()->quick_register ? 'normal' : 'full'; } if ( in_array( static::$_registrationType, array( 'normal', 'full' ) ) and !\IPS\Login\Handler::findMethod( 'IPS\Login\Handler\Standard' ) ) { static::$_registrationType = 'disabled'; } } return static::$_registrationType; } /* !Backwards Compatibility for login handlers created before 4.3 */ /** * Get Legacy Handlers * * @deprecated * @return array */ public static function handlers() { $return = array(); foreach ( \IPS\Db::i()->select( '*', 'core_login_methods', array( 'login_enabled=1' ) ) as $method ) { $method = \IPS\Login\Handler::constructFromData( $method ); if ( $method instanceof \IPS\Login\LoginAbstract ) { $return[ mb_substr( get_class( $method ), 10 ) ] = $method; } } return $return; } }