<?php

$configurationFile = '../lucidAuth.config.php';
if (!file_exists($configurationFile)) {
	throw new Exception(sprintf('Missing config file. Please rename \'%1$s.example\' to \'%1$s\' and edit it to reflect your setup.', explode('../', $configurationFile)[1]));
}
$settings = include_once($configurationFile);
try {
#	switch ($settings->Database['Driver']) {
#		case 'sqlite':
#			$database = new PDO('sqlite:' . $settings->Database['Path']);
	if (is_writable($settings->Sqlite['Path'])) {
		$pdoDB = new PDO('sqlite:' . $settings->Sqlite['Path']);
	} else {
		throw new Exception(sprintf('Database file \'%1$s\' is not writable', $settings->Sqlite['Path']));
	}
#	}
}
catch (Exception $e) {
	throw new Exception(sprintf('Unable to connect to database \'%1$s\'', $settings->Sqlite['Path']));
}

function authenticateLDAP (string $username, string $password) {
	global $settings;

	if (!empty($username) && !empty($password)) {
		// Handle login requests

		$ds = ldap_connect($settings->LDAP['Server'], $settings->LDAP['Port']);

		// Strict namingconvention: only allow alphabetic characters
		$sanitizedUsername = preg_replace('([^a-zA-Z]*)', '', $_POST['username']);
		$qualifiedUsername = $settings->LDAP['Domain'] . '\\' . $sanitizedUsername;

		if (@ldap_bind($ds, $qualifiedUsername, utf8_encode($_POST['password']))) {
			// Successful authentication; get additional userdetails from authenticationsource
			$ldapSearchResults = ldap_search($ds, $settings->LDAP['BaseDN'], "sAMAccountName=$sanitizedUsername");
			$commonName = ldap_get_entries($ds, $ldapSearchResults)[0]['cn'][0];
			// Create JWT-payload
			$jwtPayload = [
				'iat'	=> time(),					// Issued at: time when the token was generated
				'iss'	=> $_SERVER['SERVER_NAME'],	// Issuer
				'sub'	=> $qualifiedUsername,		// Subject (ie. username)
				'name'	=> $commonName				// Common name (as retrieved from AD)
			];

			$secureToken = JWT::encode($jwtPayload, base64_decode($settings->JWT['PrivateKey_base64']));

			return ['status' => 'Success', 'token' => $secureToken];
		} else {
			// LDAP authentication failed!
			return ['status' => 'Fail', 'reason' => '1'];
		}
	} else {
		// Empty username or passwords not allowed!
		return ['status' => 'Fail', 'reason' => '1'];
	}
}

function storeToken (string $secureToken, string $qualifiedUsername, string $httpHost) {
	global $settings, $pdoDB;

	// Save authentication token in database serverside
	try {
		$pdoQuery = $pdoDB->prepare('
			INSERT INTO SecureToken (UserId, Value)
			SELECT User.Id, :securetoken
			FROM User
			WHERE LOWER(User.Username) = :qualifiedusername
		');
		$pdoQuery->execute([
			':securetoken'			=>	$secureToken,
			':qualifiedusername'	=>	strtolower($qualifiedUsername)
		]);
	}
	catch (Exception $e) {
		return ['status' => 'Fail', 'reason' => $e];
	}

	// Save authentication token in cookie clientside
	$cookieDomain = array_values(array_filter($settings->Session['CookieDomains'], function ($value) use ($httpHost) {
		// Check if $_SERVER['HTTP_HOST'] matches any of the configured domains (either explicitly or as a subdomain)
		//   This might seem backwards, but relying on $_SERVER directly allows spoofed values with potential security risks
		return (strlen($value) > strlen($httpHost)) ? false : (0 === substr_compare($httpHost, $value, -strlen($value)));
	}))[0];
	if ($cookieDomain && setcookie('JWT', $secureToken, (time() + $settings->Session['Duration']), '/', '.' . $cookieDomain)) {
		return ['status' => 'Success'];
	} else {
		return ['status' => 'Fail', 'reason' => 'Unable to store cookie(s)'];
	}
}

function validateToken (string $secureToken) {
	global $settings, $pdoDB;

	// Decode provided authentication token
	try {
		$jwtPayload = JWT::decode($secureToken, base64_decode($settings->JWT['PrivateKey_base64']), $settings->JWT['Algorithm']);
	} catch (Exception $e) {
		// Invalid token
		if ($settings->Debug['LogToFile']) {
			file_put_contents('../validateToken.log', (new DateTime())->format('Y-m-d\TH:i:s.u') . ' --- Provided token could not be decoded' . PHP_EOL, FILE_APPEND);
		}
		return ['status' => 'Fail', 'reason' => '1'];
	}

	if ((int)$jwtPayload->iat < (time() - (int)$settings->Session['Duration'])) {
		// Expired token
		if ($settings->Debug['LogToFile']) {
			file_put_contents('../validateToken.log', (new DateTime())->format('Y-m-d\TH:i:s.u') . ' --- Provided token has expired' . PHP_EOL, FILE_APPEND);
		}
		return ['status' => 'Fail', 'reason' => '3'];
	}

	// Retrieve all authentication tokens from database matching username
	$pdoQuery = $pdoDB->prepare('
		SELECT User.Id, SecureToken.Value
		FROM SecureToken
		LEFT JOIN User
			ON (User.Id=SecureToken.UserId)
		WHERE LOWER(User.Username) = :username
	');
	$pdoQuery->execute([
			':username'	=>	(string) strtolower($jwtPayload->sub)
		]);
	foreach($pdoQuery->fetchAll(PDO::FETCH_ASSOC) as $row) {
		try {
			$storedTokens[] = JWT::decode($row['Value'], base64_decode($settings->JWT['PrivateKey_base64']), $settings->JWT['Algorithm']);
            $currentUserId = $row['Id'];
		} catch (Exception $e) {
			continue;
		}
	}

	// Compare provided authentication token to all stored tokens in database
	if (!empty($storedTokens) && sizeof(array_filter($storedTokens, function ($value) use ($jwtPayload) {
		return $value->iat === $jwtPayload->iat;
	})) === 1) {
		return [
			'status'	=> 'Success',
			'name'		=> $jwtPayload->name,
            'uid'       => $currentUserId
		];
	} else {
		if ($settings->Debug['LogToFile']) {
			file_put_contents('../validateToken.log', (new DateTime())->format('Y-m-d\TH:i:s.u') . ' --- Either no matching token or multiple matching tokens found in database' . PHP_EOL, FILE_APPEND);
		}
		return ['status' => 'Fail', 'reason' => '2'];
	}
}

?>