<?php
###############################################################################
# DuoUniversal.php
#
# Compatible with duo_universal_php 1.1.2 (NO NONCE)
#
###############################################################################

namespace CodePunch\Config\Security;

use CodePunch\Base\Util as UTIL;
use Duo\DuoUniversal\Client;
use Duo\DuoUniversal\DuoException;
use Exception;

###############################################################################
# Manual includes (no Composer)
###############################################################################

include_once(UTIL::get_install_folder_path() . "thirdparty/2fa/duosecurity/duo_universal_php/src/Client.php");
include_once(UTIL::get_install_folder_path() . "thirdparty/2fa/duosecurity/duo_universal_php/src/DuoException.php");

###############################################################################

class DuoUniversal extends Base {

	public function getKeys() {
		return array(
			'Integration Key' => '(Client ID) Obtained from Duo',
			'Secret Key'      => '(Client Secret) Obtained from Duo',
			'Host Name'       => '(API Host Name) Obtained from Duo',
			'HTTP_Proxy'      => 'If you have an HTTP proxy server eg: localhost:8081. Leave empty otherwise.'
		);
	}

	public function get() {

		$auth = $this->getAuthentication();
		$username = UTIL::get_from_array($_SESSION[\CodePunch\Config\Auth::USERNAME], "");

		if ($username == "" || !$auth || !$auth->getDatabase()) {
			UTIL::print("Invalid primary login. There is no user information to trigger 2FA!");
			exit();
		}

		$setup = new \CodePunch\Config\Settings($auth);

		$twofa_cookie_set_days = $setup->getString("twofa_cookie_set_days", 0);
		if ($twofa_cookie_set_days <= 0 || $twofa_cookie_set_days > 45) {
			$twofa_cookie_set_days = 0;
		}

		$duo_integration_key = $setup->getEncryptedOption("duouniversal_key_integration_key", "");
		$duo_secret_key      = $setup->getEncryptedOption("duouniversal_key_secret_key", "");
		$duo_hostname        = $setup->getEncryptedOption("duouniversal_key_host_name", "");
		$duo_http_proxy      = $setup->getEncryptedOption("duouniversal_key_http_proxy", "");
		$duo_http_proxy      = strlen($duo_http_proxy) < 2 ? "" : $duo_http_proxy;

		$duo_failmode = "CLOSED";

		$redirect_uri = str_replace("http://", "https://", UTIL::get_root_url()) . "login.php";

		try {
			$duo_client = new Client(
				$duo_integration_key,
				$duo_secret_key,
				$duo_hostname,
				$redirect_uri,
				true,
				$duo_http_proxy
			);
		}
		catch (DuoException $e) {
			$this->logAndExit($e);
		}
		catch (Exception $e) {
			$this->logAndExit($e);
		}

		// Check if this is callback from Duo
		if (isset($_REQUEST['state']) && isset($_REQUEST['duo_code'])) {
			$this->handleCallback($duo_client, $auth, $username, $twofa_cookie_set_days);
			exit();
		}

		// Otherwise initiate Duo flow
		$this->startPrompt($duo_client, $username, $duo_failmode);
		exit();
	}

	###############################################################################
	# Start Duo Authentication
	###############################################################################

	private function startPrompt(Client $duo_client, $username, $duo_failmode) {

		try {
			$duo_client->healthCheck();

			$state = $duo_client->generateState();
			$_SESSION['DUO_UNIVERSAL_STATE'] = $state;

			$prompt_uri = $duo_client->createAuthUrl($username, $state);

			header("Location: " . $prompt_uri);
			exit();
		}
		catch (DuoException $e) {

			$logger = new \CodePunch\Base\CPLogger();
			$logger->error($e->getMessage());

			if ($duo_failmode == "OPEN") {
				$logger->error("Login 'Successful', but 2FA Not Performed.");
			} else {
				$logger->error("2FA Unavailable. Confirm Duo configuration.");
			}

			UTIL::print(get_class($this) . ": " . $e->getMessage());
			exit();
		}
		catch (Exception $e) {
			$this->logAndExit($e);
		}
	}

	###############################################################################
	# Handle Duo Callback
	###############################################################################

	private function handleCallback(Client $duo_client, $auth, $username, $twofa_cookie_set_days) {

		$request_state    = UTIL::get_from_array($_REQUEST['state'], "");
		$request_duo_code = UTIL::get_from_array($_REQUEST['duo_code'], "");

		$session_state = UTIL::get_from_array($_SESSION['DUO_UNIVERSAL_STATE'], "");

		if ($request_state === "" || $request_duo_code === "") {
			UTIL::print("Duo callback missing required parameters.");
			exit();
		}

		if ($session_state === "") {
			UTIL::print("Duo session state missing. Please retry login.");
			exit();
		}

		// Constant-time comparison
		if (!hash_equals($session_state, $request_state)) {
			$this->clearSession();
			UTIL::print("Invalid Duo state.");
			exit();
		}

		try {
			$decoded_token = $duo_client->exchangeAuthorizationCodeFor2FAResult(
				$request_duo_code,
				$username
			);

			$this->clearSession();

			$auth->processSecondaryLogin($username, $twofa_cookie_set_days);
			exit();
		}
		catch (DuoException $e) {
			$this->clearSession();
			$this->logAndExit($e);
		}
		catch (Exception $e) {
			$this->clearSession();
			$this->logAndExit($e);
		}
	}

	###############################################################################
	# Helpers
	###############################################################################

	private function clearSession() {
		unset($_SESSION['DUO_UNIVERSAL_STATE']);
	}

	private function logAndExit($e) {
		$logger = new \CodePunch\Base\CPLogger();
		$logger->error($e->getMessage());
		UTIL::print(get_class($this) . ": " . $e->getMessage());
		exit();
	}
}