<?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\Source;

use Exception;
use SimpleSAML\Auth\Source;
use SimpleSAML\Error\AuthSource;
use SimpleSAML\Error\Error;
use SimpleSAML\Logger;
use SimpleSAML\Module\core\Auth\UserPassBase;
use SimpleSAML\Module\lifestyle\Auth\Mfa;
use SimpleSAML\Module\lifestyle\Auth\PasswordReset;
use SimpleSAML\Module\lifestyle\Auth\UserApplication;
use SimpleSAML\Module\lifestyle\Utils\Request;
use SimpleSAML\Module\lifestyle\Utils\Security;
use SimpleSAML\Module\lifestyle\Utils\StringUtils;
use SimpleSAML\Module\lifestyle\Webservice\Handler;
use SimpleSAML\Module\lifestyle\Webservice\Model\Response\IdP\PostLogin\Response;

/**
 * Class WebserviceSelect
 *
 * Simple SQL authentication source
 *
 * - Get authentication-source from foreign database
 * - Authenticate with selected source
 *
 * @copyright  2019 Lifestyle Webconsulting GmbH
 * @link       https://www.life-style.de
 * @package SimpleSAML\Module\lifestyle\Auth\Source
 */
class WebserviceSelect extends UserPassBase
{
    /**
     * List of available auth sources
     *
     * @var array
     */
    private $_authSources;

    /**
     * WebserviceSelect constructor.
     * @param array $info
     * @param array $config
     * @throws Exception
     */
    public function __construct($info, &$config)
    {
        assert('is_array($info)');
        assert('is_array($config)');

        parent::__construct($info, $config);

        if (empty($config['authsources']) || !is_array($config['authsources'])) {
            throw new Exception('Invalid attributes for authentication source ' . $this->authId . ': Missing authsources-array!');
        }
        $this->_authSources = $config['authsources'];
    }

    /**
     * Login user
     *
     * @param string $username
     * @param string $password
     * @return array
     * @throws Exception
     * @throws Error
     */
    protected function login($username, $password)
    {
        assert('is_string($username)');
        assert('is_string($password)');

        $this->validateCaptcha($username);
        $user = $this->getUser($username, $password);
        $authId = $this->getUserAuthId($user);
        $authSource = $this->getUserAuthSource($authId);
        $attributes = $this->loginUser($username, $password, $user, $authId, $authSource);

        if ($user->isMfaEnabled()) {
            Mfa::createFromRequest()->redirect(
                $attributes,
                $user->getMfaQrCode(),
                $user->isForcePasswordReset(),
                $user->getUserGuid(),
                $user->getResetPasswordToken()
            );
        }

        if ($user->isForcePasswordReset()) {
            PasswordReset::createFromRequest()->redirect(
                $username,
                $user->getUserGuid(),
                $user->getResetPasswordToken()
            );
        }

        Handler::setLoginStatus($user->getUsername(), true);

        return array_merge(
            $attributes,
            UserApplication::createFromRequest()->getApplications($user->getUsername())
        );
    }

    /**
     * @param string $username
     * @throws Error
     */
    private function validateCaptcha($username)
    {
        if (true !== Security::validate($username, $_POST)) {
            $this->logFailure($username, 'Captcha validation failed.');
            Handler::setLoginStatus($username, false);
            throw new Error('WRONGCAPTCHA');
        }
    }

    /**
     * @param string $username
     * @param string $password
     * @return Response
     * @throws Error
     */
    private function getUser($username, $password)
    {
        $user = Handler::getUser($username, $password);

        // User not found
        if (null === $user) {
            $this->logFailure($username, 'User not found.');
            throw new Error('WRONGUSERPASS');
        }

        return $user;
    }

    /**
     * @param Response $user
     * @return string
     * @throws Exception
     */
    private function getUserAuthId(Response $user)
    {
        $authId = $user->getUserAuthId();

        if (empty($authId)) {
            $authId = reset($this->_authSources);
            Logger::debug(sprintf(
                'No authentication source set for user "%s". Trying first from list: "%s"',
                $user->getUsername(),
                $authId
            ));
        }

        if (empty($authId)) {
            throw new Exception(sprintf(
                'Could not find authentication source with id "%s" in configured authentication sources for id "%s"',
                $authId,
                $this->authId
            ));
        }

        // Prevent from infinite loops
        if ($authId == $this->authId) {
            throw new Exception(sprintf(
                'Cannot authenticate against "%s"! Check the value in auth_id-column of your database.',
                $this->authId
            ));
        }

        return $authId;
    }

    /**
     * @param string $authId
     * @return UserPassBase
     * @throws \SimpleSAML\Error\Exception
     */
    private function getUserAuthSource($authId)
    {
        $source = Source::getById($authId);

        if (!($source instanceof UserPassBase)) {
            throw new Exception(sprintf(
                'Could not find authentication source with id "%s".',
                $authId
            ));
        }

        return $source;
    }

    /**
     * @param string $username
     * @param string $password
     * @param Response $user
     * @param string $authId
     * @param Source $authSource
     * @return array
     * @throws Error
     */
    private function loginUser(
        $username,
        $password,
        Response $user,
        $authId,
        Source $authSource
    ) {
        Logger::debug(sprintf(
            'Selected auth-source: %s, using username: %s, Ldap-search-attributes: %s',
            $authId,
            $user->getUsername(),
            $user->getUserLdapSearchAttributes()
        ));

        $isLdapUser = false !== stripos($user->getUserAuthId(), 'ldap');
        $loginUsername = $isLdapUser && strlen($user->getUserLdapSearchValue()) ? $user->getUserLdapSearchValue() : $username;
        $ldapSearchAttributes = $isLdapUser && strlen($user->getUserLdapSearchAttributes()) ? StringUtils::trimsplit(',', $user->getUserLdapSearchAttributes()) : null;

        $attributes = [];
        $authenticated = false;
        try {
            $attributes = $authSource->login($loginUsername, $password, null, $ldapSearchAttributes);
            $authenticated = true;
        } catch (Error $e) {
            Logger::debug('Failed source ' . $authId . ' with error: ' . $e->getMessage());
        } catch (AuthSource $e) {
            Logger::debug('Failed source ' . $authId . ' with error: ' . $e->getMessage());
        }

        if (!$authenticated) {
            $this->logFailure($username, 'Login failure for user ' . $username);
            Handler::setLoginStatus($username, false);
            throw new Error('WRONGUSERPASS');
        }

        return array_merge(
            $attributes,
            $user->getUserAttributes(),
            [
                'authsource' => [$authId],
                'useridentifier' => [hash('sha512', $username . mt_rand())],
            ]
        );
    }

    /**
     * @param string $username
     * @param string $message
     */
    private function logFailure($username, $message)
    {
        Logger::stats(sprintf(
            'Login failure for user %s from ip %s: %s',
            $username,
            Request::getClientIp(),
            $message
        ));
    }
}
