rugk/xenforo-threema-gateway

View on GitHub
src/library/ThreemaGateway/Tfa/Reversed.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php
/**
 * Two factor authentication provider for Threema Gateway which waites for a
 * secret/code transfered via Threema.
 *
 * @package ThreemaGateway
 * @author rugk
 * @copyright Copyright (c) 2015-2016 rugk
 * @license MIT
 */

/**
 * TFA where the user sends a login secret via Threema.
 */
class ThreemaGateway_Tfa_Reversed extends ThreemaGateway_Tfa_AbstractProvider
{
    /**
     * Return a description of the 2FA methode.
     */
    public function getDescription()
    {
        /** @var XenForo_Options $options */
        $options = XenForo_Application::getOptions();
        /** @var array $params */
        $params = [
            'board' => $options->boardTitle
        ];

        return new XenForo_Phrase('tfa_' . $this->_providerId . '_desc', $params);
    }

    /**
     * Called when verifying displaying the choose 2FA mode.
     *
     * @return bool
     */
    public function canEnable()
    {
        if (!parent::canEnable()) {
            return false;
        }

        // check whether it is activated in the settings
        /** @var XenForo_Options $options */
        $options = XenForo_Application::getOptions();
        if (!$options->threema_gateway_tfa_reversed) {
            return false;
        }

        // this 2FA mode requires end-to-end encryption
        if (!$this->gatewaySettings->isEndToEnd()) {
            return false;
        }

        // check specific permissions
        if (!$this->gatewayPermissions->hasPermission('receive') ||
            !$this->gatewayPermissions->hasPermission('fetch')
        ) {
            return false;
        }

        return true;
    }

    /**
     * Called when trying to verify user. Creates secret and registers callback
     * request.
     *
     * @param  string $context
     * @param  array  $user
     * @param  string $userIp
     * @param  array  $providerData
     * @return array
     */
    public function triggerVerification($context, array $user, $userIp, array &$providerData)
    {
        parent::triggerVerification($context, $user, $userIp, $providerData);

        if (!$providerData) {
            return [];
        }

        // this 2FA mode requires end-to-end encryption
        if (!$this->gatewaySettings->isEndToEnd()) {
            throw new XenForo_Exception(new XenForo_Phrase('threema_this_action_required_e2e'));
        }

        /** @var XenForo_Options $options */
        $options = XenForo_Application::getOptions();

        /** @var string $secret random 6 digit string */
        $secret = $this->generateRandomSecret();

        $providerData['secret']          = $secret;
        $providerData['secretGenerated'] = XenForo_Application::$time;

        //secret is only valid for some time
        if ($context == 'setup') {
            $providerData['validationTime'] = $options->threema_gateway_tfa_reversed_validation_setup * 60; //default: 10 minutes
        } else {
            $providerData['validationTime'] = $options->threema_gateway_tfa_reversed_validation * 60; //default: 3 minutes
        }

        // most importantly register message request for Threema callback
        $this->registerPendingConfirmationMessage(
            $providerData,
            ThreemaGateway_Model_TfaPendingMessagesConfirmation::PENDING_REQUEST_CODE,
            $user
        );

        return [];
    }

    /**
     * Called when trying to verify user. Shows code, so user can send it via
     * Threema.
     *
     * @param  XenForo_View $view
     * @param  string       $context
     * @param  array        $user
     * @param  array        $providerData
     * @param  array        $triggerData
     * @return string       HTML code
     */
    public function renderVerification(XenForo_View $view, $context, array $user,
                                        array $providerData, array $triggerData)
    {
        parent::renderVerification($view, $context, $user, $providerData, $triggerData);

        $triggerData['secret'] = $providerData['secret'];
        if ($providerData['useNumberSmilies']) {
            $triggerData['secretWithSmiley'] = ThreemaGateway_Helper_Emoji::parseUnicode(
                ThreemaGateway_Helper_Emoji::replaceDigits($triggerData['secret'])
            );
        }

        /** @var XenForo_Options $xenOptions */
        $xenOptions = XenForo_Application::getOptions();

        $params = [
            'data' => $providerData,
            'trigger' => $triggerData,
            'context' => $context,
            'validationTime' => $this->parseTime($providerData['validationTime']),
            'gatewayid' => $this->gatewaySettings->getId(),
            'autoTrigger' => $xenOptions->threema_gateway_tfa_reversed_auto_trigger
        ];
        return $view->createTemplateObject('two_step_threemagw_reversed', $params)->render();
    }

    /**
     * Called when trying to verify user. Checks whether the secret was received
     * from the Threema Gateway callback.
     *
     * @param string $context
     * @param array  $input
     * @param array  $user
     * @param array  $providerData
     *
     * @return bool
     */
    public function verifyFromInput($context, XenForo_Input $input, array $user, array &$providerData)
    {
        /** @var bool $result from parent, for error checking */
        $result = parent::verifyFromInput($context, $input, $user, $providerData);

        // let errors pass through
        if (!$result) {
            return $result;
        }

        // verify that secret has not expired yet
        if (!$this->verifySecretIsInTime($providerData)) {
            return false;
        }

        // check whether secret has been received at all
        if (!isset($providerData['receivedSecret'])) {
            return false;
        }

        // prevent replay attacks
        if (!$this->verifyNoReplayAttack($providerData, $providerData['receivedSecret'])) {
            return false;
        }

        // check whether the secret is the same as required
        if (!$this->stringCompare($providerData['secret'], $providerData['receivedSecret'])) {
            return false;
        }

        $this->updateReplayCheckData($providerData, $providerData['receivedSecret']);

        // unregister confirmation
        $this->unregisterPendingConfirmationMessage(
            $providerData,
            ThreemaGateway_Model_TfaPendingMessagesConfirmation::PENDING_REQUEST_CODE
        );

        $this->resetProviderOptionsForTrigger($context, $providerData);

        return true;
    }

    /**
     * Verifies the Treema ID formally after it was entered/changed.
     *
     * @param XenForo_Input $input
     * @param array         $user
     * @param array         $error
     *
     * @return array
     */
    public function verifySetupFromInput(XenForo_Input $input, array $user, &$error)
    {
        /** @var array $providerData */
        $providerData = parent::verifySetupFromInput($input, $user, $error);

        // let errors pass through
        if (!$providerData) {
            return $providerData;
        }

        //add other options to provider data
        $providerData['useNumberSmilies'] = $input->filterSingle('useNumberSmilies', XenForo_Input::BOOLEAN);

        return $providerData;
    }

    /**
     * Called before the setup verification is shown.
     *
     * @param array $providerData
     * @param array $triggerData
     *
     * @return bool
     */
    protected function initiateSetupData(array &$providerData, array &$triggerData)
    {
        return true;
    }

    /**
     * Generates the default provider options at setup time before it is
     * displayed to the user.
     *
     * @return array
     */
    protected function generateDefaultData()
    {
        return [
            'useNumberSmilies' => true,
        ];
    }

    /**
     * Adjust the view params for managing the 2FA mode, e.g. add special
     * params needed by your template.
     *
     * @param array  $viewParams
     * @param string $context
     * @param array  $user
     *
     * @return array
     */
    protected function adjustViewParams(array $viewParams, $context, array $user)
    {
        /** @var XenForo_Options $xenOptions */
        $xenOptions = XenForo_Application::getOptions();

        $viewParams += [
            'https' => XenForo_Application::$secure,
            'showqrcode' => $xenOptions->threema_gateway_tfa_reversed_show_qr_code,
            'gatewayid' => $this->gatewaySettings->getId()
        ];

        return $viewParams;
    }

    /**
     * Resets the provider options to make sure the current 2FA verification
     * does not affect the next one.
     *
     * @param string $context
     * @param array  $providerData
     */
    protected function resetProviderOptionsForTrigger($context, array &$providerData)
    {
        parent::resetProviderOptionsForTrigger($context, $providerData);

        unset($providerData['receivedSecret']);
    }
}