Version 5.0.0 beta 1
This commit is contained in:
1 parent
25ddeb65d6
commit
15c7beabc5
6736 files changed
+627902
-497943
No files matched your search
@@ -11,16 +11,20 @@
|
||||
namespace IPS\MFA\Authy;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use DomainException;
|
||||
use function defined;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authy API Exception
|
||||
*/
|
||||
class _Exception extends \DomainException
|
||||
class Exception extends DomainException
|
||||
{
|
||||
/**
|
||||
* @brief Token reused
|
||||
@@ -47,7 +51,7 @@ class _Exception extends \DomainException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserMessage()
|
||||
public function getUserMessage(): string
|
||||
{
|
||||
switch ( $this->getCode() )
|
||||
{
|
||||
|
||||
+199
-174
@@ -11,21 +11,46 @@
|
||||
namespace IPS\MFA\Authy;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use DomainException;
|
||||
use IPS\DateTime;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Address;
|
||||
use IPS\Helpers\Form\CheckboxSet;
|
||||
use IPS\Helpers\Form\Custom;
|
||||
use IPS\Helpers\Form\Radio;
|
||||
use IPS\Helpers\Form\Text;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Log;
|
||||
use IPS\Member;
|
||||
use IPS\Member\Group;
|
||||
use IPS\MFA\Authy\Exception as AuthyException;
|
||||
use IPS\MFA\MFAHandler;
|
||||
use IPS\Output;
|
||||
use IPS\Request;
|
||||
use IPS\Session;
|
||||
use IPS\Settings;
|
||||
use IPS\Theme;
|
||||
use function count;
|
||||
use function defined;
|
||||
use function in_array;
|
||||
use function intval;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi Factor Authentication Handler for Authy
|
||||
*/
|
||||
class _Handler extends \IPS\MFA\MFAHandler
|
||||
class Handler extends MFAHandler
|
||||
{
|
||||
/**
|
||||
* @brief Key
|
||||
*/
|
||||
protected $key = 'authy';
|
||||
protected string $key = 'authy';
|
||||
|
||||
/* !Setup */
|
||||
|
||||
@@ -34,29 +59,29 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled()
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return \IPS\Settings::i()->authy_enabled;
|
||||
return Settings::i()->authy_enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Member *can* use this handler (even if they have not yet configured it)
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function memberCanUseHandler( \IPS\Member $member )
|
||||
public function memberCanUseHandler( Member $member ): bool
|
||||
{
|
||||
return \IPS\Settings::i()->authy_groups == '*' or $member->inGroup( explode( ',', \IPS\Settings::i()->authy_groups ) );
|
||||
return Settings::i()->authy_groups == '*' or $member->inGroup( explode( ',', Settings::i()->authy_groups ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Member has configured this handler
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function memberHasConfiguredHandler( \IPS\Member $member )
|
||||
public function memberHasConfiguredHandler( Member $member ): bool
|
||||
{
|
||||
return isset( $member->mfa_details['authy'] ) and $member->mfa_details['authy']['setup'];
|
||||
}
|
||||
@@ -64,17 +89,17 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* Show a setup screen
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @param bool $showingMultipleHandlers Set to TRUE if multiple options are being displayed
|
||||
* @param \IPS\Http\Url $url URL for page
|
||||
* @param Url $url URL for page
|
||||
* @return string
|
||||
*/
|
||||
public function configurationScreen( \IPS\Member $member, $showingMultipleHandlers, \IPS\Http\Url $url )
|
||||
public function configurationScreen( Member $member, bool $showingMultipleHandlers, Url $url ): string
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
|
||||
/* Starting again? */
|
||||
if ( isset( $mfaDetails['authy']['pendingId'] ) and isset( \IPS\Request::i()->_new ) )
|
||||
if ( isset( $mfaDetails['authy']['pendingId'] ) and isset( Request::i()->_new ) )
|
||||
{
|
||||
unset( $mfaDetails['authy']['pendingId'] );
|
||||
$member->mfa_details = $mfaDetails;
|
||||
@@ -85,42 +110,42 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
if ( isset( $mfaDetails['authy'] ) and isset( $mfaDetails['authy']['pendingId'] ) and !isset( $_SESSION['authyConfigureError'] ) )
|
||||
{
|
||||
/* Asking for a text or call instead? */
|
||||
$availableMethods = explode( ',', \IPS\Settings::i()->authy_setup );
|
||||
if ( isset( \IPS\Request::i()->authy_method ) and $mfaDetails['authy']['setupMethod'] == 'authy' and \in_array( \IPS\Request::i()->authy_method, $availableMethods ) )
|
||||
$availableMethods = explode( ',', Settings::i()->authy_setup );
|
||||
if ( isset( Request::i()->authy_method ) and $mfaDetails['authy']['setupMethod'] == 'authy' and in_array( Request::i()->authy_method, $availableMethods ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
/* Send text or make call */
|
||||
if ( \IPS\Request::i()->authy_method == 'phone' )
|
||||
if ( Request::i()->authy_method == 'phone' )
|
||||
{
|
||||
static::totp( "call/{$mfaDetails['authy']['pendingId']}", 'get', array( 'force' => 'true' ) );
|
||||
}
|
||||
elseif ( \IPS\Request::i()->authy_method == 'sms' )
|
||||
elseif ( Request::i()->authy_method == 'sms' )
|
||||
{
|
||||
static::totp( "sms/{$mfaDetails['authy']['pendingId']}", 'get', array( 'force' => 'true' ) );
|
||||
}
|
||||
|
||||
/* Update details */
|
||||
$mfaDetails['authy']['setupMethod'] = \IPS\Request::i()->authy_method;
|
||||
$mfaDetails['authy']['setupMethod'] = Request::i()->authy_method;
|
||||
$member->mfa_details = $mfaDetails;
|
||||
$member->save();
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'authy' );
|
||||
$_SESSION['authyAuthError'] = \IPS\Member::loggedIn()->isAdmin() ? $e->getMessage() : 'authy_error';
|
||||
Log::log( $e, 'authy' );
|
||||
$_SESSION['authyAuthError'] = Member::loggedIn()->isAdmin() ? $e->getMessage() : 'authy_error';
|
||||
}
|
||||
}
|
||||
|
||||
/* Display */
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->authyAuthenticate( $mfaDetails['authy']['setupMethod'], TRUE, isset( $_SESSION['authyAuthError'] ) ? $_SESSION['authyAuthError'] : 'authy_error', TRUE, explode( ',', \IPS\Settings::i()->authy_setup ), NULL, $url );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->authyAuthenticate( $mfaDetails['authy']['setupMethod'], TRUE, $_SESSION['authyAuthError'] ?? 'authy_error', TRUE, explode( ',', Settings::i()->authy_setup ), NULL, $url );
|
||||
}
|
||||
else
|
||||
{
|
||||
/* If they have used their allowed attempts, make them wait */
|
||||
if ( isset( $mfaDetails['authy'] ) and isset( $mfaDetails['authy']['changeAttempts'] ) and $mfaDetails['authy']['changeAttempts'] >= \IPS\Settings::i()->authy_setup_tries )
|
||||
if ( isset( $mfaDetails['authy'] ) and isset( $mfaDetails['authy']['changeAttempts'] ) and $mfaDetails['authy']['changeAttempts'] >= Settings::i()->authy_setup_tries )
|
||||
{
|
||||
$lockEndTime = $mfaDetails['authy']['lastChangeAttempt'] + ( \IPS\Settings::i()->authy_setup_lockout * 3600 );
|
||||
$lockEndTime = $mfaDetails['authy']['lastChangeAttempt'] + ( Settings::i()->authy_setup_lockout * 3600 );
|
||||
if ( $lockEndTime < time() )
|
||||
{
|
||||
$mfaDetails['authy']['changeAttempts'] = 0;
|
||||
@@ -129,32 +154,32 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->authySetupLockout( $showingMultipleHandlers, \IPS\DateTime::ts( $lockEndTime ) );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->authySetupLockout( $showingMultipleHandlers, DateTime::ts( $lockEndTime ) );
|
||||
}
|
||||
}
|
||||
|
||||
/* Otherwise show the form */
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->authySetup( isset( \IPS\Request::i()->countryCode ) ? \IPS\Request::i()->countryCode : \IPS\Helpers\Form\Address::calculateDefaultCountry(), isset( \IPS\Request::i()->phoneNumber ) ? \IPS\Request::i()->phoneNumber : '', $showingMultipleHandlers, explode( ',', \IPS\Settings::i()->authy_setup ), isset( $_SESSION['authyConfigureError'] ) ? $_SESSION['authyConfigureError'] : NULL );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->authySetup( isset( Request::i()->countryCode ) ? Request::i()->countryCode : Address::calculateDefaultCountry(), isset( Request::i()->phoneNumber ) ? Request::i()->phoneNumber : '', $showingMultipleHandlers, explode( ',', Settings::i()->authy_setup ), $_SESSION['authyConfigureError'] ?? NULL );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit configuration screen. Return TRUE if was accepted
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function configurationScreenSubmit( \IPS\Member $member )
|
||||
public function configurationScreenSubmit( Member $member ): bool
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
|
||||
/* If we've enterred a code, verify it */
|
||||
if ( isset( $mfaDetails['authy'] ) and isset( $mfaDetails['authy']['pendingId'] ) and isset( \IPS\Request::i()->authy_auth_code ) )
|
||||
if ( isset( $mfaDetails['authy'] ) and isset( $mfaDetails['authy']['pendingId'] ) and isset( Request::i()->authy_auth_code ) )
|
||||
{
|
||||
$_SESSION['authyAuthError'] = NULL;
|
||||
try
|
||||
{
|
||||
$response = static::totp( "verify/" . preg_replace( '/[^A-Z0-9]/i', '', \IPS\Request::i()->authy_auth_code ) . "/{$mfaDetails['authy']['pendingId']}", 'get' );
|
||||
$response = static::totp( "verify/" . preg_replace( '/[^A-Z0-9]/i', '', Request::i()->authy_auth_code ) . "/{$mfaDetails['authy']['pendingId']}" );
|
||||
|
||||
$mfaDetails['authy'] = array( 'id' => $mfaDetails['authy']['pendingId'], 'setup' => true );
|
||||
$member->mfa_details = $mfaDetails;
|
||||
@@ -164,39 +189,39 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
|
||||
return true;
|
||||
}
|
||||
catch ( Exception $e )
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
if ( \in_array( $e->getCode(), array( Exception::TOKEN_REUSED, Exception::TOKEN_INVALID ) ) )
|
||||
if ( in_array( $e->getCode(), array( AuthyException::TOKEN_REUSED, AuthyException::TOKEN_INVALID ) ) )
|
||||
{
|
||||
$_SESSION['authyAuthError'] = $e->getUserMessage();
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Log::log( $e, 'authy' );
|
||||
$_SESSION['authyAuthError'] = \IPS\Member::loggedIn()->isAdmin() ? $e->getMessage() : $e->getUserMessage();
|
||||
Log::log( $e, 'authy' );
|
||||
$_SESSION['authyAuthError'] = Member::loggedIn()->isAdmin() ? $e->getMessage() : $e->getUserMessage();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'authy' );
|
||||
$_SESSION['authyAuthError'] = \IPS\Member::loggedIn()->isAdmin() ? $e->getMessage() : 'authy_error';
|
||||
Log::log( $e, 'authy' );
|
||||
$_SESSION['authyAuthError'] = Member::loggedIn()->isAdmin() ? $e->getMessage() : 'authy_error';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Otherwise we need to generate an ID */
|
||||
elseif ( \IPS\Request::i()->phoneNumber )
|
||||
elseif ( Request::i()->phoneNumber )
|
||||
{
|
||||
/* Do we need to wait a while? */
|
||||
if ( isset( $mfaDetails['authy'] ) and isset( $mfaDetails['authy']['changeAttempts'] ) and $mfaDetails['authy']['changeAttempts'] >= \IPS\Settings::i()->authy_setup_tries )
|
||||
if ( isset( $mfaDetails['authy'] ) and isset( $mfaDetails['authy']['changeAttempts'] ) and $mfaDetails['authy']['changeAttempts'] >= Settings::i()->authy_setup_tries )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Call Authy */
|
||||
$availableMethods = explode( ',', \IPS\Settings::i()->authy_setup );
|
||||
$method = ( isset( \IPS\Request::i()->method ) and \in_array( \IPS\Request::i()->method, $availableMethods ) ) ? \IPS\Request::i()->method : array_shift( $availableMethods );
|
||||
$availableMethods = explode( ',', Settings::i()->authy_setup );
|
||||
$method = ( isset( Request::i()->method ) and in_array( Request::i()->method, $availableMethods ) ) ? Request::i()->method : array_shift( $availableMethods );
|
||||
$_SESSION['authyConfigureError'] = NULL;
|
||||
try
|
||||
{
|
||||
@@ -204,11 +229,11 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
$data = array(
|
||||
'user' => array(
|
||||
'email' => $member->email,
|
||||
'cellphone' => \IPS\Request::i()->phoneNumber,
|
||||
'country_code' => explode( '-', \IPS\Request::i()->countryCode )[1]
|
||||
'cellphone' => Request::i()->phoneNumber,
|
||||
'country_code' => explode( '-', Request::i()->countryCode )[1]
|
||||
)
|
||||
);
|
||||
if ( \IPS\Settings::i()->authy_method != 'authy' )
|
||||
if ( Settings::i()->authy_method != 'authy' )
|
||||
{
|
||||
$data['send_install_link_via_sms'] = false;
|
||||
}
|
||||
@@ -228,23 +253,23 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
static::totp( "sms/{$response['user']['id']}", 'get', array( 'force' => 'true' ) );
|
||||
}
|
||||
}
|
||||
catch ( Exception $e )
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
if ( \in_array( $e->getCode(), array( Exception::USER_INVALID, Exception::PHONE_NUMBER_INVALID ) ) )
|
||||
if ( in_array( $e->getCode(), array( AuthyException::USER_INVALID, AuthyException::PHONE_NUMBER_INVALID ) ) )
|
||||
{
|
||||
$_SESSION['authyConfigureError'] = $e->getUserMessage();
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Log::log( $e, 'authy' );
|
||||
$_SESSION['authyConfigureError'] = \IPS\Member::loggedIn()->isAdmin() ? $e->getMessage() : 'authy_error';
|
||||
Log::log( $e, 'authy' );
|
||||
$_SESSION['authyConfigureError'] = Member::loggedIn()->isAdmin() ? $e->getMessage() : 'authy_error';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'authy' );
|
||||
$_SESSION['authyConfigureError'] = \IPS\Member::loggedIn()->isAdmin() ? $e->getMessage() : 'authy_error';
|
||||
Log::log( $e, 'authy' );
|
||||
$_SESSION['authyConfigureError'] = Member::loggedIn()->isAdmin() ? $e->getMessage() : 'authy_error';
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -278,14 +303,14 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* Get the form for a member to authenticate
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param \IPS\Http\Url $url URL for page
|
||||
* @param Member $member The member
|
||||
* @param Url $url URL for page
|
||||
* @return string
|
||||
*/
|
||||
public function authenticationScreen( \IPS\Member $member, \IPS\Http\Url $url )
|
||||
public function authenticationScreen( Member $member, Url $url ): string
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
$availableMethods = explode( ',', \IPS\Settings::i()->authy_method );
|
||||
$availableMethods = explode( ',', Settings::i()->authy_method );
|
||||
|
||||
/* If we sent a code, but it was more than one minute ago, log a failure and reset */
|
||||
if ( isset( $mfaDetails['authy']['sent'] ) and $mfaDetails['authy']['sent']['time'] < ( time() - 60 ) )
|
||||
@@ -297,46 +322,46 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
}
|
||||
|
||||
/* If Authy app is one of the available options... */
|
||||
if ( \in_array( 'authy', $availableMethods ) )
|
||||
if ( in_array( 'authy', $availableMethods ) )
|
||||
{
|
||||
/* Are we getting a onetouch status? */
|
||||
if ( \IPS\Request::i()->onetouchCheck and \IPS\Request::i()->isAjax() )
|
||||
if ( Request::i()->onetouchCheck and Request::i()->isAjax() )
|
||||
{
|
||||
\IPS\Output::i()->json( array( 'status' => \intval( $this->_onetouchCheck( $member, \IPS\Request::i()->onetouchCheck ) ) ) );
|
||||
Output::i()->json( array( 'status' => intval( $this->_onetouchCheck( $member, Request::i()->onetouchCheck ) ) ) );
|
||||
}
|
||||
|
||||
/* If it is not the only option... */
|
||||
if ( \count( $availableMethods ) > 1 )
|
||||
if ( count( $availableMethods ) > 1 )
|
||||
{
|
||||
/* If they have asked for a text/call instead, do that */
|
||||
if ( !isset( $mfaDetails['authy']['sent'] ) and isset( \IPS\Request::i()->authy_method ) and \in_array( \IPS\Request::i()->authy_method, $availableMethods ) )
|
||||
if ( !isset( $mfaDetails['authy']['sent'] ) and isset( Request::i()->authy_method ) and in_array( Request::i()->authy_method, $availableMethods ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
/* Send text or make call */
|
||||
if ( \IPS\Request::i()->authy_method == 'phone' )
|
||||
if ( Request::i()->authy_method == 'phone' )
|
||||
{
|
||||
static::totp( "call/{$mfaDetails['authy']['id']}", 'get', array( 'force' => 'true' ) );
|
||||
}
|
||||
elseif ( \IPS\Request::i()->authy_method == 'sms' )
|
||||
elseif ( Request::i()->authy_method == 'sms' )
|
||||
{
|
||||
static::totp( "sms/{$mfaDetails['authy']['id']}", 'get', array( 'force' => 'true' ) );
|
||||
}
|
||||
|
||||
/* Update details */
|
||||
$mfaDetails['authy']['sent'] = array( 'method' => \IPS\Request::i()->authy_method, 'time' => time() );
|
||||
$mfaDetails['authy']['sent'] = array( 'method' => Request::i()->authy_method, 'time' => time() );
|
||||
$member->mfa_details = $mfaDetails;
|
||||
$member->save();
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'authy' );
|
||||
$_SESSION['authyAuthError'] = \IPS\Member::loggedIn()->isAdmin() ? $e->getMessage() : 'authy_error';
|
||||
Log::log( $e, 'authy' );
|
||||
$_SESSION['authyAuthError'] = Member::loggedIn()->isAdmin() ? $e->getMessage() : 'authy_error';
|
||||
}
|
||||
}
|
||||
if ( isset( $mfaDetails['authy']['sent'] ) )
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->authyAuthenticate( $mfaDetails['authy']['sent']['method'], TRUE, isset( $_SESSION['authyAuthError'] ) ? $_SESSION['authyAuthError'] : 'authy_error', FALSE, $availableMethods, NULL, $url );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->authyAuthenticate( $mfaDetails['authy']['sent']['method'], TRUE, $_SESSION['authyAuthError'] ?? 'authy_error', FALSE, $availableMethods, NULL, $url );
|
||||
}
|
||||
|
||||
/* Otherwise, check if they have the app installed. If they do, show the Authy authenticate page */
|
||||
@@ -354,7 +379,7 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
|
||||
if ( $userHasAuthyApp )
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->authyAuthenticate( 'authy', TRUE, isset( $_SESSION['authyAuthError'] ) ? $_SESSION['authyAuthError'] : 'authy_error', FALSE, $availableMethods, $this->_onetouchInit( $member, $url ), $url );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->authyAuthenticate( 'authy', TRUE, $_SESSION['authyAuthError'] ?? 'authy_error', FALSE, $availableMethods, $this->_onetouchInit( $member, $url ), $url );
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
@@ -363,12 +388,12 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/* If it is the only option, show it anyway */
|
||||
else
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->authyAuthenticate( 'authy', TRUE, isset( $_SESSION['authyAuthError'] ) ? $_SESSION['authyAuthError'] : 'authy_error', FALSE, $availableMethods, $this->_onetouchInit( $member, $url ), $url );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->authyAuthenticate( 'authy', TRUE, $_SESSION['authyAuthError'] ?? 'authy_error', FALSE, $availableMethods, $this->_onetouchInit( $member, $url ), $url );
|
||||
}
|
||||
}
|
||||
|
||||
/* If text message is the only available option, or we have chosen that option, do that... */
|
||||
if ( \in_array( 'sms', $availableMethods ) and ( \count( $availableMethods ) == 1 ) or ( isset( $mfaDetails['authy']['sent'] ) and $mfaDetails['authy']['sent']['method'] == 'sms' ) or ( isset( \IPS\Request::i()->authy_method ) and \IPS\Request::i()->authy_method == 'sms' ) )
|
||||
if ( in_array( 'sms', $availableMethods ) and ( count( $availableMethods ) == 1 ) or ( isset( $mfaDetails['authy']['sent'] ) and $mfaDetails['authy']['sent']['method'] == 'sms' ) or ( isset( Request::i()->authy_method ) and Request::i()->authy_method == 'sms' ) )
|
||||
{
|
||||
/* Send the text if we haven't already */
|
||||
if ( !isset( $mfaDetails['authy']['sent'] ) )
|
||||
@@ -379,8 +404,8 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'authy' );
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->authyError( $e->getMessage() );
|
||||
Log::log( $e, 'authy' );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->authyError( $e->getMessage() );
|
||||
}
|
||||
|
||||
$mfaDetails['authy']['sent'] = array( 'method' => 'sms', 'time' => time() );
|
||||
@@ -389,11 +414,11 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
}
|
||||
|
||||
/* Show screen */
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->authyAuthenticate( 'sms', TRUE, isset( $_SESSION['authyAuthError'] ) ? $_SESSION['authyAuthError'] : 'authy_error', FALSE, $availableMethods, NULL, $url );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->authyAuthenticate( 'sms', TRUE, $_SESSION['authyAuthError'] ?? 'authy_error', FALSE, $availableMethods, NULL, $url );
|
||||
}
|
||||
|
||||
/* If we have confirmed the phone call, do that now */
|
||||
if ( ( isset( $mfaDetails['authy']['sent'] ) and $mfaDetails['authy']['sent']['method'] == 'phone' ) or ( isset( \IPS\Request::i()->authy_method ) and \IPS\Request::i()->authy_method == 'phone' ) )
|
||||
if ( ( isset( $mfaDetails['authy']['sent'] ) and $mfaDetails['authy']['sent']['method'] == 'phone' ) or ( isset( Request::i()->authy_method ) and Request::i()->authy_method == 'phone' ) )
|
||||
{
|
||||
/* Send the text if we haven't already */
|
||||
if ( !isset( $mfaDetails['authy']['sent'] ) )
|
||||
@@ -404,8 +429,8 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'authy' );
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->authyError( $e->getMessage() );
|
||||
Log::log( $e, 'authy' );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->authyError( $e->getMessage() );
|
||||
}
|
||||
|
||||
$mfaDetails['authy']['sent'] = array( 'method' => 'call', 'time' => time() );
|
||||
@@ -414,29 +439,29 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
}
|
||||
|
||||
/* Show screen */
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->authyAuthenticate( 'phone', TRUE, isset( $_SESSION['authyAuthError'] ) ? $_SESSION['authyAuthError'] : 'authy_error', FALSE, $availableMethods, NULL, $url );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->authyAuthenticate( 'phone', TRUE, $_SESSION['authyAuthError'] ?? 'authy_error', FALSE, $availableMethods, NULL, $url );
|
||||
}
|
||||
|
||||
/* Otherwise we're going to show a screen */
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->authyAuthenticate( \count( $availableMethods ) == 1 ? 'phone' : 'choose', FALSE, isset( $_SESSION['authyAuthError'] ) ? $_SESSION['authyAuthError'] : 'authy_error', FALSE, $availableMethods, NULL, $url );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->authyAuthenticate( count( $availableMethods ) == 1 ? 'phone' : 'choose', FALSE, $_SESSION['authyAuthError'] ?? 'authy_error', FALSE, $availableMethods, NULL, $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, initiate a OneTouch request and get the ID
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param \IPS\Http\Url $url URL for page
|
||||
* @param Member $member The member
|
||||
* @param Url $url URL for page
|
||||
* @return string|null
|
||||
*/
|
||||
protected function _onetouchInit( \IPS\Member $member, \IPS\Http\Url $url )
|
||||
protected function _onetouchInit( Member $member, Url $url ): ?string
|
||||
{
|
||||
if ( \IPS\Settings::i()->authy_onetouch )
|
||||
if ( Settings::i()->authy_onetouch )
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
|
||||
if ( isset( $mfaDetails['onetouch'] ) and $mfaDetails['onetouch']['time'] > ( time() - 30 ) )
|
||||
{
|
||||
$response = static::onetouch( "approval_requests/" . preg_replace( '/[^A-Z0-9\-]/i', '', $mfaDetails['onetouch']['id'] ), 'get' );
|
||||
$response = static::onetouch( "approval_requests/" . preg_replace( '/[^A-Z0-9\-]/i', '', $mfaDetails['onetouch']['id'] ) );
|
||||
|
||||
if ( $response['approval_request']['status'] === 'pending' )
|
||||
{
|
||||
@@ -464,23 +489,23 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check the status of a onetouch request
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $id The onetouch request ID
|
||||
* @return string|null
|
||||
* @param Member $member The member
|
||||
* @param string $id The onetouch request ID
|
||||
* @return bool|string|null
|
||||
*/
|
||||
protected function _onetouchCheck( \IPS\Member $member, $id )
|
||||
protected function _onetouchCheck( Member $member, string $id ): bool|string|null
|
||||
{
|
||||
if ( \IPS\Settings::i()->authy_onetouch )
|
||||
if ( Settings::i()->authy_onetouch )
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
|
||||
try
|
||||
{
|
||||
$response = static::onetouch( "approval_requests/" . preg_replace( '/[^A-Z0-9\-]/i', '', $id ), 'get' );
|
||||
$response = static::onetouch( "approval_requests/" . preg_replace( '/[^A-Z0-9\-]/i', '', $id ) );
|
||||
return $response['approval_request']['status'] === 'approved';
|
||||
}
|
||||
catch ( \Exception $e ) {}
|
||||
@@ -491,10 +516,10 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* Submit authentication screen. Return TRUE if was accepted
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function authenticationScreenSubmit( \IPS\Member $member )
|
||||
public function authenticationScreenSubmit( Member $member ): bool
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
|
||||
@@ -502,14 +527,14 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
|
||||
try
|
||||
{
|
||||
if ( isset( \IPS\Request::i()->authy_auth_code ) )
|
||||
if ( isset( Request::i()->authy_auth_code ) )
|
||||
{
|
||||
$response = static::totp( "verify/" . preg_replace( '/[^A-Z0-9]/i', '', \IPS\Request::i()->authy_auth_code ) . "/{$mfaDetails['authy']['id']}", 'get' );
|
||||
$response = static::totp( "verify/" . preg_replace( '/[^A-Z0-9]/i', '', Request::i()->authy_auth_code ) . "/{$mfaDetails['authy']['id']}" );
|
||||
$mfaDetails['authy'] = array( 'id' => $mfaDetails['authy']['id'], 'setup' => true );
|
||||
}
|
||||
elseif ( isset( \IPS\Request::i()->onetouch ) )
|
||||
elseif ( isset( Request::i()->onetouch ) )
|
||||
{
|
||||
return $this->_onetouchCheck( $member, \IPS\Request::i()->onetouch );
|
||||
return $this->_onetouchCheck( $member, Request::i()->onetouch );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -520,23 +545,23 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
|
||||
return true;
|
||||
}
|
||||
catch ( Exception $e )
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
if ( \in_array( $e->getCode(), array( Exception::TOKEN_REUSED, Exception::TOKEN_INVALID ) ) )
|
||||
if ( in_array( $e->getCode(), array( AuthyException::TOKEN_REUSED, AuthyException::TOKEN_INVALID ) ) )
|
||||
{
|
||||
$_SESSION['authyAuthError'] = $e->getUserMessage();
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Log::log( $e, 'authy' );
|
||||
$_SESSION['authyAuthError'] = \IPS\Member::loggedIn()->isAdmin() ? $e->getMessage() : $e->getUserMessage();
|
||||
Log::log( $e, 'authy' );
|
||||
$_SESSION['authyAuthError'] = Member::loggedIn()->isAdmin() ? $e->getMessage() : $e->getUserMessage();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'authy' );
|
||||
$_SESSION['authyAuthError'] = \IPS\Member::loggedIn()->isAdmin() ? $e->getMessage() : 'authy_error';
|
||||
Log::log( $e, 'authy' );
|
||||
$_SESSION['authyAuthError'] = Member::loggedIn()->isAdmin() ? $e->getMessage() : 'authy_error';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -547,22 +572,22 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
* Toggle
|
||||
*
|
||||
* @param bool $enabled On/Off
|
||||
* @return bool
|
||||
* @return void
|
||||
*/
|
||||
public function toggle( $enabled )
|
||||
public function toggle( bool $enabled ): void
|
||||
{
|
||||
/* This handler is deprecated, so if it's already disabled, don't allow it to be re-enabled */
|
||||
if( !$this->isEnabled() )
|
||||
{
|
||||
return FALSE;
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $enabled )
|
||||
{
|
||||
static::verifyApiKey( \IPS\Settings::i()->authy_key );
|
||||
static::verifyApiKey( Settings::i()->authy_key );
|
||||
}
|
||||
|
||||
\IPS\Settings::i()->changeValues( array( 'authy_enabled' => $enabled ) );
|
||||
Settings::i()->changeValues( array( 'authy_enabled' => $enabled ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -570,53 +595,53 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function acpSettings()
|
||||
public function acpSettings(): string
|
||||
{
|
||||
if( !$this->isEnabled() )
|
||||
{
|
||||
\IPS\Output::i()->error( 'authy_deprecated_message', '2C345/3' );
|
||||
Output::i()->error( 'authy_deprecated_message', '2C345/3' );
|
||||
}
|
||||
|
||||
$form = new \IPS\Helpers\Form;
|
||||
$form = new Form;
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'authy_key', \IPS\Settings::i()->authy_key, TRUE, array(), function( $val ) {
|
||||
$details = \IPS\MFA\Authy\Handler::verifyApiKey( $val );
|
||||
if ( !$details['app']['sms_enabled'] and ( array_key_exists( 'sms', \IPS\Request::i()->authy_setup ) or array_key_exists( 'sms', \IPS\Request::i()->authy_method ) ) )
|
||||
$form->add( new Text( 'authy_key', Settings::i()->authy_key, TRUE, array(), function( $val ) {
|
||||
$details = Handler::verifyApiKey( $val );
|
||||
if ( !$details['app']['sms_enabled'] and ( array_key_exists( 'sms', Request::i()->authy_setup ) or array_key_exists( 'sms', Request::i()->authy_method ) ) )
|
||||
{
|
||||
throw new \DomainException('authy_key_no_sms');
|
||||
throw new DomainException('authy_key_no_sms');
|
||||
}
|
||||
if ( !$details['app']['phone_calls_enabled'] and ( array_key_exists( 'phone', \IPS\Request::i()->authy_setup ) or array_key_exists( 'phone', \IPS\Request::i()->authy_method ) ) )
|
||||
if ( !$details['app']['phone_calls_enabled'] and ( array_key_exists( 'phone', Request::i()->authy_setup ) or array_key_exists( 'phone', Request::i()->authy_method ) ) )
|
||||
{
|
||||
throw new \DomainException('authy_key_no_sms');
|
||||
throw new DomainException('authy_key_no_sms');
|
||||
}
|
||||
if ( !$details['app']['onetouch_enabled'] and \IPS\Request::i()->authy_onetouch )
|
||||
if ( !$details['app']['onetouch_enabled'] and Request::i()->authy_onetouch )
|
||||
{
|
||||
throw new \DomainException('authy_key_no_onetouch');
|
||||
throw new DomainException('authy_key_no_onetouch');
|
||||
}
|
||||
}, NULL, \IPS\Member::loggedIn()->language()->addToStack('authy_key_suffix') ) );
|
||||
}, NULL, Member::loggedIn()->language()->addToStack('authy_key_suffix') ) );
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'authy_groups', \IPS\Settings::i()->authy_groups == '*' ? '*' : explode( ',', \IPS\Settings::i()->authy_groups ), FALSE, array(
|
||||
$form->add( new CheckboxSet( 'authy_groups', Settings::i()->authy_groups == '*' ? '*' : explode( ',', Settings::i()->authy_groups ), FALSE, array(
|
||||
'multiple' => TRUE,
|
||||
'options' => array_combine( array_keys( \IPS\Member\Group::groups() ), array_map( function( $_group ) { return (string) $_group; }, \IPS\Member\Group::groups() ) ),
|
||||
'options' => array_combine( array_keys( Group::groups() ), array_map( function( $_group ) { return (string) $_group; }, Group::groups() ) ),
|
||||
'unlimited' => '*',
|
||||
'unlimitedLang' => 'everyone',
|
||||
'impliedUnlimited' => TRUE
|
||||
) ) );
|
||||
|
||||
$form->addHeader('authy_setup_header');
|
||||
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'authy_setup', explode( ',', \IPS\Settings::i()->authy_setup ), TRUE, array( 'options' => array(
|
||||
$form->add( new CheckboxSet( 'authy_setup', explode( ',', Settings::i()->authy_setup ), TRUE, array( 'options' => array(
|
||||
'authy' => 'authy_method_authy',
|
||||
'sms' => 'authy_method_sms',
|
||||
'phone' => 'authy_method_phone',
|
||||
) ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Custom( 'authy_setup_protection', array( \IPS\Settings::i()->authy_setup_tries, \IPS\Settings::i()->authy_setup_lockout ), FALSE, array(
|
||||
$form->add( new Custom( 'authy_setup_protection', array( Settings::i()->authy_setup_tries, Settings::i()->authy_setup_lockout ), FALSE, array(
|
||||
'getHtml' => function( $field ) {
|
||||
return \IPS\Theme::i()->getTemplate('settings')->authySetupProtection( $field->value );
|
||||
return Theme::i()->getTemplate('settings')->authySetupProtection( $field->value );
|
||||
}
|
||||
) ) );
|
||||
|
||||
$form->addHeader('authy_authenticate_header');
|
||||
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'authy_method', explode( ',', \IPS\Settings::i()->authy_method ), TRUE, array(
|
||||
$form->add( new CheckboxSet( 'authy_method', explode( ',', Settings::i()->authy_method ), TRUE, array(
|
||||
'options' => array(
|
||||
'authy' => 'authy_method_authy',
|
||||
'sms' => 'authy_method_sms',
|
||||
@@ -626,7 +651,7 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
'authy' => array( 'authy_onetouch' )
|
||||
)
|
||||
) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'authy_onetouch', \IPS\Settings::i()->authy_onetouch, TRUE, array(
|
||||
$form->add( new Radio( 'authy_onetouch', Settings::i()->authy_onetouch, TRUE, array(
|
||||
'options' => array(
|
||||
'1' => 'authy_onetouch_on',
|
||||
'0' => 'authy_onetouch_off',
|
||||
@@ -643,8 +668,8 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
$values['authy_method'] = isset( $values['authy_method'] ) ? implode( ',', $values['authy_method'] ) : '';
|
||||
$form->saveAsSettings( $values );
|
||||
|
||||
\IPS\Session::i()->log( 'acplogs__mfa_handler_enabled', array( "mfa_authy_title" => TRUE ) );
|
||||
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=settings&controller=mfa' ), 'saved' );
|
||||
Session::i()->log( 'acplogs__mfa_handler_enabled', array( "mfa_authy_title" => TRUE ) );
|
||||
Output::i()->redirect( Url::internal( 'app=core&module=settings&controller=mfa' ), 'saved' );
|
||||
}
|
||||
|
||||
return (string) $form;
|
||||
@@ -657,10 +682,10 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* If member has configured this handler, disable it
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
* @param Member $member The member
|
||||
* @return void
|
||||
*/
|
||||
public function disableHandlerForMember( \IPS\Member $member )
|
||||
public function disableHandlerForMember( Member $member ): void
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
|
||||
@@ -669,7 +694,7 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
try
|
||||
{
|
||||
static::totp( "users/{$mfaDetails['authy']['id']}/delete", 'post', array(
|
||||
'user_ip' => \IPS\Request::i()->ipAddress()
|
||||
'user_ip' => Request::i()->ipAddress()
|
||||
) );
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
@@ -688,21 +713,21 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function ucpTitle()
|
||||
public function ucpTitle(): string
|
||||
{
|
||||
$availableMethods = explode( ',', \IPS\Settings::i()->authy_method );
|
||||
$availableMethods = explode( ',', Settings::i()->authy_method );
|
||||
|
||||
if ( \in_array( 'authy', $availableMethods ) )
|
||||
if ( in_array( 'authy', $availableMethods ) )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('mfa_authy_title');
|
||||
return Member::loggedIn()->language()->addToStack('mfa_authy_title');
|
||||
}
|
||||
elseif ( \in_array( 'sms', $availableMethods ) and \count( $availableMethods ) == 1 )
|
||||
elseif ( in_array( 'sms', $availableMethods ) and count( $availableMethods ) == 1 )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('mfa_sms_title');
|
||||
return Member::loggedIn()->language()->addToStack('mfa_sms_title');
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('mfa_phone_title');
|
||||
return Member::loggedIn()->language()->addToStack('mfa_phone_title');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,32 +736,32 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function ucpDesc()
|
||||
public function ucpDesc(): string
|
||||
{
|
||||
$availableMethods = explode( ',', \IPS\Settings::i()->authy_method );
|
||||
$availableMethods = explode( ',', Settings::i()->authy_method );
|
||||
|
||||
if ( \in_array( 'authy', $availableMethods ) )
|
||||
if ( in_array( 'authy', $availableMethods ) )
|
||||
{
|
||||
if ( \count( $availableMethods ) == 1 )
|
||||
if ( count( $availableMethods ) == 1 )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('mfa_authy_only_desc_user');
|
||||
return Member::loggedIn()->language()->addToStack('mfa_authy_only_desc_user');
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('mfa_authy_mixed_desc_user');
|
||||
return Member::loggedIn()->language()->addToStack('mfa_authy_mixed_desc_user');
|
||||
}
|
||||
}
|
||||
elseif ( \in_array( 'sms', $availableMethods ) and \count( $availableMethods ) == 1 )
|
||||
elseif ( in_array( 'sms', $availableMethods ) and count( $availableMethods ) == 1 )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('mfa_sms_desc_user');
|
||||
return Member::loggedIn()->language()->addToStack('mfa_sms_desc_user');
|
||||
}
|
||||
elseif ( \in_array( 'phone', $availableMethods ) and \count( $availableMethods ) == 1 )
|
||||
elseif ( in_array( 'phone', $availableMethods ) and count( $availableMethods ) == 1 )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('mfa_phone_desc_user');
|
||||
return Member::loggedIn()->language()->addToStack('mfa_phone_desc_user');
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('mfa_sms_or_phone_desc_user');
|
||||
return Member::loggedIn()->language()->addToStack('mfa_sms_or_phone_desc_user');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -745,21 +770,21 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function recoveryButton()
|
||||
public function recoveryButton(): string
|
||||
{
|
||||
$availableMethods = explode( ',', \IPS\Settings::i()->authy_method );
|
||||
$availableMethods = explode( ',', Settings::i()->authy_method );
|
||||
|
||||
if ( \in_array( 'authy', $availableMethods ) and \count( $availableMethods ) == 1 )
|
||||
if ( in_array( 'authy', $availableMethods ) and count( $availableMethods ) == 1 )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('mfa_authy_recovery');
|
||||
return Member::loggedIn()->language()->addToStack('mfa_authy_recovery');
|
||||
}
|
||||
elseif ( \in_array( 'sms', $availableMethods ) and \count( $availableMethods ) == 1 )
|
||||
elseif ( in_array( 'sms', $availableMethods ) and count( $availableMethods ) == 1 )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('mfa_sms_recovery');
|
||||
return Member::loggedIn()->language()->addToStack('mfa_sms_recovery');
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('mfa_phone_recovery');
|
||||
return Member::loggedIn()->language()->addToStack('mfa_phone_recovery');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -768,12 +793,12 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* Make TOTP API Call
|
||||
*
|
||||
* @param string $endpoint The endpoint to call
|
||||
* @param string $method 'get' or 'post'
|
||||
* @param array $data Post data or additional query string parameters
|
||||
* @param string $endpoint The endpoint to call
|
||||
* @param string $method 'get' or 'post'
|
||||
* @param array|null $data Post data or additional query string parameters
|
||||
* @return array
|
||||
*/
|
||||
public static function totp( $endpoint, $method='get', $data=NULL )
|
||||
public static function totp( string $endpoint, string $method='get', array $data=NULL ): array
|
||||
{
|
||||
return static::_api( "protected/json/{$endpoint}", $method, $data );
|
||||
}
|
||||
@@ -781,12 +806,12 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* Make OneTouch API Call
|
||||
*
|
||||
* @param string $endpoint The endpoint to call
|
||||
* @param string $method 'get' or 'post'
|
||||
* @param array $data Post data or additional query string parameters
|
||||
* @param string $endpoint The endpoint to call
|
||||
* @param string $method 'get' or 'post'
|
||||
* @param array|null $data Post data or additional query string parameters
|
||||
* @return array
|
||||
*/
|
||||
public static function onetouch( $endpoint, $method='get', $data=NULL )
|
||||
public static function onetouch( string $endpoint, string $method='get', array $data=NULL ): array
|
||||
{
|
||||
return static::_api( "onetouch/json/{$endpoint}", $method, $data );
|
||||
}
|
||||
@@ -794,14 +819,14 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* Make API Call
|
||||
*
|
||||
* @param string $endpoint The endpoint to call
|
||||
* @param string $method 'get' or 'post'
|
||||
* @param array $data Post data or additional query string parameters
|
||||
* @param string $endpoint The endpoint to call
|
||||
* @param string $method 'get' or 'post'
|
||||
* @param array|null $data Post data or additional query string parameters
|
||||
* @return array
|
||||
*/
|
||||
protected static function _api( $endpoint, $method='get', $data=NULL )
|
||||
protected static function _api( string $endpoint, string $method='get', array $data=NULL ): array
|
||||
{
|
||||
$url = \IPS\Http\Url::external("https://api.authy.com/{$endpoint}")->setQueryString( 'api_key', \IPS\Settings::i()->authy_key );
|
||||
$url = Url::external("https://api.authy.com/{$endpoint}")->setQueryString( 'api_key', Settings::i()->authy_key );
|
||||
|
||||
if ( $method == 'get' )
|
||||
{
|
||||
@@ -816,7 +841,7 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
|
||||
if ( !$response['success'] )
|
||||
{
|
||||
throw new Exception( $response['message'], $response['error_code'] );
|
||||
throw new \IPS\Http\Request\Exception( $response['message'], $response['error_code'] );
|
||||
}
|
||||
|
||||
return $response;
|
||||
@@ -825,19 +850,19 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* Verify an Authy API Key
|
||||
*
|
||||
* @param string $val The API key submitted
|
||||
* @param string $val The API key submitted
|
||||
* @return array
|
||||
* @throws \DomainException
|
||||
* @throws DomainException
|
||||
*/
|
||||
public static function verifyApiKey( $val )
|
||||
public static function verifyApiKey( string $val ): array
|
||||
{
|
||||
try
|
||||
{
|
||||
return \IPS\Http\Url::external("https://api.authy.com/protected/json/app/details")->setQueryString( 'api_key', $val )->request()->get()->decodeJson();
|
||||
return Url::external("https://api.authy.com/protected/json/app/details")->setQueryString( 'api_key', $val )->request()->get()->decodeJson();
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
throw new \DomainException( $e->getMessage() );
|
||||
throw new DomainException( $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,21 +11,51 @@
|
||||
namespace IPS\MFA\GoogleAuthenticator;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
|
||||
use BaconQrCode\Renderer\ImageRenderer;
|
||||
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
||||
use BaconQrCode\Writer;
|
||||
use DateInterval;
|
||||
use IPS\DateTime;
|
||||
use IPS\Db;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\CheckboxSet;
|
||||
use IPS\Http\Url;
|
||||
use IPS\IPS;
|
||||
use IPS\Login;
|
||||
use IPS\Member;
|
||||
use IPS\Member\Group;
|
||||
use IPS\MFA\MFAHandler;
|
||||
use IPS\Output;
|
||||
use IPS\Request;
|
||||
use IPS\Session;
|
||||
use IPS\Settings;
|
||||
use IPS\Theme;
|
||||
use UnderflowException;
|
||||
use function chr;
|
||||
use function count;
|
||||
use function defined;
|
||||
use function function_exists;
|
||||
use function in_array;
|
||||
use function ord;
|
||||
use function substr;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi Factor Authentication Handler for Google Authenticator
|
||||
*/
|
||||
class _Handler extends \IPS\MFA\MFAHandler
|
||||
class Handler extends MFAHandler
|
||||
{
|
||||
/**
|
||||
* @brief Key
|
||||
*/
|
||||
protected $key = 'google';
|
||||
protected string $key = 'google';
|
||||
|
||||
/* !Setup */
|
||||
|
||||
@@ -34,28 +64,29 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled()
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return \IPS\Settings::i()->googleauth_enabled;
|
||||
return Settings::i()->googleauth_enabled;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Member *can* use this handler (even if they have not yet configured it)
|
||||
*
|
||||
* @return bool
|
||||
* @param Member $member
|
||||
* @return bool
|
||||
*/
|
||||
public function memberCanUseHandler( \IPS\Member $member )
|
||||
public function memberCanUseHandler( Member $member ): bool
|
||||
{
|
||||
return \IPS\Settings::i()->googleauth_groups == '*' or $member->inGroup( explode( ',', \IPS\Settings::i()->googleauth_groups ) );
|
||||
return Settings::i()->googleauth_groups == '*' or $member->inGroup( explode( ',', Settings::i()->googleauth_groups ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Member has configured this handler
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function memberHasConfiguredHandler( \IPS\Member $member )
|
||||
public function memberHasConfiguredHandler( Member $member ): bool
|
||||
{
|
||||
return isset( $member->mfa_details['google'] );
|
||||
}
|
||||
@@ -63,75 +94,76 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* Show a setup screen
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @param bool $showingMultipleHandlers Set to TRUE if multiple options are being displayed
|
||||
* @param \IPS\Http\Url $url URL for page
|
||||
* @param Url $url URL for page
|
||||
* @return string
|
||||
*/
|
||||
public function configurationScreen( \IPS\Member $member, $showingMultipleHandlers, \IPS\Http\Url $url )
|
||||
public function configurationScreen( Member $member, bool $showingMultipleHandlers, Url $url ): string
|
||||
{
|
||||
/* Generate a secret */
|
||||
if ( isset( \IPS\Request::i()->secret ) )
|
||||
if ( isset( Request::i()->secret ) )
|
||||
{
|
||||
$secret = \IPS\Request::i()->secret;
|
||||
$secret = Request::i()->secret;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( \function_exists( 'random_bytes' ) )
|
||||
if ( function_exists( 'random_bytes' ) )
|
||||
{
|
||||
$randomString = random_bytes( 16 );
|
||||
}
|
||||
elseif ( \function_exists( 'mcrypt_create_iv' ) )
|
||||
elseif ( function_exists( 'mcrypt_create_iv' ) )
|
||||
{
|
||||
$randomString = mcrypt_create_iv( 16, MCRYPT_DEV_URANDOM );
|
||||
}
|
||||
elseif ( \function_exists( 'openssl_random_pseudo_bytes' ) )
|
||||
elseif ( function_exists( 'openssl_random_pseudo_bytes' ) )
|
||||
{
|
||||
$randomString = openssl_random_pseudo_bytes( 16 );
|
||||
}
|
||||
else
|
||||
{
|
||||
$randomString = \substr( md5( uniqid( microtime(), true ) ) . md5( uniqid( microtime(), true ) ), 0, 16 );
|
||||
$randomString = substr( md5( uniqid( microtime(), true ) ) . md5( uniqid( microtime(), true ) ), 0, 16 );
|
||||
}
|
||||
$validChars = array( 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7', '=' );
|
||||
$secret = '';
|
||||
for ( $i = 0; $i < 16; ++$i )
|
||||
{
|
||||
$secret .= $validChars[ \ord( $randomString[ $i ] ) & 31 ];
|
||||
$secret .= $validChars[ ord( $randomString[ $i ] ) & 31 ];
|
||||
}
|
||||
}
|
||||
|
||||
\IPS\IPS::$PSR0Namespaces['BaconQrCode'] = \IPS\ROOT_PATH . '/system/3rd_party/BaconQrCode/src';
|
||||
\IPS\IPS::$PSR0Namespaces['DASPRiD'] = \IPS\ROOT_PATH . '/system/3rd_party/DASPRiD';
|
||||
$renderer = new \BaconQrCode\Renderer\ImageRenderer(
|
||||
new \BaconQrCode\Renderer\RendererStyle\RendererStyle(150),
|
||||
new \BaconQrCode\Renderer\Image\SvgImageBackEnd()
|
||||
|
||||
/* Generate QR code */
|
||||
IPS::$PSR0Namespaces['BaconQrCode'] = \IPS\ROOT_PATH . '/system/3rd_party/BaconQrCode/src';
|
||||
IPS::$PSR0Namespaces['DASPRiD'] = \IPS\ROOT_PATH . '/system/3rd_party/DASPRiD';
|
||||
$renderer = new ImageRenderer(
|
||||
new RendererStyle(150),
|
||||
new SvgImageBackEnd()
|
||||
);
|
||||
$writer = new \BaconQrCode\Writer($renderer);
|
||||
$data = "otpauth://totp/{$member->email}?secret={$secret}&issuer=" . rawurlencode( \IPS\Settings::i()->board_name );
|
||||
$writer = new Writer($renderer);
|
||||
$data = "otpauth://totp/{$member->email}?secret={$secret}&issuer=" . rawurlencode( Settings::i()->board_name );
|
||||
|
||||
$str = $writer->writeString($data);
|
||||
|
||||
$qrCode = 'data:image/svg+xml;utf8,' . rawurlencode($str);
|
||||
|
||||
/* Display */
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->googleAuthenticatorSetup( $qrCode, $secret, $showingMultipleHandlers );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->googleAuthenticatorSetup( $qrCode, $secret, $showingMultipleHandlers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit configuration screen. Return TRUE if was accepted
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function configurationScreenSubmit( \IPS\Member $member )
|
||||
public function configurationScreenSubmit( Member $member ): bool
|
||||
{
|
||||
if ( \IPS\Request::i()->google_authenticator_setup_code )
|
||||
if ( Request::i()->google_authenticator_setup_code )
|
||||
{
|
||||
if ( static::checkSubmittedCode( \IPS\Request::i()->google_authenticator_setup_code, \IPS\Request::i()->secret, $member ) )
|
||||
if ( static::checkSubmittedCode( Request::i()->google_authenticator_setup_code, Request::i()->secret, $member ) )
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
$mfaDetails['google'] = \IPS\Request::i()->secret;
|
||||
$mfaDetails['google'] = Request::i()->secret;
|
||||
$member->mfa_details = $mfaDetails;
|
||||
$member->save();
|
||||
|
||||
@@ -149,37 +181,37 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* Get the form for a member to authenticate
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param \IPS\Http\Url $url URL for page
|
||||
* @param Member $member The member
|
||||
* @param Url $url URL for page
|
||||
* @return string
|
||||
*/
|
||||
public function authenticationScreen( \IPS\Member $member, \IPS\Http\Url $url )
|
||||
public function authenticationScreen( Member $member, Url $url ): string
|
||||
{
|
||||
try
|
||||
{
|
||||
$waitUntil = ( \IPS\Db::i()->select( 'time', 'core_googleauth_used_codes', array( '`member`=?', $member->member_id ), 'time DESC', 1 )->first() * 30 ) + 30;
|
||||
$waitUntil = ( Db::i()->select( 'time', 'core_googleauth_used_codes', array( '`member`=?', $member->member_id ), 'time DESC', 1 )->first() * 30 ) + 30;
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
catch ( UnderflowException $e )
|
||||
{
|
||||
$waitUntil = NULL;
|
||||
}
|
||||
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->googleAuthenticatorAuth( $waitUntil );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->googleAuthenticatorAuth( $waitUntil );
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit authentication screen. Return TRUE if was accepted
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return string
|
||||
* @param Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function authenticationScreenSubmit( \IPS\Member $member )
|
||||
public function authenticationScreenSubmit( Member $member ): bool
|
||||
{
|
||||
if ( \IPS\Request::i()->google_authenticator_auth_code )
|
||||
if ( Request::i()->google_authenticator_auth_code )
|
||||
{
|
||||
if ( $codeTime = static::checkSubmittedCode( \IPS\Request::i()->google_authenticator_auth_code, $member->mfa_details['google'], $member ) )
|
||||
if ( $codeTime = static::checkSubmittedCode( Request::i()->google_authenticator_auth_code, $member->mfa_details['google'], $member ) )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_googleauth_used_codes', array(
|
||||
Db::i()->insert( 'core_googleauth_used_codes', array(
|
||||
'member' => $member->member_id,
|
||||
'time' => $codeTime
|
||||
) );
|
||||
@@ -196,11 +228,11 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
* Toggle
|
||||
*
|
||||
* @param bool $enabled On/Off
|
||||
* @return bool
|
||||
* @return void
|
||||
*/
|
||||
public function toggle( $enabled )
|
||||
public function toggle( bool $enabled ): void
|
||||
{
|
||||
\IPS\Settings::i()->changeValues( array( 'googleauth_enabled' => $enabled ) );
|
||||
Settings::i()->changeValues( array( 'googleauth_enabled' => $enabled ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,12 +240,12 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function acpSettings()
|
||||
public function acpSettings(): string
|
||||
{
|
||||
$form = new \IPS\Helpers\Form;
|
||||
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'googleauth_groups', \IPS\Settings::i()->googleauth_groups == '*' ? '*' : explode( ',', \IPS\Settings::i()->googleauth_groups ), FALSE, array(
|
||||
$form = new Form;
|
||||
$form->add( new CheckboxSet( 'googleauth_groups', Settings::i()->googleauth_groups == '*' ? '*' : explode( ',', Settings::i()->googleauth_groups ), FALSE, array(
|
||||
'multiple' => TRUE,
|
||||
'options' => array_combine( array_keys( \IPS\Member\Group::groups() ), array_map( function( $_group ) { return (string) $_group; }, \IPS\Member\Group::groups() ) ),
|
||||
'options' => array_combine( array_keys( Group::groups() ), array_map( function( $_group ) { return (string) $_group; }, Group::groups() ) ),
|
||||
'unlimited' => '*',
|
||||
'unlimitedLang' => 'everyone',
|
||||
'impliedUnlimited' => TRUE
|
||||
@@ -223,8 +255,8 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
{
|
||||
$values['googleauth_groups'] = ( $values['googleauth_groups'] == '*' ) ? '*' : implode( ',', $values['googleauth_groups'] );
|
||||
$form->saveAsSettings( $values );
|
||||
\IPS\Session::i()->log( 'acplogs__mfa_handler_enabled', array( "mfa_google_title" => TRUE ) );
|
||||
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=settings&controller=mfa' ), 'saved' );
|
||||
Session::i()->log( 'acplogs__mfa_handler_enabled', array( "mfa_google_title" => TRUE ) );
|
||||
Output::i()->redirect( Url::internal( 'app=core&module=settings&controller=mfa' ), 'saved' );
|
||||
}
|
||||
|
||||
return (string) $form;
|
||||
@@ -235,10 +267,10 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* If member has configured this handler, disable it
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
* @param Member $member The member
|
||||
* @return void
|
||||
*/
|
||||
public function disableHandlerForMember( \IPS\Member $member )
|
||||
public function disableHandlerForMember( Member $member ): void
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
unset( $mfaDetails['google'] );
|
||||
@@ -254,23 +286,23 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* Verify a submitted code with a ±30 seconds leeway
|
||||
*
|
||||
* @param string $submittedCode The code that was submitted
|
||||
* @param string $secret The secret key
|
||||
* @param \IPS\Member $member The member this is for
|
||||
* @param string $submittedCode The code that was submitted
|
||||
* @param string $secret The secret key
|
||||
* @param Member $member The member this is for
|
||||
* @return int|FALSE
|
||||
*/
|
||||
protected static function checkSubmittedCode( $submittedCode, $secret, $member )
|
||||
protected static function checkSubmittedCode(string $submittedCode, string $secret, Member $member ): int|FALSE
|
||||
{
|
||||
$submittedCode = str_replace( ' ', '', $submittedCode );
|
||||
|
||||
$validTimes = array( new \IPS\DateTime(), ( new \IPS\DateTime() )->add( new \DateInterval('PT30S') ), ( new \IPS\DateTime() )->sub( new \DateInterval('PT30S') ) );
|
||||
$blockedTimes = iterator_to_array( \IPS\Db::i()->select( 'time', 'core_googleauth_used_codes', array( '`member`=?', $member->member_id ) ) );
|
||||
$validTimes = array( new DateTime(), ( new DateTime() )->add( new DateInterval('PT30S') ), ( new DateTime() )->sub( new DateInterval('PT30S') ) );
|
||||
$blockedTimes = iterator_to_array( Db::i()->select( 'time', 'core_googleauth_used_codes', array( '`member`=?', $member->member_id ) ) );
|
||||
|
||||
$allowedCodes = array();
|
||||
foreach ( $validTimes as $time )
|
||||
{
|
||||
$codeTime = floor( $time->getTimestamp() / 30 );
|
||||
if ( !\in_array( $codeTime, $blockedTimes ) )
|
||||
if ( !in_array( $codeTime, $blockedTimes ) )
|
||||
{
|
||||
$allowedCodes[ static::getCodeForSecretAtTime( $secret, $time ) ] = $codeTime;
|
||||
}
|
||||
@@ -278,7 +310,7 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
|
||||
foreach ( $allowedCodes as $code => $time )
|
||||
{
|
||||
if ( \IPS\Login::compareHashes( (string) $code, $submittedCode ) )
|
||||
if ( Login::compareHashes( (string) $code, $submittedCode ) )
|
||||
{
|
||||
return $time;
|
||||
}
|
||||
@@ -290,11 +322,11 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* Get the code
|
||||
*
|
||||
* @param string $secret The secret key for the user
|
||||
* @param \IPS\DateTime $time Timestamp
|
||||
* @param string $secret The secret key for the user
|
||||
* @param DateTime $time Timestamp
|
||||
* @return string
|
||||
*/
|
||||
protected static function getCodeForSecretAtTime( $secret, \IPS\DateTime $time )
|
||||
protected static function getCodeForSecretAtTime( string $secret, DateTime $time ): string
|
||||
{
|
||||
/* Decode secret key */
|
||||
$secret = str_split( str_replace( '=', '', $secret ) );
|
||||
@@ -308,17 +340,17 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
$block .= str_pad( base_convert( $chars[ $secret[ $i + $j ] ], 10, 2 ), 5, '0', STR_PAD_LEFT );
|
||||
}
|
||||
$eightBits = str_split( $block, 8 );
|
||||
for ( $z = 0; $z < \count( $eightBits ); ++$z )
|
||||
for ( $z = 0; $z < count( $eightBits ); ++$z )
|
||||
{
|
||||
$decodedSecretKey .= ( ( $y = \chr( base_convert( $eightBits[ $z ], 2, 10) ) ) || \ord( $y ) == 48) ? $y : '';
|
||||
$decodedSecretKey .= ( ( $y = chr( base_convert( $eightBits[ $z ], 2, 10) ) ) || ord( $y ) == 48) ? $y : '';
|
||||
}
|
||||
}
|
||||
|
||||
/* Hash the timestamp with the secret key */
|
||||
$hash = hash_hmac('SHA1', \chr(0).\chr(0).\chr(0).\chr(0).pack('N*', floor( $time->getTimestamp() / 30 ) ), $decodedSecretKey, true);
|
||||
$hash = hash_hmac('SHA1', chr(0). chr(0). chr(0). chr(0).pack('N*', floor( $time->getTimestamp() / 30 ) ), $decodedSecretKey, true);
|
||||
|
||||
/* Unpack it */
|
||||
$value = unpack( 'N', \substr( $hash, \ord( \substr( $hash, -1 ) ) & 0x0F, 4 ) );
|
||||
$value = unpack( 'N', substr( $hash, ord( substr( $hash, -1 ) ) & 0x0F, 4 ) );
|
||||
$value = $value[1];
|
||||
|
||||
/* Get 32 bits */
|
||||
|
||||
+121
-97
@@ -11,16 +11,36 @@
|
||||
namespace IPS\MFA;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use DateInterval;
|
||||
use IPS\Application;
|
||||
use IPS\DateTime;
|
||||
use IPS\Db;
|
||||
use IPS\Helpers\Form\YesNo;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Member;
|
||||
use IPS\MFA\Verify\Handler;
|
||||
use IPS\Output;
|
||||
use IPS\Request;
|
||||
use IPS\Session;
|
||||
use IPS\Settings;
|
||||
use IPS\Theme;
|
||||
use function count;
|
||||
use function defined;
|
||||
use function in_array;
|
||||
use const IPS\DEV_FORCE_MFA;
|
||||
use const IPS\DISABLE_MFA;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract Multi Factor Authentication Handler and Factory
|
||||
*/
|
||||
abstract class _MFAHandler
|
||||
abstract class MFAHandler
|
||||
{
|
||||
/* !Access Methods */
|
||||
|
||||
@@ -29,10 +49,10 @@ abstract class _MFAHandler
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function areas()
|
||||
public static function areas(): array
|
||||
{
|
||||
$return = array();
|
||||
foreach ( \IPS\Application::allExtensions( 'core', 'MFAArea', FALSE, 'core', NULL, FALSE ) as $k => $v )
|
||||
foreach ( Application::allExtensions( 'core', 'MFAArea', FALSE, 'core', NULL, FALSE ) as $k => $v )
|
||||
{
|
||||
$return[ $k ] = "MFA_{$k}";
|
||||
}
|
||||
@@ -44,13 +64,13 @@ abstract class _MFAHandler
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function handlers()
|
||||
public static function handlers(): array
|
||||
{
|
||||
return array(
|
||||
'authy' => new \IPS\MFA\Authy\Handler(),
|
||||
'google' => new \IPS\MFA\GoogleAuthenticator\Handler(),
|
||||
'questions' => new \IPS\MFA\SecurityQuestions\Handler(),
|
||||
'verify' => new \IPS\MFA\Verify\Handler()
|
||||
'authy' => new Authy\Handler(),
|
||||
'google' => new GoogleAuthenticator\Handler(),
|
||||
'questions' => new SecurityQuestions\Handler(),
|
||||
'verify' => new Handler()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,7 +79,7 @@ abstract class _MFAHandler
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function resetAuthentication()
|
||||
public static function resetAuthentication() : void
|
||||
{
|
||||
if ( isset( $_SESSION['MFAAuthenticated'] ) )
|
||||
{
|
||||
@@ -70,44 +90,44 @@ abstract class _MFAHandler
|
||||
/**
|
||||
* 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
|
||||
* @param string $app The application which owns the MFAArea extension
|
||||
* @param string $area The MFAArea key
|
||||
* @param Url $url URL for page
|
||||
* @param Member|null $member The member, or NULL for currently logged in member
|
||||
* @return string|null
|
||||
*/
|
||||
public static function accessToArea( $app, $area, \IPS\Http\Url $url, \IPS\Member $member = NULL )
|
||||
public static function accessToArea( string $app, string $area, Url $url, ?Member $member = NULL ): ?string
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
$member = $member ?: Member::loggedIn();
|
||||
|
||||
/* Constant to disable MFA for emergency recovery */
|
||||
if ( \IPS\DISABLE_MFA )
|
||||
if ( 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 ) ) )
|
||||
if ( !Settings::i()->security_questions_areas or !in_array( "{$app}_{$area}", explode( ',', 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() ) ) ) )
|
||||
if ( !DEV_FORCE_MFA and ( isset( $_SESSION['MFAAuthenticated'] ) and ( !Settings::i()->security_questions_timer or ( ( $_SESSION['MFAAuthenticated'] + ( 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 ( Settings::i()->mfa_required_groups != '*' and !$member->inGroup( explode( ',', 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' )
|
||||
if ( isset( Request::i()->_mfa ) and Request::i()->_mfa == 'optout' )
|
||||
{
|
||||
\IPS\Session::i()->csrfCheck();
|
||||
Session::i()->csrfCheck();
|
||||
|
||||
$member->members_bitoptions['security_questions_opt_out'] = TRUE;
|
||||
$member->save();
|
||||
@@ -137,15 +157,15 @@ abstract class _MFAHandler
|
||||
/* 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' ) );
|
||||
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( '2fa.css', 'core', 'global' ) );
|
||||
return $lockedOutScreen;
|
||||
}
|
||||
|
||||
/* "Try another way to sign in" */
|
||||
if ( isset( \IPS\Request::i()->_mfa ) )
|
||||
if ( isset( 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' )
|
||||
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( '2fa.css', 'core', 'global' ) );
|
||||
if ( Request::i()->_mfa == 'alt' )
|
||||
{
|
||||
/* What handlers have we configured? */
|
||||
$configuredHandlers = array();
|
||||
@@ -157,26 +177,26 @@ abstract class _MFAHandler
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( \IPS\Request::i()->_mfaMethod ) and array_key_exists( \IPS\Request::i()->_mfaMethod, $configuredHandlers ) )
|
||||
if ( isset( Request::i()->_mfaMethod ) and array_key_exists( Request::i()->_mfaMethod, $configuredHandlers ) )
|
||||
{
|
||||
return static::_showHandlerAuthScreen( $configuredHandlers[ \IPS\Request::i()->_mfaMethod ], $url->setQueryString( array( '_mfa' => 'alt', '_mfaMethod' => \IPS\Request::i()->_mfaMethod ) ), $member );
|
||||
return static::_showHandlerAuthScreen( $configuredHandlers[ Request::i()->_mfaMethod ], $url->setQueryString( array( '_mfa' => 'alt', '_mfaMethod' => 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 ( $app === 'core' and $area === 'AuthenticateFront' and !in_array( "app_AuthenticateFrontKnown", explode( ',', Settings::i()->security_questions_areas ) ) )
|
||||
{
|
||||
if ( \IPS\Db::i()->select( 'COUNT(*)', 'core_members_known_devices', array( 'member_id=?', $member->member_id ) )->first() )
|
||||
if ( 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 );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->mfaRecovery( $configuredHandlers, $url, $knownDevicesAvailable );
|
||||
}
|
||||
elseif ( \IPS\Request::i()->_mfa == 'knownDevice' )
|
||||
elseif ( Request::i()->_mfa == 'knownDevice' )
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'system', 'core' )->mfaKnownDeviceInfo( $url );
|
||||
return Theme::i()->getTemplate( 'system', 'core' )->mfaKnownDeviceInfo( $url );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,24 +205,24 @@ abstract class _MFAHandler
|
||||
{
|
||||
if ( $handler->memberHasConfiguredHandler( $member ) )
|
||||
{
|
||||
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( '2fa.css', 'core', 'global' ) );
|
||||
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, 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' ) )
|
||||
$showSetupForm = ( Settings::i()->mfa_required_groups == '*' or $member->inGroup( explode( ',', Settings::i()->mfa_required_groups ) ) ) ? 'mfa_required_prompt' : 'mfa_optional_prompt';
|
||||
if ( Settings::i()->$showSetupForm === 'access' or ( $app === 'core' and $area === 'AuthenticateAdmin' and Settings::i()->$showSetupForm === 'immediate' ) )
|
||||
{
|
||||
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( '2fa.css', 'core', 'global' ) );
|
||||
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, 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 ) )
|
||||
if ( isset( Request::i()->mfa_setup ) and isset( Request::i()->csrfKey ) )
|
||||
{
|
||||
\IPS\Session::i()->csrfCheck();
|
||||
Session::i()->csrfCheck();
|
||||
foreach ( $acceptableHandlers as $key => $handler )
|
||||
{
|
||||
if ( ( \count( $acceptableHandlers ) == 1 ) or $key == \IPS\Request::i()->mfa_method )
|
||||
if ( ( count( $acceptableHandlers ) == 1 ) or $key == Request::i()->mfa_method )
|
||||
{
|
||||
if ( $handler->configurationScreenSubmit( $member ) )
|
||||
{
|
||||
@@ -218,24 +238,26 @@ abstract class _MFAHandler
|
||||
}
|
||||
|
||||
/* No, show it */
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->mfaSetup( $acceptableHandlers, $member, $url );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->mfaSetup( $acceptableHandlers, $member, $url );
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param MFAHandler $handler The handler to use
|
||||
* @param Url $url URL for page
|
||||
* @param Member $member The member
|
||||
* @return string|null
|
||||
*/
|
||||
protected static function _showHandlerAuthScreen( \IPS\MFA\MFAHandler $handler, \IPS\Http\Url $url, \IPS\Member $member )
|
||||
protected static function _showHandlerAuthScreen( MFAHandler $handler, Url $url, Member $member ): ?string
|
||||
{
|
||||
/* Did we just submit it? */
|
||||
if ( isset( \IPS\Request::i()->mfa_auth ) )
|
||||
if ( isset( Request::i()->mfa_auth ) )
|
||||
{
|
||||
\IPS\Session::i()->csrfCheck();
|
||||
Session::i()->csrfCheck();
|
||||
if ( $handler->authenticationScreenSubmit( $member ) )
|
||||
{
|
||||
$member->failed_mfa_attempts = 0;
|
||||
@@ -248,25 +270,25 @@ abstract class _MFAHandler
|
||||
$member->failed_mfa_attempts++;
|
||||
$member->save();
|
||||
|
||||
\IPS\Request::i()->mfa_auth = NULL;
|
||||
Request::i()->mfa_auth = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* No, show it */
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->mfaAuthenticate( $handler->authenticationScreen( $member, $url ), $url );
|
||||
return 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
|
||||
* @param Member $member The member
|
||||
* @return string|null
|
||||
*/
|
||||
protected static function _lockedOutScreen( \IPS\Member $member )
|
||||
protected static function _lockedOutScreen( Member $member ): ?string
|
||||
{
|
||||
if ( $member->failed_mfa_attempts >= \IPS\Settings::i()->security_questions_tries )
|
||||
if ( $member->failed_mfa_attempts >= Settings::i()->security_questions_tries )
|
||||
{
|
||||
if ( \IPS\Settings::i()->mfa_lockout_behaviour == 'lock' )
|
||||
if ( Settings::i()->mfa_lockout_behaviour == 'lock' )
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
if ( !isset( $mfaDetails['_lockouttime'] ) )
|
||||
@@ -275,10 +297,10 @@ abstract class _MFAHandler
|
||||
$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() ) );
|
||||
$member->logHistory( 'core', 'login', array( 'type' => 'mfalock', 'count' => $member->failed_mfa_attempts, 'unlockTime' => DateTime::create()->add( new DateInterval( 'PT' . Settings::i()->mfa_lockout_time . 'M' ) )->getTimestamp() ) );
|
||||
}
|
||||
|
||||
$lockEndTime = \IPS\DateTime::ts( $mfaDetails['_lockouttime'] )->add( new \DateInterval( 'PT' . \IPS\Settings::i()->mfa_lockout_time . 'M' ) );
|
||||
$lockEndTime = DateTime::ts( $mfaDetails['_lockouttime'] )->add( new DateInterval( 'PT' . Settings::i()->mfa_lockout_time . 'M' ) );
|
||||
if ( $lockEndTime->getTimestamp() < time() )
|
||||
{
|
||||
unset( $mfaDetails['_lockouttime'] );
|
||||
@@ -288,18 +310,20 @@ abstract class _MFAHandler
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->mfaLockout( ( \IPS\Settings::i()->mfa_lockout_time > 1440 ) ? $lockEndTime : $lockEndTime->localeTime( FALSE ) );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->mfaLockout( ( Settings::i()->mfa_lockout_time > 1440 ) ? $lockEndTime : $lockEndTime->localeTime( FALSE ) );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( $member->failed_mfa_attempts == \IPS\Settings::i()->security_questions_tries )
|
||||
if ( $member->failed_mfa_attempts == 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();
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->mfaLockout();
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* !Setup */
|
||||
@@ -309,89 +333,89 @@ abstract class _MFAHandler
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function isEnabled();
|
||||
abstract public function isEnabled(): bool;
|
||||
|
||||
/**
|
||||
* Member *can* use this handler (even if they have not yet configured it)
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function memberCanUseHandler( \IPS\Member $member );
|
||||
abstract public function memberCanUseHandler( Member $member ): bool;
|
||||
|
||||
/**
|
||||
* Member has configured this handler
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function memberHasConfiguredHandler( \IPS\Member $member );
|
||||
abstract public function memberHasConfiguredHandler( Member $member ): bool;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param Member $member The member
|
||||
* @param bool $showingMultipleHandlers Set to TRUE if multiple options are being displayed
|
||||
* @param Url $url URL for page
|
||||
* @return string
|
||||
*/
|
||||
abstract public function configurationScreen( \IPS\Member $member, $showingMultipleHandlers, \IPS\Http\Url $url );
|
||||
abstract public function configurationScreen( Member $member, bool $showingMultipleHandlers, Url $url ): string;
|
||||
|
||||
/**
|
||||
* Submit configuration screen. Return TRUE if was accepted
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function configurationScreenSubmit( \IPS\Member $member );
|
||||
abstract public function configurationScreenSubmit( Member $member ): bool;
|
||||
|
||||
/* !Authentication */
|
||||
|
||||
/**
|
||||
* Get the form for a member to authenticate
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param \IPS\Http\Url $url URL for page
|
||||
* @param Member $member The member
|
||||
* @param Url $url URL for page
|
||||
* @return string
|
||||
*/
|
||||
abstract public function authenticationScreen( \IPS\Member $member, \IPS\Http\Url $url );
|
||||
abstract public function authenticationScreen( Member $member, Url $url ): string;
|
||||
|
||||
/**
|
||||
* Submit authentication screen. Return TRUE if was accepted
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return string
|
||||
* @param Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function authenticationScreenSubmit( \IPS\Member $member );
|
||||
abstract public function authenticationScreenSubmit( Member $member ): bool;
|
||||
|
||||
/* !ACP */
|
||||
|
||||
/**
|
||||
* Toggle
|
||||
*
|
||||
* @param bool $enabled On/Off
|
||||
* @return bool
|
||||
* @param bool $enabled On/Off
|
||||
* @return void
|
||||
*/
|
||||
abstract public function toggle( $enabled );
|
||||
abstract public function toggle( bool $enabled ) : void;
|
||||
|
||||
/**
|
||||
* ACP Settings
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function acpSettings();
|
||||
abstract public function acpSettings(): string;
|
||||
|
||||
/**
|
||||
* Configuration options when editing member account in ACP
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @return array
|
||||
*/
|
||||
public function acpConfiguration( \IPS\Member $member )
|
||||
public function acpConfiguration( Member $member ): array
|
||||
{
|
||||
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( new YesNo( "mfa_{$this->key}_title", $this->memberHasConfiguredHandler( $member ), FALSE, array(), NULL, NULL, NULL, "mfa_{$this->key}_title" ) );
|
||||
}
|
||||
return array();
|
||||
}
|
||||
@@ -399,11 +423,11 @@ abstract class _MFAHandler
|
||||
/**
|
||||
* Save configuration when editing member account in ACP
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param array $values Values from form
|
||||
* @return array
|
||||
* @param Member $member The member
|
||||
* @param array $values Values from form
|
||||
* @return void
|
||||
*/
|
||||
public function acpConfigurationSave( \IPS\Member $member, $values )
|
||||
public function acpConfigurationSave(Member $member, array $values ) : void
|
||||
{
|
||||
if ( isset( $values["mfa_{$this->key}_title"] ) and !$values["mfa_{$this->key}_title"] )
|
||||
{
|
||||
@@ -419,19 +443,19 @@ abstract class _MFAHandler
|
||||
/**
|
||||
* If member has configured this handler, disable it
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
* @param Member $member The member
|
||||
* @return void
|
||||
*/
|
||||
abstract public function disableHandlerForMember( \IPS\Member $member );
|
||||
abstract public function disableHandlerForMember( Member $member ) : void;
|
||||
|
||||
/**
|
||||
* Get title for UCP
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function ucpTitle()
|
||||
public function ucpTitle(): string
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack("mfa_{$this->key}_title");
|
||||
return Member::loggedIn()->language()->addToStack("mfa_{$this->key}_title");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -439,9 +463,9 @@ abstract class _MFAHandler
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function ucpDesc()
|
||||
public function ucpDesc(): string
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack("mfa_{$this->key}_desc_user");
|
||||
return Member::loggedIn()->language()->addToStack("mfa_{$this->key}_desc_user");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -449,9 +473,9 @@ abstract class _MFAHandler
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function recoveryButton()
|
||||
public function recoveryButton(): string
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack("mfa_recovery_{$this->key}");
|
||||
return Member::loggedIn()->language()->addToStack("mfa_recovery_{$this->key}");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,21 +11,49 @@
|
||||
namespace IPS\MFA\SecurityQuestions;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use DomainException;
|
||||
use Exception;
|
||||
use IPS\core\modules\admin\settings\securityquestions;
|
||||
use IPS\Db;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\CheckboxSet;
|
||||
use IPS\Helpers\Form\Matrix;
|
||||
use IPS\Helpers\Form\Number;
|
||||
use IPS\Helpers\Form\Radio;
|
||||
use IPS\Helpers\Form\Select;
|
||||
use IPS\Helpers\Form\Text;
|
||||
use IPS\Helpers\Form\YesNo;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Member;
|
||||
use IPS\Member\Group;
|
||||
use IPS\MFA\MFAHandler;
|
||||
use IPS\Output;
|
||||
use IPS\Request;
|
||||
use IPS\Session;
|
||||
use IPS\Settings;
|
||||
use IPS\Text\Encrypt;
|
||||
use IPS\Theme;
|
||||
use OutOfRangeException;
|
||||
use UnderflowException;
|
||||
use function count;
|
||||
use function defined;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi Factor Authentication Handler for Security Questions
|
||||
*/
|
||||
class _Handler extends \IPS\MFA\MFAHandler
|
||||
class Handler extends MFAHandler
|
||||
{
|
||||
/**
|
||||
* @brief Key
|
||||
*/
|
||||
protected $key = 'questions';
|
||||
protected string $key = 'questions';
|
||||
|
||||
/* !Setup */
|
||||
|
||||
@@ -34,29 +62,29 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled()
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return \IPS\Settings::i()->security_questions_enabled;
|
||||
return Settings::i()->security_questions_enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Member *can* use this handler (even if they have not yet configured it)
|
||||
*
|
||||
* @param \IPS\Member $member Member to check
|
||||
* @param Member $member Member to check
|
||||
* @return bool
|
||||
*/
|
||||
public function memberCanUseHandler( \IPS\Member $member )
|
||||
public function memberCanUseHandler( Member $member ): bool
|
||||
{
|
||||
return \IPS\Settings::i()->security_questions_groups == '*' or $member->inGroup( explode( ',', \IPS\Settings::i()->security_questions_groups ) );
|
||||
return Settings::i()->security_questions_groups == '*' or $member->inGroup( explode( ',', Settings::i()->security_questions_groups ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Member has configured this handler
|
||||
*
|
||||
* @param \IPS\Member $member Member to check
|
||||
* @param Member $member Member to check
|
||||
* @return bool
|
||||
*/
|
||||
public function memberHasConfiguredHandler( \IPS\Member $member )
|
||||
public function memberHasConfiguredHandler( Member $member ): bool
|
||||
{
|
||||
return $member->members_bitoptions['has_security_answers'];
|
||||
}
|
||||
@@ -64,12 +92,12 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* Show a setup screen
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @param bool $showingMultipleHandlers Set to TRUE if multiple options are being displayed
|
||||
* @param \IPS\Http\Url $url URL for page
|
||||
* @param Url $url URL for page
|
||||
* @return string
|
||||
*/
|
||||
public function configurationScreen( \IPS\Member $member, $showingMultipleHandlers, \IPS\Http\Url $url )
|
||||
public function configurationScreen( Member $member, bool $showingMultipleHandlers, Url $url ): string
|
||||
{
|
||||
$securityQuestions = array();
|
||||
foreach ( Question::roots() as $question )
|
||||
@@ -77,34 +105,34 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
$securityQuestions[ $question->id ] = $question->_title;
|
||||
}
|
||||
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->securityQuestionsSetup( $securityQuestions, $showingMultipleHandlers );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->securityQuestionsSetup( $securityQuestions, $showingMultipleHandlers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit configuration screen. Return TRUE if was accepted
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function configurationScreenSubmit( \IPS\Member $member )
|
||||
public function configurationScreenSubmit( Member $member ): bool
|
||||
{
|
||||
$answers = array();
|
||||
|
||||
$isReconfiguring = $this->memberHasConfiguredHandler( $member );
|
||||
|
||||
foreach ( \IPS\Request::i()->security_question as $k => $v )
|
||||
foreach ( Request::i()->security_question as $k => $v )
|
||||
{
|
||||
$answers[ $v ] = array(
|
||||
'answer_question_id' => $v,
|
||||
'answer_member_id' => $member->member_id,
|
||||
'answer_answer' => \IPS\Text\Encrypt::fromPlaintext( \IPS\Request::i()->security_answer[ $k ] )->tag()
|
||||
'answer_answer' => Encrypt::fromPlaintext( Request::i()->security_answer[ $k ] )->tag()
|
||||
);
|
||||
}
|
||||
|
||||
if ( \count( $answers ) >= \IPS\Settings::i()->security_questions_number )
|
||||
if ( count( $answers ) >= Settings::i()->security_questions_number )
|
||||
{
|
||||
\IPS\Db::i()->delete( 'core_security_answers', array( 'answer_member_id=?', $member->member_id ) );
|
||||
\IPS\Db::i()->insert( 'core_security_answers', $answers );
|
||||
Db::i()->delete( 'core_security_answers', array( 'answer_member_id=?', $member->member_id ) );
|
||||
Db::i()->insert( 'core_security_answers', $answers );
|
||||
|
||||
$member->members_bitoptions['has_security_answers'] = TRUE;
|
||||
$member->save();
|
||||
@@ -123,59 +151,59 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* Get the form for a member to authenticate
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param \IPS\Http\Url $url URL for page
|
||||
* @param Member $member The member
|
||||
* @param Url $url URL for page
|
||||
* @return string
|
||||
*/
|
||||
public function authenticationScreen( \IPS\Member $member, \IPS\Http\Url $url )
|
||||
public function authenticationScreen( Member $member, Url $url ): string
|
||||
{
|
||||
try
|
||||
{
|
||||
$chosenAnswer = \IPS\Db::i()->select( '*', 'core_security_answers', array( 'answer_member_id=? AND answer_is_chosen=1', $member->member_id ) )->first();
|
||||
$chosenAnswer = Db::i()->select( '*', 'core_security_answers', array( 'answer_member_id=? AND answer_is_chosen=1', $member->member_id ) )->first();
|
||||
$chosenQuestion = Question::load( $chosenAnswer['answer_question_id'] );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
catch ( Exception $e )
|
||||
{
|
||||
$chosenQuestion = NULL;
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_security_answers', array( 'answer_member_id=?', $member->member_id ), 'RAND()' ) as $chosenAnswer )
|
||||
foreach ( Db::i()->select( '*', 'core_security_answers', array( 'answer_member_id=?', $member->member_id ), 'RAND()' ) as $chosenAnswer )
|
||||
{
|
||||
try
|
||||
{
|
||||
$chosenQuestion = Question::load( $chosenAnswer['answer_question_id'] );
|
||||
\IPS\Db::i()->update( 'core_security_answers', array( 'answer_is_chosen' => 1 ), array( 'answer_member_id=? AND answer_question_id=?', $member->member_id, $chosenQuestion->id ) );
|
||||
Db::i()->update( 'core_security_answers', array( 'answer_is_chosen' => 1 ), array( 'answer_member_id=? AND answer_question_id=?', $member->member_id, $chosenQuestion->id ) );
|
||||
break;
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) { }
|
||||
catch ( OutOfRangeException $e ) { }
|
||||
}
|
||||
if ( !$chosenQuestion )
|
||||
{
|
||||
return NULL;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->securityQuestionsAuth( $chosenQuestion );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->securityQuestionsAuth( $chosenQuestion );
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit authentication screen. Return TRUE if was accepted
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return string
|
||||
* @param Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function authenticationScreenSubmit( \IPS\Member $member )
|
||||
public function authenticationScreenSubmit( Member $member ): bool
|
||||
{
|
||||
try
|
||||
{
|
||||
$authenticated = \IPS\Request::i()->security_answer == \IPS\Text\Encrypt::fromTag( \IPS\Db::i()->select( 'answer_answer', 'core_security_answers', array( 'answer_member_id=? AND answer_is_chosen=1', $member->member_id ) )->first() )->decrypt();
|
||||
$authenticated = Request::i()->security_answer == Encrypt::fromTag( Db::i()->select( 'answer_answer', 'core_security_answers', array( 'answer_member_id=? AND answer_is_chosen=1', $member->member_id ) )->first() )->decrypt();
|
||||
|
||||
if( $authenticated )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_security_answers', array( 'answer_is_chosen' => 0 ), array( 'answer_member_id=? AND answer_is_chosen=?', $member->member_id, 1 ) );
|
||||
Db::i()->update( 'core_security_answers', array( 'answer_is_chosen' => 0 ), array( 'answer_member_id=? AND answer_is_chosen=?', $member->member_id, 1 ) );
|
||||
}
|
||||
|
||||
return $authenticated;
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
catch ( UnderflowException $e )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
@@ -187,20 +215,20 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
* Toggle
|
||||
*
|
||||
* @param bool $enabled On/Off
|
||||
* @return bool
|
||||
* @return void
|
||||
*/
|
||||
public function toggle( $enabled )
|
||||
public function toggle( bool $enabled ) : void
|
||||
{
|
||||
if( $enabled )
|
||||
{
|
||||
/* Check we have some questions */
|
||||
if ( !count( \IPS\MFA\SecurityQuestions\Question::roots() ) )
|
||||
if ( !count( Question::roots() ) )
|
||||
{
|
||||
throw new \DomainException( 'no_questions' );
|
||||
throw new DomainException( 'no_questions' );
|
||||
}
|
||||
}
|
||||
|
||||
\IPS\Settings::i()->changeValues( array( 'security_questions_enabled' => $enabled ) );
|
||||
Settings::i()->changeValues( array( 'security_questions_enabled' => $enabled ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,7 +236,7 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function acpSettings()
|
||||
public function acpSettings(): string
|
||||
{
|
||||
/* Init */
|
||||
$activeTabContents = '';
|
||||
@@ -216,21 +244,21 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
'settings' => 'security_question_settings',
|
||||
'questions' => 'security_questions_questions'
|
||||
);
|
||||
$activeTab = ( isset( \IPS\Request::i()->tab ) and array_key_exists( \IPS\Request::i()->tab, $tabs ) ) ? \IPS\Request::i()->tab : 'handlers';
|
||||
$activeTab = ( isset( Request::i()->tab ) and array_key_exists( Request::i()->tab, $tabs ) ) ? Request::i()->tab : 'handlers';
|
||||
|
||||
/* Settings Form */
|
||||
if ( $activeTab === 'settings' )
|
||||
{
|
||||
$form = new \IPS\Helpers\Form;
|
||||
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'security_questions_groups', \IPS\Settings::i()->security_questions_groups == '*' ? '*' : explode( ',', \IPS\Settings::i()->security_questions_groups ), FALSE, array(
|
||||
$form = new Form;
|
||||
$form->add( new CheckboxSet( 'security_questions_groups', Settings::i()->security_questions_groups == '*' ? '*' : explode( ',', Settings::i()->security_questions_groups ), FALSE, array(
|
||||
'multiple' => TRUE,
|
||||
'options' => array_combine( array_keys( \IPS\Member\Group::groups() ), array_map( function( $_group ) { return (string) $_group; }, \IPS\Member\Group::groups() ) ),
|
||||
'options' => array_combine( array_keys( Group::groups() ), array_map( function( $_group ) { return (string) $_group; }, Group::groups() ) ),
|
||||
'unlimited' => '*',
|
||||
'unlimitedLang' => 'everyone',
|
||||
'impliedUnlimited' => TRUE
|
||||
), NULL, NULL, NULL, 'security_questions_groups' ) );
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'security_questions_prompt', \IPS\Settings::i()->security_questions_prompt, FALSE, array( 'options' => array( 'register' => 'security_questions_prompt_register', 'optional' => 'security_questions_prompt_optional', 'access' => 'security_questions_prompt_access' ) ), NULL, NULL, NULL, 'security_questions_prompt' ) );
|
||||
$form->add( new \IPS\Helpers\Form\Number( 'security_questions_number', \IPS\Settings::i()->security_questions_number, FALSE, array( 'min' => 1, 'max' => \IPS\Db::i()->select( 'COUNT(*)', 'core_security_questions' )->first() ?: NULL ), NULL, NULL, NULL, 'security_questions_number' ) );
|
||||
$form->add( new Radio( 'security_questions_prompt', Settings::i()->security_questions_prompt, FALSE, array( 'options' => array( 'register' => 'security_questions_prompt_register', 'optional' => 'security_questions_prompt_optional', 'access' => 'security_questions_prompt_access' ) ), NULL, NULL, NULL, 'security_questions_prompt' ) );
|
||||
$form->add( new Number( 'security_questions_number', Settings::i()->security_questions_number, FALSE, array( 'min' => 1, 'max' => Db::i()->select( 'COUNT(*)', 'core_security_questions' )->first() ?: NULL ), NULL, NULL, NULL, 'security_questions_number' ) );
|
||||
|
||||
$form->addMessage('nexus_mfa_reset_answers');
|
||||
|
||||
@@ -238,8 +266,8 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
{
|
||||
$values['security_questions_groups'] = ( $values['security_questions_groups'] == '*' ) ? '*' : implode( ',', $values['security_questions_groups'] );
|
||||
$form->saveAsSettings( $values );
|
||||
\IPS\Session::i()->log( 'acplogs__mfa_handler_enabled', array( "mfa_questions_title" => TRUE ) );
|
||||
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=settings&controller=mfa' ), 'saved' );
|
||||
Session::i()->log( 'acplogs__mfa_handler_enabled', array( "mfa_questions_title" => TRUE ) );
|
||||
Output::i()->redirect( Url::internal( 'app=core&module=settings&controller=mfa' ), 'saved' );
|
||||
}
|
||||
|
||||
$activeTabContents = (string) $form;
|
||||
@@ -248,49 +276,49 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/* Questions table */
|
||||
else
|
||||
{
|
||||
$controller = new \IPS\core\modules\admin\settings\securityquestions;
|
||||
$controller = new securityquestions;
|
||||
$controller->execute();
|
||||
$activeTabContents = \IPS\Output::i()->output;
|
||||
$activeTabContents = Output::i()->output;
|
||||
}
|
||||
|
||||
|
||||
/* Output */
|
||||
if( \IPS\Request::i()->isAjax() )
|
||||
if( Request::i()->isAjax() )
|
||||
{
|
||||
return $activeTabContents;
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'global' )->tabs( $tabs, $activeTab, $activeTabContents, \IPS\Http\Url::internal( "app=core&module=settings&controller=mfa&tab=handlers&do=settings&key=questions" ) );
|
||||
return Theme::i()->getTemplate( 'global' )->tabs( $tabs, $activeTab, $activeTabContents, Url::internal( "app=core&module=settings&controller=mfa&tab=handlers&do=settings&key=questions" ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options when editing member account in ACP
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @return array
|
||||
*/
|
||||
public function acpConfiguration( \IPS\Member $member )
|
||||
public function acpConfiguration( Member $member ): array
|
||||
{
|
||||
$return = array();
|
||||
$return[] = new \IPS\Helpers\Form\YesNo( "mfa_{$this->key}_title", $this->memberHasConfiguredHandler( $member ), FALSE, array( 'togglesOn' => array( 'security_question_matrix' ) ) );
|
||||
$return[] = new YesNo( "mfa_{$this->key}_title", $this->memberHasConfiguredHandler( $member ), FALSE, array( 'togglesOn' => array( 'security_question_matrix' ) ) );
|
||||
|
||||
$securityQuestions = array();
|
||||
foreach ( \IPS\MFA\SecurityQuestions\Question::roots() as $question )
|
||||
foreach (Question::roots() as $question )
|
||||
{
|
||||
$securityQuestions[ $question->id ] = $question->_title;
|
||||
}
|
||||
|
||||
$matrix = new \IPS\Helpers\Form\Matrix('security_question_matrix');
|
||||
$matrix = new Matrix('security_question_matrix');
|
||||
$matrix->columns = array(
|
||||
'security_question_q' => function( $key, $value, $data ) use ( $securityQuestions )
|
||||
{
|
||||
return new \IPS\Helpers\Form\Select( $key, $value, FALSE, array( 'options' => $securityQuestions ) );
|
||||
return new Select( $key, $value, FALSE, array( 'options' => $securityQuestions ) );
|
||||
},
|
||||
'security_question_a' => function( $key, $value, $data )
|
||||
{
|
||||
return new \IPS\Helpers\Form\Text( $key, $value );
|
||||
return new Text( $key, $value );
|
||||
},
|
||||
);
|
||||
|
||||
@@ -298,7 +326,7 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
{
|
||||
$matrix->rows[] = array(
|
||||
'security_question_q' => $questionId,
|
||||
'security_question_a' => \IPS\Text\Encrypt::fromTag( $answer )->decrypt()
|
||||
'security_question_a' => Encrypt::fromTag( $answer )->decrypt()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -311,11 +339,11 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* Save configuration when editing member account in ACP
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param array $values Values from form
|
||||
* @return array
|
||||
* @param Member $member The member
|
||||
* @param array $values Values from form
|
||||
* @return void
|
||||
*/
|
||||
public function acpConfigurationSave( \IPS\Member $member, $values )
|
||||
public function acpConfigurationSave( Member $member, array $values ): void
|
||||
{
|
||||
if ( isset( $values["mfa_{$this->key}_title"] ) and !$values["mfa_{$this->key}_title"] )
|
||||
{
|
||||
@@ -326,7 +354,7 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
return;
|
||||
}
|
||||
|
||||
\IPS\Db::i()->delete( 'core_security_answers', array( 'answer_member_id=?', $member->member_id ) );
|
||||
Db::i()->delete( 'core_security_answers', array( 'answer_member_id=?', $member->member_id ) );
|
||||
|
||||
$toInsert = array();
|
||||
|
||||
@@ -337,17 +365,17 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
$toInsert[ $row['security_question_q'] ] = array(
|
||||
'answer_question_id' => $row['security_question_q'],
|
||||
'answer_member_id' => $member->member_id,
|
||||
'answer_answer' => (string) \IPS\Text\Encrypt::fromPlaintext( $row['security_question_a'] )->tag()
|
||||
'answer_answer' => Encrypt::fromPlaintext( $row['security_question_a'] )->tag()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( \count( $toInsert ) )
|
||||
if ( count( $toInsert ) )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_security_answers', $toInsert );
|
||||
Db::i()->insert( 'core_security_answers', $toInsert );
|
||||
}
|
||||
|
||||
if ( \count( $toInsert ) >= \IPS\Settings::i()->security_questions_number )
|
||||
if ( count( $toInsert ) >= Settings::i()->security_questions_number )
|
||||
{
|
||||
$member->members_bitoptions['has_security_answers'] = TRUE;
|
||||
}
|
||||
@@ -362,12 +390,12 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* If member has configured this handler, disable it
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @return void
|
||||
*/
|
||||
public function disableHandlerForMember( \IPS\Member $member )
|
||||
public function disableHandlerForMember( Member $member ) : void
|
||||
{
|
||||
\IPS\Db::i()->delete( 'core_security_answers', array( 'answer_member_id=?', $member->member_id ) );
|
||||
Db::i()->delete( 'core_security_answers', array( 'answer_member_id=?', $member->member_id ) );
|
||||
$member->members_bitoptions['has_security_answers'] = FALSE;
|
||||
$member->save();
|
||||
|
||||
|
||||
@@ -11,56 +11,63 @@
|
||||
namespace IPS\MFA\SecurityQuestions;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Translatable;
|
||||
use IPS\Lang;
|
||||
use IPS\Node\Model;
|
||||
use function defined;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Security Question Node
|
||||
*/
|
||||
class _Question extends \IPS\Node\Model
|
||||
class Question extends Model
|
||||
{
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Store
|
||||
*/
|
||||
protected static $multitons;
|
||||
protected static array $multitons;
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Table
|
||||
*/
|
||||
public static $databaseTable = 'core_security_questions';
|
||||
public static ?string $databaseTable = 'core_security_questions';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Prefix
|
||||
*/
|
||||
public static $databasePrefix = 'question_';
|
||||
public static string $databasePrefix = 'question_';
|
||||
|
||||
/**
|
||||
* @brief [Node] Order Database Column
|
||||
*/
|
||||
public static $databaseColumnOrder = 'order';
|
||||
public static ?string $databaseColumnOrder = 'order';
|
||||
|
||||
/**
|
||||
* @brief [Node] Node Title
|
||||
*/
|
||||
public static $nodeTitle = 'security_questions';
|
||||
public static string $nodeTitle = 'security_questions';
|
||||
|
||||
/**
|
||||
* @brief [Node] Title prefix. If specified, will look for a language key with "{$key}_title" as the key
|
||||
*/
|
||||
public static $titleLangPrefix = 'security_question_';
|
||||
public static ?string $titleLangPrefix = 'security_question_';
|
||||
|
||||
/**
|
||||
* [Node] Add/Edit Form
|
||||
*
|
||||
* @param \IPS\Helpers\Form $form The form
|
||||
* @param Form $form The form
|
||||
* @return void
|
||||
*/
|
||||
public function form( &$form )
|
||||
public function form( Form &$form ) : void
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Translatable( 'security_question_title', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? "security_question_{$this->id}" : NULL ) ) ) );
|
||||
$form->add( new Translatable( 'security_question_title', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? "security_question_{$this->id}" : NULL ) ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,14 +76,14 @@ class _Question extends \IPS\Node\Model
|
||||
* @param array $values Values from the form
|
||||
* @return array
|
||||
*/
|
||||
public function formatFormValues( $values )
|
||||
public function formatFormValues( array $values ): array
|
||||
{
|
||||
if( !$this->id )
|
||||
{
|
||||
$this->save();
|
||||
}
|
||||
|
||||
\IPS\Lang::saveCustom( 'core', "security_question_{$this->id}", $values['security_question_title'] );
|
||||
Lang::saveCustom( 'core', "security_question_{$this->id}", $values['security_question_title'] );
|
||||
unset( $values['security_question_title'] );
|
||||
|
||||
return $values;
|
||||
|
||||
@@ -13,17 +13,21 @@
|
||||
namespace IPS\MFA\Verify;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use DomainException;
|
||||
use Throwable;
|
||||
|
||||
if( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
class _Exception extends \DomainException
|
||||
class Exception extends DomainException
|
||||
{
|
||||
const EXPIRED_CODE = 20404;
|
||||
|
||||
public function __construct( string $message = "", int $code = 0, ?\Throwable $previous = null )
|
||||
public function __construct( string $message = "", int $code = 0, ?Throwable $previous = null )
|
||||
{
|
||||
switch( $code )
|
||||
{
|
||||
|
||||
+116
-95
@@ -14,49 +14,70 @@ namespace IPS\MFA\Verify;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\MFA\verify\Exception;
|
||||
use DomainException;
|
||||
use IPS\DateTime;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Address;
|
||||
use IPS\Helpers\Form\CheckboxSet;
|
||||
use IPS\Helpers\Form\Custom;
|
||||
use IPS\Helpers\Form\Text;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Log;
|
||||
use IPS\Member;
|
||||
use IPS\Member\Group;
|
||||
use IPS\MFA\MFAHandler;
|
||||
use IPS\MFA\Verify\Exception as VerifyException;
|
||||
use IPS\Output;
|
||||
use IPS\Request;
|
||||
use IPS\Session;
|
||||
use IPS\Settings;
|
||||
use IPS\Theme;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function strpos;
|
||||
use function substr;
|
||||
|
||||
if( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
class _Handler extends \IPS\MFA\MFAHandler
|
||||
class Handler extends MFAHandler
|
||||
{
|
||||
/**
|
||||
* @brief Key
|
||||
*/
|
||||
protected $key = 'verify';
|
||||
protected string $key = 'verify';
|
||||
|
||||
/**
|
||||
* Handler is enabled
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled()
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return \IPS\Settings::i()->verify_enabled;
|
||||
return Settings::i()->verify_enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Member *can* use this handler (even if they have not yet configured it)
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function memberCanUseHandler( \IPS\Member $member )
|
||||
public function memberCanUseHandler( Member $member ): bool
|
||||
{
|
||||
return \IPS\Settings::i()->verify_groups == '*' or $member->inGroup( explode( ',', \IPS\Settings::i()->verify_groups ) );
|
||||
return Settings::i()->verify_groups == '*' or $member->inGroup( explode( ',', Settings::i()->verify_groups ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Member has configured this handler
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function memberHasConfiguredHandler( \IPS\Member $member )
|
||||
public function memberHasConfiguredHandler( Member $member ): bool
|
||||
{
|
||||
return isset( $member->mfa_details['verify'] ) and $member->mfa_details['verify']['setup'];
|
||||
}
|
||||
@@ -64,17 +85,17 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* Show a setup screen
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @param bool $showingMultipleHandlers Set to TRUE if multiple options are being displayed
|
||||
* @param \IPS\Http\Url $url URL for page
|
||||
* @param Url $url URL for page
|
||||
* @return string
|
||||
*/
|
||||
public function configurationScreen( \IPS\Member $member, $showingMultipleHandlers, \IPS\Http\Url $url )
|
||||
public function configurationScreen( Member $member, bool $showingMultipleHandlers, Url $url ): string
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
|
||||
/* Starting again? */
|
||||
if ( isset( $mfaDetails['verify']['pendingId'] ) and isset( \IPS\Request::i()->_new ) )
|
||||
if ( isset( $mfaDetails['verify']['pendingId'] ) and isset( Request::i()->_new ) )
|
||||
{
|
||||
unset( $mfaDetails['verify']['pendingId'] );
|
||||
$member->mfa_details = $mfaDetails;
|
||||
@@ -85,45 +106,45 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
if ( isset( $mfaDetails['verify'] ) and isset( $mfaDetails['verify']['pendingId'] ) and !isset( $_SESSION['verifyConfigureError'] ) )
|
||||
{
|
||||
/* Asking for a text or call instead? */
|
||||
$availableMethods = explode( ',', \IPS\Settings::i()->verify_setup );
|
||||
if ( isset( \IPS\Request::i()->verify_method ) and $mfaDetails['verify']['setupMethod'] == 'verify' and \in_array( \IPS\Request::i()->verify_method, $availableMethods ) )
|
||||
$availableMethods = explode( ',', Settings::i()->verify_setup );
|
||||
if ( isset( Request::i()->verify_method ) and $mfaDetails['verify']['setupMethod'] == 'verify' and in_array( Request::i()->verify_method, $availableMethods ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
/* Verify with the API */
|
||||
$response = static::_api( "Services/" . \IPS\Settings::i()->verify_service_sid . "/VerificationCheck", 'post', array(
|
||||
$response = static::_api( "Services/" . Settings::i()->verify_service_sid . "/VerificationCheck", 'post', array(
|
||||
'VerificationSid' => $mfaDetails['verify']['pendingId'],
|
||||
'Code' => \IPS\Request::i()->verify_auth_code
|
||||
'Code' => Request::i()->verify_auth_code
|
||||
) );
|
||||
|
||||
if( $response['status'] == 'approved' )
|
||||
{
|
||||
/* Update details */
|
||||
$mfaDetails['verify']['setupMethod'] = \IPS\Request::i()->verify_method;
|
||||
$mfaDetails['verify']['setupMethod'] = Request::i()->verify_method;
|
||||
$member->mfa_details = $mfaDetails;
|
||||
$member->save();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \IPS\MFA\Verify\Exception( 'verify_mfa_invalid_code' );
|
||||
throw new VerifyException( 'verify_mfa_invalid_code' );
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'verify' );
|
||||
Log::log( $e, 'verify' );
|
||||
$_SESSION['verifyAuthError'] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/* Display */
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->verifyAuthenticate( $mfaDetails['verify']['setupMethod'], TRUE, $_SESSION['verifyAuthError'] ?? 'verify_error', TRUE, explode( ',', \IPS\Settings::i()->verify_setup ), $url );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->verifyAuthenticate( $mfaDetails['verify']['setupMethod'], TRUE, $_SESSION['verifyAuthError'] ?? 'verify_error', TRUE, explode( ',', Settings::i()->verify_setup ), $url );
|
||||
}
|
||||
else
|
||||
{
|
||||
/* If they have used their allowed attempts, make them wait */
|
||||
if ( isset( $mfaDetails['verify'] ) and isset( $mfaDetails['verify']['changeAttempts'] ) and $mfaDetails['verify']['changeAttempts'] >= \IPS\Settings::i()->verify_setup_tries )
|
||||
if ( isset( $mfaDetails['verify'] ) and isset( $mfaDetails['verify']['changeAttempts'] ) and $mfaDetails['verify']['changeAttempts'] >= Settings::i()->verify_setup_tries )
|
||||
{
|
||||
$lockEndTime = $mfaDetails['verify']['lastChangeAttempt'] + ( \IPS\Settings::i()->verify_setup_lockout * 3600 );
|
||||
$lockEndTime = $mfaDetails['verify']['lastChangeAttempt'] + ( Settings::i()->verify_setup_lockout * 3600 );
|
||||
if ( $lockEndTime < time() )
|
||||
{
|
||||
$mfaDetails['verify']['changeAttempts'] = 0;
|
||||
@@ -132,35 +153,35 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->verifySetupLockout( $showingMultipleHandlers, \IPS\DateTime::ts( $lockEndTime ) );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->verifySetupLockout( $showingMultipleHandlers, DateTime::ts( $lockEndTime ) );
|
||||
}
|
||||
}
|
||||
|
||||
/* Otherwise show the form */
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->verifySetup( \IPS\Request::i()->countryCode ?? \IPS\Helpers\Form\Address::calculateDefaultCountry(), \IPS\Request::i()->phoneNumber ?? '', $showingMultipleHandlers, explode( ',', \IPS\Settings::i()->verify_setup ), $_SESSION['verifyConfigureError'] ?? NULL );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->verifySetup( Request::i()->countryCode ?? Address::calculateDefaultCountry(), Request::i()->phoneNumber ?? '', $showingMultipleHandlers, explode( ',', Settings::i()->verify_setup ), $_SESSION['verifyConfigureError'] ?? NULL );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit configuration screen. Return TRUE if was accepted
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function configurationScreenSubmit( \IPS\Member $member )
|
||||
public function configurationScreenSubmit( Member $member ): bool
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
|
||||
/* If we've enterred a code, verify it */
|
||||
if ( isset( $mfaDetails['verify'] ) and isset( $mfaDetails['verify']['pendingId'] ) and isset( \IPS\Request::i()->verify_auth_code ) )
|
||||
if ( isset( $mfaDetails['verify'] ) and isset( $mfaDetails['verify']['pendingId'] ) and isset( Request::i()->verify_auth_code ) )
|
||||
{
|
||||
$_SESSION['verifyAuthError'] = NULL;
|
||||
try
|
||||
{
|
||||
/* Verify with the API */
|
||||
$response = static::_api( "Services/" . \IPS\Settings::i()->verify_service_sid . "/VerificationCheck", 'post', array(
|
||||
$response = static::_api( "Services/" . Settings::i()->verify_service_sid . "/VerificationCheck", 'post', array(
|
||||
'VerificationSid' => $mfaDetails['verify']['pendingId'],
|
||||
'Code' => \IPS\Request::i()->verify_auth_code
|
||||
'Code' => Request::i()->verify_auth_code
|
||||
) );
|
||||
|
||||
if( $response['status'] == 'approved' )
|
||||
@@ -175,47 +196,47 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \IPS\MFA\Verify\Exception( 'verify_mfa_invalid_code' );
|
||||
throw new VerifyException( 'verify_mfa_invalid_code' );
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'verify' );
|
||||
Log::log( $e, 'verify' );
|
||||
$_SESSION['verifyAuthError'] = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Otherwise we need to generate an ID */
|
||||
elseif ( \IPS\Request::i()->phoneNumber )
|
||||
elseif ( Request::i()->phoneNumber )
|
||||
{
|
||||
/* Do we need to wait a while? */
|
||||
if ( isset( $mfaDetails['verify'] ) and isset( $mfaDetails['verify']['changeAttempts'] ) and $mfaDetails['verify']['changeAttempts'] >= \IPS\Settings::i()->verify_setup_tries )
|
||||
if ( isset( $mfaDetails['verify'] ) and isset( $mfaDetails['verify']['changeAttempts'] ) and $mfaDetails['verify']['changeAttempts'] >= Settings::i()->verify_setup_tries )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Reformat the phone number to E.164 */
|
||||
$countryCode = \substr( \IPS\Request::i()->countryCode, \strpos( \IPS\Request::i()->countryCode, "-" ) + 1 );
|
||||
$phoneNumber = "+" . $countryCode . \IPS\Request::i()->phoneNumber;
|
||||
$countryCode = substr( Request::i()->countryCode, strpos( Request::i()->countryCode, "-" ) + 1 );
|
||||
$phoneNumber = "+" . $countryCode . Request::i()->phoneNumber;
|
||||
|
||||
/* Call verify */
|
||||
$availableMethods = explode( ',', \IPS\Settings::i()->verify_setup );
|
||||
$method = ( isset( \IPS\Request::i()->method ) and \in_array( \IPS\Request::i()->method, $availableMethods ) ) ? \IPS\Request::i()->method : array_shift( $availableMethods );
|
||||
$availableMethods = explode( ',', Settings::i()->verify_setup );
|
||||
$method = ( isset( Request::i()->method ) and in_array( Request::i()->method, $availableMethods ) ) ? Request::i()->method : array_shift( $availableMethods );
|
||||
$_SESSION['verifyConfigureError'] = NULL;
|
||||
|
||||
$channel = ( \IPS\Request::i()->method == 'phone' ) ? 'call' : \IPS\Request::i()->method;
|
||||
$channel = ( Request::i()->method == 'phone' ) ? 'call' : Request::i()->method;
|
||||
|
||||
try
|
||||
{
|
||||
$response = static::_api( "Services/" . \IPS\Settings::i()->verify_service_sid . "/Verifications", 'post', array(
|
||||
$response = static::_api( "Services/" . Settings::i()->verify_service_sid . "/Verifications", 'post', array(
|
||||
'To' => $phoneNumber,
|
||||
'Channel' => $channel
|
||||
) );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'verify' );
|
||||
Log::log( $e, 'verify' );
|
||||
$_SESSION['verifyConfigureError'] = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
@@ -248,17 +269,17 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
/**
|
||||
* Get the form for a member to authenticate
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param \IPS\Http\Url $url URL for page
|
||||
* @param Member $member The member
|
||||
* @param Url $url URL for page
|
||||
* @return string
|
||||
*/
|
||||
public function authenticationScreen( \IPS\Member $member, \IPS\Http\Url $url )
|
||||
public function authenticationScreen( Member $member, Url $url ): string
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
$availableMethods = explode( ',', \IPS\Settings::i()->verify_method );
|
||||
$availableMethods = explode( ',', Settings::i()->verify_method );
|
||||
|
||||
/* If we sent a code, but it was less than one minute ago, log a failure and reset */
|
||||
if ( isset( $mfaDetails['verify']['sent'] ) and $mfaDetails['verify']['sent']['time'] < ( time() - 60 ) and !isset( \IPS\Request::i()->verify_auth_code ) )
|
||||
if ( isset( $mfaDetails['verify']['sent'] ) and $mfaDetails['verify']['sent']['time'] < ( time() - 60 ) and !isset( Request::i()->verify_auth_code ) )
|
||||
{
|
||||
unset( $mfaDetails['verify']['sent'] );
|
||||
$member->mfa_details = $mfaDetails;
|
||||
@@ -267,14 +288,14 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
}
|
||||
|
||||
/* If text message is the only available option, or we have chosen that option, do that... */
|
||||
$selectedMethod = \IPS\Request::i()->verify_method ?? NULL;
|
||||
$selectedMethod = Request::i()->verify_method ?? NULL;
|
||||
if( $selectedMethod === NULL )
|
||||
{
|
||||
if( isset( \IPS\Request::i()->verify_auth_code ) and isset( $mfaDetails['verify']['sent'] ) )
|
||||
if( isset( Request::i()->verify_auth_code ) and isset( $mfaDetails['verify']['sent'] ) )
|
||||
{
|
||||
$selectedMethod = $mfaDetails['verify']['sent']['method'];
|
||||
}
|
||||
elseif( \count( $availableMethods ) == 1 )
|
||||
elseif( count( $availableMethods ) == 1 )
|
||||
{
|
||||
$selectedMethod = $availableMethods[0];
|
||||
}
|
||||
@@ -286,7 +307,7 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
try
|
||||
{
|
||||
$channel = $selectedMethod == 'phone' ? 'call' : $selectedMethod;
|
||||
$response = static::_api( "Services/" . \IPS\Settings::i()->verify_service_sid . "/Verifications", 'post', array(
|
||||
$response = static::_api( "Services/" . Settings::i()->verify_service_sid . "/Verifications", 'post', array(
|
||||
'To' => $mfaDetails['verify']['id'],
|
||||
'Channel' => $channel
|
||||
) );
|
||||
@@ -298,22 +319,22 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'verify' );
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->verifyError( $e->getMessage() );
|
||||
Log::log( $e, 'verify' );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->verifyError( $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
/* Show screen */
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->verifyAuthenticate( $selectedMethod ?: 'choose', ( isset( $mfaDetails['verify']['sent'] ) and $mfaDetails['verify']['sent']['method'] == $selectedMethod ), $_SESSION['verifyAuthError'] ?? 'verify_error', $mfaDetails['verify']['setup'] ?? FALSE, $availableMethods, $url );
|
||||
return Theme::i()->getTemplate( 'login', 'core', 'global' )->verifyAuthenticate( $selectedMethod ?: 'choose', ( isset( $mfaDetails['verify']['sent'] ) and $mfaDetails['verify']['sent']['method'] == $selectedMethod ), $_SESSION['verifyAuthError'] ?? 'verify_error', $mfaDetails['verify']['setup'] ?? FALSE, $availableMethods, $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit authentication screen. Return TRUE if was accepted
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return string
|
||||
* @param Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function authenticationScreenSubmit( \IPS\Member $member )
|
||||
public function authenticationScreenSubmit( Member $member ): bool
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
|
||||
@@ -321,12 +342,12 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
|
||||
try
|
||||
{
|
||||
if ( isset( \IPS\Request::i()->verify_auth_code ) )
|
||||
if ( isset( Request::i()->verify_auth_code ) )
|
||||
{
|
||||
/* Verify with the API */
|
||||
$response = static::_api( "Services/" . \IPS\Settings::i()->verify_service_sid . "/VerificationCheck", 'post', array(
|
||||
$response = static::_api( "Services/" . Settings::i()->verify_service_sid . "/VerificationCheck", 'post', array(
|
||||
'To' => $mfaDetails['verify']['id'],
|
||||
'Code' => \IPS\Request::i()->verify_auth_code
|
||||
'Code' => Request::i()->verify_auth_code
|
||||
) );
|
||||
|
||||
if( $response['status'] == 'approved' )
|
||||
@@ -338,7 +359,7 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \IPS\MFA\Verify\Exception( 'verify_mfa_invalid_code' );
|
||||
throw new VerifyException( 'verify_mfa_invalid_code' );
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -348,7 +369,7 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'verify' );
|
||||
Log::log( $e, 'verify' );
|
||||
$_SESSION['verifyAuthError'] = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
@@ -358,16 +379,16 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
* Toggle
|
||||
*
|
||||
* @param bool $enabled On/Off
|
||||
* @return bool
|
||||
* @return void
|
||||
*/
|
||||
public function toggle( $enabled )
|
||||
public function toggle( bool $enabled ) : void
|
||||
{
|
||||
if ( $enabled )
|
||||
{
|
||||
static::verifyApiKeys( \IPS\Settings::i()->verify_sid, \IPS\Settings::i()->verify_token );
|
||||
static::verifyApiKeys( Settings::i()->verify_sid, Settings::i()->verify_token );
|
||||
}
|
||||
|
||||
\IPS\Settings::i()->changeValues( array( 'verify_enabled' => $enabled ) );
|
||||
Settings::i()->changeValues( array( 'verify_enabled' => $enabled ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -375,34 +396,34 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function acpSettings()
|
||||
public function acpSettings(): string
|
||||
{
|
||||
$form = new \IPS\Helpers\Form;
|
||||
$form = new Form;
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'verify_sid', \IPS\Settings::i()->verify_sid, TRUE, array(), NULL, NULL, \IPS\Member::loggedIn()->language()->addToStack( 'verify_sid_suffix' ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'verify_token', \IPS\Settings::i()->verify_token, TRUE ) );
|
||||
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'verify_groups', \IPS\Settings::i()->verify_groups == '*' ? '*' : explode( ',', \IPS\Settings::i()->verify_groups ), FALSE, array(
|
||||
$form->add( new Text( 'verify_sid', Settings::i()->verify_sid, TRUE, array(), NULL, NULL, Member::loggedIn()->language()->addToStack( 'verify_sid_suffix' ) ) );
|
||||
$form->add( new Text( 'verify_token', Settings::i()->verify_token, TRUE ) );
|
||||
$form->add( new CheckboxSet( 'verify_groups', Settings::i()->verify_groups == '*' ? '*' : explode( ',', Settings::i()->verify_groups ), FALSE, array(
|
||||
'multiple' => TRUE,
|
||||
'options' => array_combine( array_keys( \IPS\Member\Group::groups() ), array_map( function( $_group ) { return (string) $_group; }, \IPS\Member\Group::groups() ) ),
|
||||
'options' => array_combine( array_keys( Group::groups() ), array_map( function( $_group ) { return (string) $_group; }, Group::groups() ) ),
|
||||
'unlimited' => '*',
|
||||
'unlimitedLang' => 'everyone',
|
||||
'impliedUnlimited' => TRUE
|
||||
) ) );
|
||||
|
||||
$form->addHeader('verify_setup_header');
|
||||
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'verify_setup', explode( ',', \IPS\Settings::i()->verify_setup ), TRUE, array( 'options' => array(
|
||||
$form->add( new CheckboxSet( 'verify_setup', explode( ',', Settings::i()->verify_setup ), TRUE, array( 'options' => array(
|
||||
'sms' => 'verify_method_sms',
|
||||
'phone' => 'verify_method_phone',
|
||||
'whatsapp' => 'verify_method_whatsapp'
|
||||
) ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Custom( 'verify_setup_protection', array( \IPS\Settings::i()->verify_setup_tries, \IPS\Settings::i()->verify_setup_lockout ), FALSE, array(
|
||||
$form->add( new Custom( 'verify_setup_protection', array( Settings::i()->verify_setup_tries, Settings::i()->verify_setup_lockout ), FALSE, array(
|
||||
'getHtml' => function( $field ) {
|
||||
return \IPS\Theme::i()->getTemplate('settings')->verifySetupProtection( $field->value );
|
||||
return Theme::i()->getTemplate('settings')->verifySetupProtection( $field->value );
|
||||
}
|
||||
) ) );
|
||||
|
||||
$form->addHeader('verify_authenticate_header');
|
||||
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'verify_method', explode( ',', \IPS\Settings::i()->verify_method ), TRUE, array(
|
||||
$form->add( new CheckboxSet( 'verify_method', explode( ',', Settings::i()->verify_method ), TRUE, array(
|
||||
'options' => array(
|
||||
'sms' => 'verify_method_sms',
|
||||
'phone' => 'verify_method_phone',
|
||||
@@ -417,11 +438,11 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
$data = static::verifyApiKeys( $values['verify_sid'], $values['verify_token'] );
|
||||
|
||||
/* If we do not have any services yet, create one */
|
||||
if( !\count( $data['services'] ) )
|
||||
if( !count( $data['services'] ) )
|
||||
{
|
||||
/* Temporarily set the tokens so that we can properly make the next call */
|
||||
\IPS\Settings::i()->verify_sid = $values['verify_sid'];
|
||||
\IPS\Settings::i()->verify_token = $values['verify_token'];
|
||||
Settings::i()->verify_sid = $values['verify_sid'];
|
||||
Settings::i()->verify_token = $values['verify_token'];
|
||||
$service = static::_api( "Services", 'post', array(
|
||||
'FriendlyName' => 'IPS MFA'
|
||||
) );
|
||||
@@ -441,10 +462,10 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
$values['verify_method'] = isset( $values['verify_method'] ) ? implode( ',', $values['verify_method'] ) : '';
|
||||
$form->saveAsSettings( $values );
|
||||
|
||||
\IPS\Session::i()->log( 'acplogs__mfa_handler_enabled', array( "mfa_verify_title" => TRUE ) );
|
||||
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=settings&controller=mfa' ), 'saved' );
|
||||
Session::i()->log( 'acplogs__mfa_handler_enabled', array( "mfa_verify_title" => TRUE ) );
|
||||
Output::i()->redirect( Url::internal( 'app=core&module=settings&controller=mfa' ), 'saved' );
|
||||
}
|
||||
catch( \DomainException $e )
|
||||
catch( DomainException $e )
|
||||
{
|
||||
$form->error = $e->getMessage();
|
||||
}
|
||||
@@ -459,13 +480,13 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
* @param string $sid The Verify SID
|
||||
* @param string $token The Verify Auth Token
|
||||
* @return array
|
||||
* @throws \DomainException
|
||||
* @throws DomainException
|
||||
*/
|
||||
public static function verifyApiKeys( string $sid, string $token ) : array
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external("https://verify.twilio.com/v2/Services" )
|
||||
$response = Url::external("https://verify.twilio.com/v2/Services" )
|
||||
->request()
|
||||
->login( $sid, $token )
|
||||
->get()
|
||||
@@ -473,24 +494,24 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
|
||||
if( !isset( $response['services'] ) )
|
||||
{
|
||||
throw new \DomainException( $response['message'] );
|
||||
throw new DomainException( $response['message'] );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
throw new \DomainException( $e->getMessage() );
|
||||
throw new DomainException( $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If member has configured this handler, disable it
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
* @param Member $member The member
|
||||
* @return void
|
||||
*/
|
||||
public function disableHandlerForMember( \IPS\Member $member )
|
||||
public function disableHandlerForMember( Member $member ): void
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
|
||||
@@ -512,23 +533,23 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
* @param string $method 'get' or 'post'
|
||||
* @param array|null $data Post data or additional query string parameters
|
||||
* @return array
|
||||
* @throws \IPS\MFA\Verify\Exception
|
||||
* @throws VerifyException
|
||||
*/
|
||||
protected static function _api( string $endpoint, string $method='get', ?array $data=NULL ) : array
|
||||
{
|
||||
$url = \IPS\Http\Url::external("https://verify.twilio.com/v2/{$endpoint}" );
|
||||
$url = Url::external("https://verify.twilio.com/v2/{$endpoint}" );
|
||||
|
||||
if ( $method == 'get' )
|
||||
{
|
||||
$response = $url->setQueryString( $data )
|
||||
->request()
|
||||
->login( \IPS\Settings::i()->verify_sid, \IPS\Settings::i()->verify_token )
|
||||
->login( Settings::i()->verify_sid, Settings::i()->verify_token )
|
||||
->get();
|
||||
}
|
||||
else
|
||||
{
|
||||
$response = $url->request()
|
||||
->login( \IPS\Settings::i()->verify_sid, \IPS\Settings::i()->verify_token )
|
||||
->login( Settings::i()->verify_sid, Settings::i()->verify_token )
|
||||
->post( $data );
|
||||
|
||||
}
|
||||
@@ -537,7 +558,7 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
|
||||
if( isset( $response['code'] ) AND isset( $response['message'] ) )
|
||||
{
|
||||
throw new \IPS\MFA\Verify\Exception( $response['message'], $response['code'] );
|
||||
throw new VerifyException( $response['message'], $response['code'] );
|
||||
}
|
||||
|
||||
return $response;
|
||||
|
||||
Reference in new issue
Block a user