diff --git a/tequila.php b/tequila.php index 2a74123..f784a4b 100644 --- a/tequila.php +++ b/tequila.php @@ -1,414 +1,416 @@ // Copyright (C) 2021 Liip SA // Copyright (C) 2021 Doran Kayoumi // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see require __DIR__ . '/vendor/autoload.php'; require_once "exception.php"; class TequilaClient { - const VERSION = "4.0.0"; + const VERSION = "4.1.0"; const TEQUILA_BIN = "/cgi-bin/tequila"; const COOKIE_NAME = "TequilaPHP"; const COOKIE_LIFE = 0; const BODY_GLUE = "+"; const SESSION_KEY = "Tequila-Session-Key"; const SESSION_CREATION = "Tequila-Session-Creation"; const LANGUAGE_FRENCH = 0; const LANGUAGE_ENGLISH = 1; const LANGUAGE_GERMAN = 2; const SERVER_ENDPOINTS = ["createrequest", "fetchattributes", "logout"]; private $serverURL; private $timeout; private $logoutURL; private $language; private $applicationURL; private $applicationName; private $attributes = []; private $key; private $stack; /** * @brief Class constructor * * @codeCoverageIgnore * * @param $server - Tequila server URL * @param $timeout - Session timeout * @param $applicationName - Name of the application using the client * @param $applicationURL - (optional) URL the application using the client * @param $language - (Default: English) Language of the application */ function __construct( string $server, int $timeout, string $applicationName, string $applicationURL = "", int $language = self::LANGUAGE_ENGLISH, callable $handler = null, bool $debug = false ) { $this->serverURL = $server . self::TEQUILA_BIN; $this->timeout = $timeout; $this->language = $language; $this->applicationURL = $applicationURL; $this->applicationName = $applicationName; $this->debug = (bool) $debug; $this->stack = GuzzleHttp\HandlerStack::create($handler); // if no application URL was specified, we try to generate it if (empty($this->applicationURL)) { $this->applicationURL = $this->serverApplicationURL(); } } /** * @brief User authentication to server * * @param $wantedAttributes - (optional) List of attributes about the user that the server will return * @param $filters - (optional) Filters that will be applied to the user attributes * @param $authorised - (optional) Tequila server restrictions to lift */ public function authenticate( array $wantedAttributes = [], string $filters = "", string $authorised = "", array $allowedRequestHosts = [] ) { $this->log(__FUNCTION__ . "(...)"); if ($this->preExistingSession()) { return; } - // check if a session creation was already started. - if (!empty($_COOKIE[self::COOKIE_NAME])) { - $attributes = $this->fetchAttributes($_COOKIE[self::COOKIE_NAME], $allowedRequestHosts); + // fetchAttributes needs valid auth_check param and a valid session creation. + if (!empty($_COOKIE[self::COOKIE_NAME]) && isset($_GET["auth_check"])) { + $attributes = $this->fetchAttributes($_COOKIE[self::COOKIE_NAME], $_GET["auth_check"], $allowedRequestHosts); if ( !empty($attributes) && isset($attributes['uniqueid']) && isset($attributes['user']) && isset($attributes['key']) ) { // Only create a valid session and keep the key if the mandatory attributes are present. $this->key = $_COOKIE[self::COOKIE_NAME]; $this->createSession($attributes); return; } } $this->key = $this->createRequest($wantedAttributes, $filters, $authorised); setcookie(self::COOKIE_NAME, $this->key, self::COOKIE_LIFE, "", "", (strpos($this->applicationURL, "https://") === 0), true ); header("Location: {$this->serverURL}/requestauth?requestkey={$this->key}"); exit; } /** * @brief Logout from Tequila server * * @param $redirectUrl - (optional) URL to redirect to after logout */ public function logout(string $redirectUrl = "") { $this->log(__FUNCTION__ . "(...)"); // Delete cookie by setting expiration time in the past with root path setcookie(self::COOKIE_NAME, "", time() - 3600); unset($_SESSION[self::SESSION_KEY]); unset($_SESSION[self::SESSION_CREATION]); $this->contactServer("logout"); $redirectUrl = empty($redirectUrl) ? $this->applicationURL : urlencode($redirectUrl); header("Location: {$this->serverURL}/logout?urlaccess={$redirectUrl}"); unset($this->key); } /** * @brief Sends an authentication request * * @param $wantedAttributes - (optional) List of attributes about the user that the server will return * @param $filters - (optional) Filters that will be applied to the user attributes * @param $authorised - (optional) Tequila server restrictions to lift * * @return Key returned by the Tequila server */ private function createRequest( array $wantedAttributes = [], string $filters = "", string $authorised = "" ) : string { $body = []; - $body["urlaccess"] = $this->applicationURL; - $body["dontappendkey"] = "1"; - $body["language"] = $this->language; - $body["service"] = $this->applicationName; - $body["request"] = implode(self::BODY_GLUE, $wantedAttributes); - $body["allows"] = $filters; - $body["require"] = $authorised; + $body["urlaccess"] = $this->applicationURL; + $body["dontappendkey"] = "1"; + $body["language"] = $this->language; + $body["service"] = $this->applicationName; + $body["request"] = implode(self::BODY_GLUE, $wantedAttributes); + $body["allows"] = $filters; + $body["require"] = $authorised; + $body["mode_auth_check"]= "1"; $res = $this->contactServer('createrequest', $body); preg_match('/^(?P\w+)=(?P\w+)\s*$/', $res, $matches); if (!empty($matches["key"]) && $matches["key"] == "key") { $this->log(__FUNCTION__ . "(...): ".$matches["value"]); return $matches["value"]; } $this->log(__FUNCTION__ . "(...): got no requestkey"); throw new TequilaException("No requestkey obtained from createRequest"); } /** * @brief Retrieve the attributes of an authenticated user * (i.e. the ones requested when establishing an authentication) * * @param $key - the request key * * @return Array containing all the user attributes */ - private function fetchAttributes(string $key, array $allowedRequestHosts = []) : array { + private function fetchAttributes(string $key, string $auth_check, array $allowedRequestHosts = []) : array { $this->log(__FUNCTION__ . "(...)"); $body = []; $body["key"] = $key; + $body["auth_check"] = $auth_check; $body["allowedrequesthosts"] = implode("|", $allowedRequestHosts); try { $res = $this->contactServer('fetchattributes', $body); } catch (TequilaException $e) { // fetchAttributes failed, return empty return []; } $result = []; $attributes = explode("\n", $res); foreach ($attributes as $attribute) { $attribute = trim($attribute); if (!$attribute) { continue; } list($key, $val) = explode("=", $attribute, 2); $result[$key] = $val; } return $result; } /** * @brief Sends a POST request to one of the servers endpoints * * @param $endpoint - the server endpoint to contact * @param $fields - (optional) the fields to add to the requests body * * @throws TequilaException if the server returns a code other than 200, that no connection could be established * or that we're trying to acces an unknow endpoint * * @return Body of the server response */ private function contactServer($endpoint, $fields = []) : string { // check if it's a valid endpoint if (!in_array($endpoint, self::SERVER_ENDPOINTS)) { throw new TequilaException("Unknown endpoint {$endpoint}"); } $this->log(__FUNCTION__ ."(".$endpoint.", ".preg_replace("#\r|\n#", "", var_export($fields, true)).")"); try { $client = new GuzzleHttp\Client([ 'base_uri' => $this->serverURL . "/", "handler" => $this->stack ]); /** * Note: First things first, sorry for the horrible code that follows. * So, the Tequila server doesn't understand/use normal POST requests and * only works cleartext body. */ $reqBody = []; if (is_array($fields) && count($fields)) { foreach ($fields as $key => $val) { $reqBody[] = "{$key}={$val}"; } } $response = $client->request("POST", $endpoint, [ "headers" => ["User-Agent" => "Tequila-PHP-Client/".self::VERSION], "body" => implode("\n", $reqBody) . "\n", ]); return $response->getBody(); } catch (GuzzleHttp\Exception\RequestException $e) { if (!$e->hasResponse()) { throw new TequilaException("No response from server : {$e->getMessage()}", 1, $e); } $response = $e->getResponse(); throw new TequilaException("Unexpected return from server : [{$response->getStatusCode()}] {$response->getReasonPhrase()}", 1, $e); } catch (GuzzleHttp\Exception\ConnectException $e) { throw new TequilaException("Connection to {$this->serverURL} server failed", 0, $e); } } /** * @brief Check if a session was previously established * * @return True if a session was previously established * @return False if no session was previously established */ private function preExistingSession() : bool { if (empty($_SESSION)) { return false; } if (empty($this->attributes)) { return false; } // check if the session hasn't expired. if ( !array_key_exists(self::SESSION_CREATION, $_SESSION) or (time() - $_SESSION[self::SESSION_CREATION]) > $this->timeout ) { return false; } if (!array_key_exists(self::SESSION_KEY, $_SESSION)) { return false; } $this->key = $_SESSION[self::SESSION_KEY]; return true; } /** * @brief Establish a new session * * @param $attributes - the user attributes returned by the server */ private function createSession(array $attributes) { $_SESSION[self::SESSION_CREATION] = time(); foreach ($attributes as $key => $val) { $this->attributes[$key] = $val; } if (array_key_exists("key", $attributes)) { $_SESSION[self::SESSION_KEY] = $attributes["key"]; } } /** * @brief Determine the applicationURL from $_SERVER */ private function serverApplicationURL(array $server = []) : string { // Primarily for tests // @codeCoverageIgnoreStart if (empty($server)) { global $_SERVER; $server = $_SERVER; } // @codeCoverageIgnoreEnd $protocol = !empty($server['HTTPS']) && $server['HTTPS'] == 'on' ? "https://" : "http://"; $port = !empty($server["SERVER_PORT"]) ? $server["SERVER_PORT"] : ($protocol == "https://" ? 443 : 80); $dotport = ":".$port; if ( ($protocol == "https://" && $port == 443) || ($protocol == "http://" && $port == 80) ) { $dotport = ""; } $php_self = !empty($server["PHP_SELF"]) ? $server["PHP_SELF"] : "/"; $applicationURL = $protocol . $server['SERVER_NAME'] . $dotport . $php_self; if (!empty($server['PATH_INFO'])) { $applicationURL .= $server['PATH_INFO']; } if (!empty($server['QUERY_STRING'])) { $applicationURL .= "?" . $server['QUERY_STRING']; } $this->log(__FUNCTION__ ."(): {$applicationURL}"); return $applicationURL; } /** * Getter for key * * @codeCoverageIgnore */ public function getKey() : string { return $this->key; } /** * Getter for attributes * * @codeCoverageIgnore */ public function getAttributes() : array { return $this->attributes; } /** * If debug mode enabled * * @return bool */ public function is_debugging() { return $this->debug; } /** * A debug function, dumps to the php log * * @codeCoverageIgnore * @param string $msg Log message */ private function log($msg) { if ($this->is_debugging()) { error_log('tequila-php-client: ' . $msg); } } }