<?php

/**
 * Lifestyle Webconsulting GmbH
 *
 * LICENSE: This Software is the property of Lifestyle Webconsulting GmbH (Aschaffenburg, Germany)
 * and is private by copyright law - it is NOT Freeware.
 *
 * Any unauthorized use of this software without a valid license
 * is a violation of the license agreement and will be prosecuted by
 * civil and criminal law.
 *
 * @copyright  2019 Lifestyle Webconsulting GmbH
 * @link       https://www.life-style.de
 */

namespace SimpleSAML\Module\lifestyle\Auth;

use Exception;
use SimpleSAML\Auth\Source;
use SimpleSAML\Auth\State;
use SimpleSAML\Error\BadRequest;
use SimpleSAML\Error\Error;
use SimpleSAML\Error\NoState;
use SimpleSAML\Logger;
use SimpleSAML\Module;
use SimpleSAML\Module\core\Auth\UserPassBase;
use SimpleSAML\Module\lifestyle\Utils\Request;
use SimpleSAML\Module\lifestyle\Webservice\Handler;
use SimpleSAML\Utils\HTTP;

/**
 * Class Mfa
 *
 * @copyright  2019 Lifestyle Webconsulting GmbH
 * @link       https://www.life-style.de
 * @package SimpleSAML\Module\lifestyle\Auth
 */
class Mfa
{
    /**
     * @var string
     */
    private $authStateId;

    /**
     * Mfa constructor.
     * @param string $authStateId
     */
    private function __construct($authStateId)
    {
        $this->authStateId = $authStateId;
    }

    /**
     * @return Mfa
     * @throws BadRequest
     */
    public static function createFromRequest()
    {
        if (!array_key_exists('AuthState', $_REQUEST)) {
            throw new BadRequest('Missing AuthState parameter.');
        }
        return new self($_REQUEST['AuthState']);
    }

    /**
     * @param array $attributes
     * @param string $mfaQrCode
     * @param bool $forcePasswordReset
     * @param string $userGuid
     * @param string $resetPasswordToken
     * @throws NoState
     */
    public function redirect(array $attributes, $mfaQrCode, $forcePasswordReset, $userGuid, $resetPasswordToken)
    {
        Logger::info('MFA enabled for current user. Starting MFA dialogue.');

        // Re-Load state from current request
        $state = State::loadState($this->authStateId, UserPassBase::STAGEID);

        // Add user attributes to state
        $state['Attributes'] = $attributes;
        $state['MfaQrCode'] = $mfaQrCode;

        // Add password-reset data for redirect after successful mfa dialogue
        if ($forcePasswordReset) {
            $state['passwordReset'] = [
                'userGuid' => $userGuid,
                'resetPasswordToken' => $resetPasswordToken,
            ];
        } else {
            unset($state['passwordReset']);
        }

        // Save the $state-array, so that we can restore it after a redirect.
        $id = State::saveState($state, UserPassBase::STAGEID);

        // Redirect to the mfa form.
        $url = Module::getModuleURL('lifestyle/loginmfa.php');
        $params = array('AuthState' => $id);
        HTTP::redirectTrustedURL($url, $params);

        /* The previous function never returns, so this code is never executed. */
        assert('FALSE');
    }

    /**
     * Handle MFA login request.
     *
     * @param $mfaToken
     * @throws Exception
     * @throws \SimpleSAML\Error\Exception
     * @throws NoState
     */
    public function handleMfaLogin($mfaToken)
    {
        assert('is_string($mfaToken)');

        // Here we retrieve the state array we saved in the authenticate-function.
        $state = State::loadState($this->authStateId, UserPassBase::STAGEID);

        // This should never happen - username must be set
        if (!is_array($state['Attributes']['username'])) {
            $this->logFailure('Username not in attributes.');
            throw new Error('WRONGMFATOKEN');
        }

        // This should never happen - username cannot be empty
        $username = reset($state['Attributes']['username']);
        if (!strlen($username)) {
            $this->logFailure('Username empty.');
            throw new Error('WRONGMFATOKEN');
        }

        // Validate token.
        $this->validateToken($username, $mfaToken);

        $this->logSuccess('User ' . $username . ' has been successfully authenticated.');

        $this->resume($username, $state);
        assert(false);
    }

    /**
     * @param string $username
     * @param string $mfaToken
     * @throws Error
     */
    private function validateToken($username, $mfaToken)
    {
        assert('is_string($username)');
        assert('is_string($mfaToken)');

        if (!Handler::validateMfaToken($username, $mfaToken)) {
            $this->logFailure('Invalid mfa token for user ' . $username);
            Handler::setLoginStatus($username, false);

            throw new Error('WRONGMFATOKEN');
        }
    }

    /**
     * @param string $username
     * @param array $state
     * @throws BadRequest
     * @throws NoState
     */
    private function resume($username, $state)
    {
        if (isset($state['passwordReset'])) {
            PasswordReset::createFromRequest()->redirect(
                $username,
                $state['passwordReset']['userGuid'],
                $state['passwordReset']['resetPasswordToken']
            );
        }

        Handler::setLoginStatus($username, true);

        // We need the real username - not the email-address - otherwise we won't get the applications
        if (isset($state['Attributes']['username']) && is_array(isset($state['Attributes']['username']))) {
            $username = reset($state['Attributes']['username']);
        }

        $state['Attributes'] = array_merge(
            $state['Attributes'],
            UserApplication::createFromRequest()->getApplications($username)
        );

        /* Return control to SimpleSAMLphp after successful authentication. */
        Source::completeAuth($state);
    }

    /**
     * @param string $message
     */
    private function logFailure($message)
    {
        Logger::stats('Unsuccessful mfa-login attempt from ' . Request::getClientIp() . ': ' . $message);
    }

    /**
     * @param string $message
     */
    private function logSuccess($message)
    {
        Logger::stats('Successful mfa-login: ' . $message);
    }
}
