258 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			258 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?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];
 | |
| 
 | |
|             $browserDetails = get_browser(null, True);
 | |
|             $geoLocation = json_decode(file_get_contents("http://ip-api.com/json/{$_SERVER['HTTP_X_REAL_IP']}"));
 | |
|             if ($geoLocation->status === 'fail') {
 | |
|                 switch ($geoLocation->message) {
 | |
|                     case 'private range':
 | |
|                     case 'reserved range':
 | |
|                         $geoLocation = json_decode(file_get_contents("http://ip-api.com/json/" . trim(file_get_contents('https://api.ipify.org')) ));
 | |
|                         break;
 | |
|                     case 'invalid query':
 | |
|                     default:
 | |
|                         $geoLocation->city = null;
 | |
|                         $geoLocation->countryCode = null;
 | |
|                         break;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             // 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)
 | |
|                 'fp'    => base64_encode(json_encode((object) [     // Fingerprint
 | |
|                     'browser'       => $browserDetails['browser'],
 | |
|                     'platform'      => $browserDetails['platform'],
 | |
|                     'city'          => $geoLocation->city,
 | |
|                     'countrycode'   => $geoLocation->countryCode
 | |
|                 ]))
 | |
| 			];
 | |
| 
 | |
| 			$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) {
 | |
|         purgeTokens($currentUserId, $settings->Session['Duration']);
 | |
| 
 | |
|         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'];
 | |
| 	}
 | |
| }
 | |
| 
 | |
| function purgeTokens(int $userID, int $maximumTokenAge) {
 | |
| 	global $settings, $pdoDB;
 | |
| 
 | |
|     $defunctTokens = []; $expiredTokens = [];
 | |
| 
 | |
|     $pdoQuery = $pdoDB->prepare('
 | |
| 		SELECT SecureToken.Id, SecureToken.Value
 | |
| 		FROM SecureToken
 | |
| 		WHERE SecureToken.UserId = :userid
 | |
| 	');
 | |
| 	$pdoQuery->execute([
 | |
| 		':userid'	=>	(int) $userID
 | |
| 	]);
 | |
| 	foreach($pdoQuery->fetchAll(PDO::FETCH_ASSOC) as $row) {
 | |
| 		try {
 | |
|             $token = JWT::decode($row['Value'], base64_decode($settings->JWT['PrivateKey_base64']), $settings->JWT['Algorithm']);
 | |
|             if ($token->iat < (time() - $maximumTokenAge)) {
 | |
|                 $expiredTokens[] = $row['Id'];
 | |
|             }
 | |
| 		} catch (Exception $e) {
 | |
| 			$defunctTokens[] = $row['Id'];
 | |
| 		}
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|         // Sadly, PDO does not support named parameters in constructions like 'IN ( :array )'
 | |
|         //   instead, the supported syntax is unnamed placeholders like 'IN (?, ?, ?, ...)'
 | |
|         $pdoQuery = $pdoDB->prepare('
 | |
|             DELETE FROM SecureToken
 | |
|             WHERE SecureToken.Id IN (' . implode( ',', array_fill(0, count(array_merge($defunctTokens, $expiredTokens)), '?')) . ')
 | |
|         ');
 | |
|         $pdoQuery->execute(array_merge($defunctTokens, $expiredTokens));
 | |
| 
 | |
|         if ($settings->Debug['LogToFile']) {
 | |
|             file_put_contents('../purgeToken.log', (new DateTime())->format('Y-m-d\TH:i:s.u') . ' --- Garbage collection succeeded (' . $userID . ' => #' . $pdoQuery->rowCount() . ')' . PHP_EOL, FILE_APPEND);
 | |
|         }
 | |
| 
 | |
|         return [
 | |
|             'status'    => 'Success',
 | |
|             'amount'    => $pdoQuery->rowCount()
 | |
|         ];
 | |
|     } catch (Exception $e) {
 | |
|         if ($settings->Debug['LogToFile']) {
 | |
|             file_put_contents('../purgeToken.log', (new DateTime())->format('Y-m-d\TH:i:s.u') . ' --- Garbage collection failed (' . $userID . ' => ' . $e . ')' . PHP_EOL, FILE_APPEND);
 | |
|         }
 | |
| 
 | |
|         return ['status' => 'Fail', 'reason' => $e];
 | |
|     }
 | |
| }
 | |
| 
 | |
| function deleteToken(array $tokenIDs, int $userID) {
 | |
|     try {
 | |
|         // Sadly, PDO does not support named parameters in constructions like 'IN ( :array )'
 | |
|         //   instead, the supported syntax is unnamed placeholders like 'IN (?, ?, ?, ...)'
 | |
|         $pdoQuery = $pdoDB->prepare('
 | |
|             DELETE FROM SecureToken
 | |
|             WHERE SecureToken.Id IN (' . implode( ',', array_fill(0, count($tokenIDs), '?')) . ')
 | |
|                 AND SecureToken.UserId = :userid
 | |
|         ');
 | |
|         $pdoQuery->execute($tokenIDs,[
 | |
|     		':userid'	=>	(int) $userID
 | |
|         ]);
 | |
| 
 | |
|         if ($settings->Debug['LogToFile']) {
 | |
|             file_put_contents('../deleteToken.log', (new DateTime())->format('Y-m-d\TH:i:s.u') . ' --- Successfully deleted specific token(s) (' . $userID . ' => #' . $pdoQuery->rowCount() . ')' . PHP_EOL, FILE_APPEND);
 | |
|         }
 | |
| 
 | |
|         return [
 | |
|             'status'    => 'Success',
 | |
|             'amount'    => $pdoQuery->rowCount()
 | |
|         ];
 | |
|     } catch (Exception $e) {
 | |
|         if ($settings->Debug['LogToFile']) {
 | |
|             file_put_contents('../deleteToken.log', (new DateTime())->format('Y-m-d\TH:i:s.u') . ' --- Failed deleting specific token(s) (' . $userID . ' => ' . $e . ')' . PHP_EOL, FILE_APPEND);
 | |
|         }
 | |
| 
 | |
|         return ['status' => 'Fail', 'reason' => $e];
 | |
|     }
 | |
| 
 | |
| }
 | |
| 
 | |
| ?>
 |