Seditio Source
Root |
./othercms/ips_4.3.4/system/MFA/MFAHandler.php
<?php
/**
 * @brief        Abstract Multi Factor Authentication Handler and Factory
 * @author        <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
 * @copyright    (c) Invision Power Services, Inc.
 * @license        https://www.invisioncommunity.com/legal/standards/
 * @package        Invision Community
 * @since        26 Aug 2016
 */

namespace IPS\MFA;

/* 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;
}

/**
 * Abstract Multi Factor Authentication Handler and Factory
 */
abstract class _MFAHandler
{
   
/* !Access Methods */
   
    /**
     * Get areas
     *
     * @return    array
     */
   
public static function areas()
    {
       
$return = array();
        foreach ( \
IPS\Application::allExtensions( 'core', 'MFAArea', FALSE, 'core', NULL, FALSE ) as $k => $v )
        {
           
$return[ $k ] = "MFA_{$k}";
        }
        return
$return;
    }
   
   
/**
     * Get handlers
     *
     * @return    array
     */
   
public static function handlers()
    {
        return array(
           
'authy'        => new \IPS\MFA\Authy\Handler(),
           
'google'    => new \IPS\MFA\GoogleAuthenticator\Handler(),
           
'questions'    => new \IPS\MFA\SecurityQuestions\Handler()
        );
    }
       
   
/**
     * Display output when trying to access an area
     *
     * @param    string            $app        The application which owns the MFAArea extension
     * @param    string            $area        The MFAArea key
     * @param    \IPS\Http\Url    $url        URL for page
     * @param    \IPS\Member        $member        The member, or NULL for currently logged in member
     * @return    string
     */
   
public static function accessToArea( $app, $area, \IPS\Http\Url $url, \IPS\Member $member = NULL )
    {
       
$member = $member ?: \IPS\Member::loggedIn();
       
       
/* Constant to disable MFA for emergency recovery */
       
if ( \IPS\DISABLE_MFA )
        {
            return
NULL;
        }
       
       
/* If MFA is not enabled for this area, do nothing */
       
if ( !\IPS\Settings::i()->security_questions_areas or !in_array( "{$app}_{$area}", explode( ',', \IPS\Settings::i()->security_questions_areas ) ) )
        {
            return
NULL;
        }
               
       
/* Are we already authenticated? */
       
if ( !\IPS\DEV_FORCE_MFA and ( isset( $_SESSION['MFAAuthenticated'] ) and ( !\IPS\Settings::i()->security_questions_timer or ( ( $_SESSION['MFAAuthenticated'] + ( \IPS\Settings::i()->security_questions_timer * 60 ) ) > time() ) ) ) )
        {
            return
NULL;
        }
       
       
/* "Opt Out" */
       
if ( \IPS\Settings::i()->mfa_required_groups != '*' and !$member->inGroup( explode( ',', \IPS\Settings::i()->mfa_required_groups ) ) )
        {
            if (
$member->members_bitoptions['security_questions_opt_out'] )
            {
                return
NULL;
            }
            if ( isset( \
IPS\Request::i()->_mfa ) and \IPS\Request::i()->_mfa == 'optout' )
            {
                \
IPS\Session::i()->csrfCheck();
               
               
$member->members_bitoptions['security_questions_opt_out'] = TRUE;
               
$member->save();

               
/* Log MFA Optout */
               
$member->logHistory( 'core', 'mfa', array( 'handler' => 'questions', 'enable' => FALSE, 'optout' => TRUE ) );

                return
NULL;
            }
        }
       
       
/* Gather all the one we *can* use */
       
$acceptableHandlers = array();
        foreach ( static::
handlers() as $key => $handler )
        {
           
/* If it's enabled and we can use it... */
           
if ( $handler->isEnabled() and $handler->memberCanUseHandler( $member ) )
            {
               
$acceptableHandlers[ $key ] = $handler;
            }
        }
        if ( !
$acceptableHandlers )
        {
            return
NULL;
        }
       
       
/* Locked out? */
       
if ( $lockedOutScreen = static::_lockedOutScreen( $member ) )
        {
            \
IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( '2fa.css', 'core', 'global' ) );
            return
$lockedOutScreen;
        }
               
       
/* "Try another way to sign in" */
       
if ( isset( \IPS\Request::i()->_mfa ) )
        {
            \
IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( '2fa.css', 'core', 'global' ) );
            if ( \
IPS\Request::i()->_mfa == 'alt' )
            {            
               
/* What handlers have we configured? */
               
$configuredHandlers = array();
                foreach (
$acceptableHandlers as $key => $handler )
                {
                    if (
$handler->memberHasConfiguredHandler( $member ) )
                    {
                       
$configuredHandlers[ $key ] = $handler;
                    }
                }
               
                if ( isset( \
IPS\Request::i()->_mfaMethod ) and array_key_exists( \IPS\Request::i()->_mfaMethod, $configuredHandlers ) )
                {
                    return static::
_showHandlerAuthScreen( $configuredHandlers[ \IPS\Request::i()->_mfaMethod ], $url->setQueryString( array( '_mfa' => 'alt', '_mfaMethod' => \IPS\Request::i()->_mfaMethod ) ), $member );
                }
               
               
/* Display */
               
$knownDevicesAvailable = FALSE;
                if (
$app === 'core' and $area === 'AuthenticateFront' and !in_array( "app_AuthenticateFrontKnown", explode( ',', \IPS\Settings::i()->security_questions_areas ) ) )
                {
                    if ( \
IPS\Db::i()->select( 'COUNT(*)', 'core_members_known_devices', array( 'member_id=?', $member->member_id ) )->first() )
                    {
                       
$knownDevicesAvailable = TRUE;
                    }
                }
               
                return \
IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->mfaRecovery( $configuredHandlers, $url, $knownDevicesAvailable );
            }
            elseif ( \
IPS\Request::i()->_mfa == 'knownDevice' )
            {
                return \
IPS\Theme::i()->getTemplate( 'system', 'core' )->mfaKnownDeviceInfo( $url );
            }
        }
                       
       
/* Normal authentication form */
       
foreach ( $acceptableHandlers as $handler )
        {
            if (
$handler->memberHasConfiguredHandler( $member ) )
            {
                \
IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( '2fa.css', 'core', 'global' ) );
                return static::
_showHandlerAuthScreen( $handler, $url, $member );
            }
        }
       
       
/* Setup form */
       
$showSetupForm = ( \IPS\Settings::i()->mfa_required_groups == '*' or $member->inGroup( explode( ',', \IPS\Settings::i()->mfa_required_groups ) ) ) ? 'mfa_required_prompt' : 'mfa_optional_prompt';
        if ( \
IPS\Settings::i()->$showSetupForm === 'access' or ( $app === 'core' and $area === 'AuthenticateAdmin' and \IPS\Settings::i()->$showSetupForm === 'immediate' ) )
        {
            \
IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( '2fa.css', 'core', 'global' ) );
           
           
/* Did we just submit it? */
           
if ( isset( \IPS\Request::i()->mfa_setup ) and isset( \IPS\Request::i()->csrfKey ) )
            {
                \
IPS\Session::i()->csrfCheck();
                foreach (
$acceptableHandlers as $key => $handler )
                {
                    if ( (
count( $acceptableHandlers ) == 1 ) or $key == \IPS\Request::i()->mfa_method )
                    {
                        if (
$handler->configurationScreenSubmit( $member ) )
                        {                            
                           
$_SESSION['MFAAuthenticated'] = time();
                            return
NULL;
                        }
                        elseif (
$lockedOutScreen = static::_lockedOutScreen( $member ) )
                        {
                            return
$lockedOutScreen;
                        }
                    }
                }
            }
           
           
/* No, show it */            
           
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->mfaSetup( $acceptableHandlers, $member, $url );
        }
    }
   
   
/**
     * Show a handler's authentication screen
     *
     * @param    \IPS\MFA\MFAHandler    $handler    The handler to use
     * @param    \IPS\Http\Url        $url        URL for page
     * @param    \IPS\Member            $member        The member
     * @return    string|null
     */
   
protected static function _showHandlerAuthScreen( \IPS\MFA\MFAHandler $handler, \IPS\Http\Url $url, \IPS\Member $member )
    {
       
/* Did we just submit it? */
       
if ( isset( \IPS\Request::i()->mfa_auth ) )
        {
            \
IPS\Session::i()->csrfCheck();
            if (
$handler->authenticationScreenSubmit( $member ) )
            {
               
$member->failed_mfa_attempts = 0;
               
$member->save();
               
$_SESSION['MFAAuthenticated'] = time();
                return
NULL;
            }
            else
            {
               
$member->failed_mfa_attempts++;
               
$member->save();
               
                \
IPS\Request::i()->mfa_auth = NULL;
            }
        }
       
       
/* No, show it */
       
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->mfaAuthenticate( $handler->authenticationScreen( $member, $url ), $url );
    }
   
   
/**
     * Show the locked out screen, if necessary
     *
     * @param    \IPS\Member            $member        The member
     * @return    string|null
     */
   
protected static function _lockedOutScreen( \IPS\Member $member )
    {
        if (
$member->failed_mfa_attempts >= \IPS\Settings::i()->security_questions_tries )
        {
            if ( \
IPS\Settings::i()->mfa_lockout_behaviour == 'lock' )
            {
               
$mfaDetails = $member->mfa_details;
                if ( !isset(
$mfaDetails['_lockouttime'] ) )
                {
                   
$mfaDetails['_lockouttime'] = time();
                   
$member->mfa_details = $mfaDetails;
                   
$member->save();
                   
                   
$member->logHistory( 'core', 'login', array( 'type' => 'mfalock', 'count' => $member->failed_mfa_attempts, 'unlockTime' => \IPS\DateTime::create()->add( new \DateInterval( 'PT' . \IPS\Settings::i()->mfa_lockout_time . 'M' ) )->getTimestamp() ) );
                }
                               
               
$lockEndTime = \IPS\DateTime::ts( $mfaDetails['_lockouttime'] )->add( new \DateInterval( 'PT' . \IPS\Settings::i()->mfa_lockout_time . 'M' ) );
                if (
$lockEndTime->getTimestamp() < time() )
                {
                    unset(
$mfaDetails['_lockouttime'] );
                   
$member->mfa_details = $mfaDetails;
                   
$member->failed_mfa_attempts = 0;
                   
$member->save();
                }
                else
                {                    
                    return \
IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->mfaLockout( ( \IPS\Settings::i()->mfa_lockout_time > 1440 ) ? $lockEndTime : $lockEndTime->localeTime( FALSE ) );
                }
            }
            else
            {
                if (
$member->failed_mfa_attempts == \IPS\Settings::i()->security_questions_tries )
                {
                   
$member->logHistory( 'core', 'login', array( 'type' => 'mfalock', 'count' => $member->failed_mfa_attempts ) );
                }
                return \
IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->mfaLockout();
            }
        }
    }
   
   
/* !Setup */
   
    /**
     * Handler is enabled
     *
     * @return    bool
     */
   
abstract public function isEnabled();
   
   
/**
     * Member *can* use this handler (even if they have not yet configured it)
     *
     * @param    \IPS\Member    $member    The member
     * @return    bool
     */
   
abstract public function memberCanUseHandler( \IPS\Member $member );
   
   
/**
     * Member has configured this handler
     *
     * @param    \IPS\Member    $member    The member
     * @return    bool
     */
   
abstract public function memberHasConfiguredHandler( \IPS\Member $member );
       
   
/**
     * Show a setup screen
     *
     * @param    \IPS\Member        $member                        The member
     * @param    bool            $showingMultipleHandlers    Set to TRUE if multiple options are being displayed
     * @param    \IPS\Http\Url    $url                        URL for page
     * @return    string
     */
   
abstract public function configurationScreen( \IPS\Member $member, $showingMultipleHandlers=FALSE, \IPS\Http\Url $url );
   
   
/**
     * Submit configuration screen. Return TRUE if was accepted
     *
     * @param    \IPS\Member        $member    The member
     * @return    bool
     */
   
abstract public function configurationScreenSubmit( \IPS\Member $member );
   
   
/* !Authentication */
   
    /**
     * Get the form for a member to authenticate
     *
     * @param    \IPS\Member        $member        The member
     * @param    \IPS\Http\Url    $url        URL for page
     * @return    string
     */
   
abstract public function authenticationScreen( \IPS\Member $member, \IPS\Http\Url $url );
   
   
/**
     * Submit authentication screen. Return TRUE if was accepted
     *
     * @param    \IPS\Member        $member    The member
     * @return    string
     */
   
abstract public function authenticationScreenSubmit( \IPS\Member $member );
   
   
/* !ACP */
   
    /**
     * Toggle
     *
     * @param    bool    $enabled    On/Off
     * @return    bool
     */
   
abstract public function toggle( $enabled );
   
   
/**
     * ACP Settings
     *
     * @return    string
     */
   
abstract public function acpSettings();
   
   
/**
     * Configuration options when editing member account in ACP
     *
     * @param    \IPS\Member            $member        The member
     * @param    \IPS\Helpers\Form    $form        Set to TRUE if multiple options are being displayed
     * @return    array
     */
   
public function acpConfiguration( \IPS\Member $member )
    {
        if (
$this->memberHasConfiguredHandler( $member ) )
        {
            return array( new \
IPS\Helpers\Form\YesNo( "mfa_{$this->key}_title", $this->memberHasConfiguredHandler( $member ), FALSE, array(), NULL, NULL, NULL, "mfa_{$this->key}_title" ) );
        }
        return array();
    }
   
   
/**
     * Save configuration when editing member account in ACP
     *
     * @param    \IPS\Member        $member        The member
     * @param    array            $values        Values from form
     * @return    array
     */
   
public function acpConfigurationSave( \IPS\Member $member, $values )
    {
        if ( isset(
$values["mfa_{$this->key}_title"] ) and !$values["mfa_{$this->key}_title"] )
        {
            if ( isset(
$member->mfa_details[ $this->key ] ) and $this->memberHasConfiguredHandler( $member ) )
            {
               
$this->disableHandlerForMember( $member );
            }
        }
    }
   
   
/* !Misc */
   
    /**
     * If member has configured this handler, disable it
     *
     * @param    \IPS\Member    $member    The member
     * @return    bool
     */
   
abstract public function disableHandlerForMember( \IPS\Member $member );
   
   
/**
     * Get title for UCP
     *
     * @return    string
     */
   
public function ucpTitle()
    {
        return \
IPS\Member::loggedIn()->language()->addToStack("mfa_{$this->key}_title");
    }
   
   
/**
     * Get description for UCP
     *
     * @return    string
     */
   
public function ucpDesc()
    {
        return \
IPS\Member::loggedIn()->language()->addToStack("mfa_{$this->key}_desc_user");
    }
   
   
/**
     * Get label for recovery button
     *
     * @return    string
     */
   
public function recoveryButton()
    {
        return \
IPS\Member::loggedIn()->language()->addToStack("mfa_recovery_{$this->key}");
    }
   
}