1, // Association writer basic "artist-standard" => 1, // Association writer basic "school-premium" => 3, // Association writer full "artist-premium" => 3, // Association writer full "manager" => 3, // Association writer full ]; /** * The time in seconds during which the user's data in DB won't be re-updated after the last successful update * Set it to 0 to disable the delay */ CONST USER_UPDATE_DELAY = 300; /** * 0 - Authentification failed, no more services will be called... * @see https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Authentication/Index.html#the-service-chain * * @var int */ const STATUS_AUTHENTICATION_FAILURE = 0; /** * 100 - OK, but call next services... * @see https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Authentication/Index.html#the-service-chain * * @var int */ const STATUS_AUTHENTICATION_CONTINUE = 100; /** * 200 - authenticated and no more checking needed * @see https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Authentication/Index.html#the-service-chain * * @var int */ const STATUS_AUTHENTICATION_SUCCESS = 200; /** * @var object */ private object $apiService; /** * Guzzle Cookie Jar * * @var CookieJar */ private CookieJar $jar; /** * @var \TYPO3\CMS\Core\Database\ConnectionPool */ private $connectionPool; public function injectConnectionPool(ConnectionPool $connectionPool) { $this->connectionPool = $connectionPool; } /** * OtAuthenticationService constructor. */ public function __construct() { $this->jar = new CookieJar; $this->apiService = GeneralUtility::makeInstance(OpentalentApiService::class, null, null, $this->jar); $this->connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); } /** * This function returns the user record back to the AbstractUserAuthentication. * It does not mean that user is authenticated, it only means that user is found. * /!\ The 'getUser' method is required by the Typo3 authentification system * @see https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Authentication/Index.html#the-auth-services-api * * @return array|bool User record or false (content of fe_users/be_users as appropriate for the current mode) * @throws GuzzleException */ public function getUser() { // Does the user already have a session on the Opentalent API? $username = $this->getAuthenticatedUsername(); if ($username != null && $this->authInfo['loginType'] == 'FE' && $this->login['status'] === 'logout') { // This is a logout request $this->logout(); return false; } if ($username != null && $this->login['status'] === 'login' && $this->login['uname'] != $username) { // The user trying to log in is not the one authenticated on the Opentalent API // We let the TYPO3 auth service handle it return false; } else if ($username == null && $this->login['status'] != 'login') { // The user has no current session on Opentalent.fr and this is not a login request return false; } else if ($this->login['status'] === 'login' && $this->login['uname'] && $this->login['uident']) { // This is a login request $username = $this->login['uname']; $password = $this->login['uident']; // Send a login request for the user to the Opentalent Api, and return the data // of the matching user, or false if le login failed $logged = $this->logUser($username, $password); if (!$logged) { return false; } } // Request the latest data for the user and write it in the Typo3 DB // * The shouldUserBeUpdated() method checks if the user was already // generated in the last minutes, to avoid unnecessary operations * if ($this->shouldUserBeUpdated($username)) { $wasUpdated = $this->createOrUpdateUser(); if (!$wasUpdated) { // An error happened during the update of the user's data // since its data may have changed (credentials, rights, rĂ´les...) // we can't allow him to connect. return false; } } // No need to check Pid for those users $this->authInfo['db_user']['checkPidList'] = ''; $this->authInfo['db_user']['check_pid_clause'] = ''; // Fetch the typo3 user from the database return $this->fetchUserRecord($username, '', $this->authInfo['db_user']); } /** * Returns the name of the user currently authenticated on the API side, or null if no user is logged in * * @return string|null * @throws GuzzleException */ protected function getAuthenticatedUsername(): ?string { $this->fillCookieJar(); try { $response = $this->apiService->get(self::ISAUTH_URI, [], ['cookies' => $this->jar]); if ($response->getStatusCode() != 200) { return null; } return json_decode((string)$response->getBody()); } catch (ApiRequestException $e) { return null; } } /** * Update the guzzle cookie jar with the current session's ones */ private function fillCookieJar() { foreach (['BEARER', 'SFSESSID'] as $cookieName) { if (array_key_exists($cookieName, $_COOKIE)) { $cookie = new SetCookie(); $cookie->setName($cookieName); $cookie->setValue($_COOKIE[$cookieName]); $cookie->setDomain(self::COOKIE_DOMAIN); $this->jar->setCookie($cookie); } } } /** * Submit a login request to the API * * @param string $username * @param string $password * @return bool Returns true if the api accepted the login request * @throws GuzzleException */ protected function logUser(string $username, string $password): bool { try { $response = $this->apiService->request( 'POST', self::LOGIN_URI, [], ['form_params' => ['_username' => $username, '_password' => $password]] ); if ($response->getStatusCode() != 200) { return false; } // The API accepted the login request // Set the cookies returned by the Api (SESSID and BEARER) $this->setCookiesFromApiResponse($response); return true; } catch (ApiRequestException $e) { return false; } } /** * Get the cookies from the API response and set them * * @param $response */ private function setCookiesFromApiResponse($response) { foreach ($response->getHeader('Set-Cookie') as $cookieStr) { $cookie = SetCookie::fromString($cookieStr); $name = $cookie->getName(); $value = $cookie->getValue(); $expires = $cookie->getExpires(); $path = $cookie->getPath(); $secure = $cookie->getSecure(); $httpOnly = $cookie->getHttpOnly(); $_COOKIE[$name] = $value; setcookie($name, $value, $expires, $path, self::COOKIE_DOMAIN, $secure, $httpOnly); setcookie($name, $value, $expires, $path, '.' . self::COOKIE_DOMAIN, $secure, $httpOnly); if (!preg_match('/(.*\.)?opentalent\.fr/', $_SERVER['HTTP_HOST'])) { setcookie($name, $value, $expires, $path, $_SERVER['HTTP_HOST'], $secure, $httpOnly); } } } /** * Compare the last update date for the user to the GENERATION_DELAY delay * and return wether the user's data may be created/updated in the Typo3 DB * * @param string $username * @return bool */ protected function shouldUserBeUpdated(string $username): bool { $cnn = $this->connectionPool->getConnectionForTable('fe_users'); $q = $cnn->select(['tx_opentalent_generationDate'], 'fe_users', ['username' => $username]); $strGenDate = $q->fetch(3)[0]; $genDate = DateTime::createFromFormat("Y-m-d H:i:s", $strGenDate); if ($genDate == null) { return true; } $now = new DateTime(); $diff = $now->getTimestamp() - $genDate->getTimestamp(); return ($diff > self::USER_UPDATE_DELAY); } /** * Create or update the Frontend-user record in the typo3 database (table 'fe_users') * with the data fetched from the Api * * @return bool */ protected function createOrUpdateUser(): bool { // Get user's data from the API $userApiData = $this->getUserData(); if (empty($userApiData)) { // An error happened, and even if the user was logged, we can not continue // (user's data and rights could have changed) return false; } $connection = $this->connectionPool->getConnectionForTable('fe_users'); // Since we don't want to store the password in the TYPO3 DB, we store a random string instead $randomStr = (new Random)->generateRandomHexString(20); // Front-end user $fe_row = [ 'username' => $userApiData['username'], 'password' => $randomStr, 'name' => $userApiData['name'], 'first_name' => $userApiData['first_name'], 'description' => '[Warning: auto-generated record, do not modify] FE User', 'usergroup' => self::GROUP_FE_ALL_UID, 'deleted' => 0, 'tx_opentalent_opentalentId' => $userApiData['id'], 'tx_opentalent_generationDate' => date('Y/m/d H:i:s') ]; // TODO: log a warning if a user with the same opentalentId exists (the user might have changed of username) $q = $connection->select( ['uid', 'tx_opentalent_opentalentId'], 'fe_users', ['username' => $userApiData['username']] ); $row = $q->fetch(3); $uid = $row[0]; $tx_opentalent_opentalentId = $row[1]; if (!$uid) { // No existing user: create $connection->insert('fe_users', $fe_row); } else { // User exists: update if (!$tx_opentalent_opentalentId > 0) { $this->writeLogMessage('WARNING: FE user ' . $userApiData['username'] . ' has been replaced by an auto-generated version.'); } $connection->update('fe_users', $fe_row, ['uid' => $uid]); } return true; } /** * Get the data for the current authenticated user from the API * * @return array */ protected function getUserData(): ?array { $this->fillCookieJar(); try { $response = $this->apiService->request('GET', self::GET_USER_DATA_URI, [], ['cookies' => $this->jar]); } catch (ApiRequestException $e) { return []; } return json_decode($response->getBody(), true); } /** * Authenticates user using Opentalent auth service. * /!\ The 'authUser' method is required by the Typo3 authentification system * @see https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Authentication/Index.html#the-auth-services-api * * @param array $user Data of user. * @return int Code that shows if user is really authenticated. * @throws GuzzleException */ public function authUser(array $user): int { if ($user['username'] == $this->getAuthenticatedUsername()) { // Tha API just validated this user's auth return self::STATUS_AUTHENTICATION_SUCCESS; } else if ($this->authInfo['loginType'] === 'FE') { return self::STATUS_AUTHENTICATION_FAILURE; } else if (isset($user['tx_opentalent_opentalentId']) and $user['tx_opentalent_opentalentId'] != null) { // This is a user from the Opentalent DB, and the API refused its auth // (For performance only, since the password stored in the Typo3 is a random string, // the auth will be refused by the other services anyway) return self::STATUS_AUTHENTICATION_FAILURE; } // This may be a user using another auth system return self::STATUS_AUTHENTICATION_CONTINUE; } /** * Send a logout request to the API, remove the sessions cookies then logout * /!\ Frontend only */ public function logout(): bool { try { $response = $this->apiService->request( 'GET', self::LOGOUT_URI ); if ($response->getStatusCode() != 200) { return false; } // The API accepted the logout request // Unset the session cookies (SESSID and BEARER) if (isset($_COOKIE['BEARER'])) { unset($_COOKIE['BEARER']); $this->unset_cookie('BEARER'); } if (isset($_COOKIE['SFSESSID'])) { unset($_COOKIE['SFSESSID']); $this->unset_cookie('SFSESSID'); } $this->pObj->logoff(); return true; } catch (RequestException $e) { return false; } catch (GuzzleException $e) { return false; } } /** * Unset a cookie by reducing its expiration date * * @param string $name */ protected function unset_cookie(string $name) { setcookie($name, '', 1, '/', $_SERVER['HTTP_HOST']); // for custom domains (not in .opentalent.fr) setcookie($name, '', 1, '/', self::COOKIE_DOMAIN); // for opentalent.fr subdomains } /** * Writes log message. Destination log depends on the current system mode. * For FE the function writes to the admin panel log. For BE messages are * sent to the system log. If developer log is enabled, messages are also * sent there. * * This function accepts variable number of arguments and can format * parameters. The syntax is the same as for sprintf() * * @param string $message Message to output * @param array $params * @see \TYPO3\CMS\Core\Utility\GeneralUtility::sysLog() */ public function writeLogMessage($message, ...$params) { if (!empty($params)) { $message = vsprintf($message, $params); } if (TYPO3_MODE === 'BE') { GeneralUtility::sysLog($message, 'ot_connect'); } else { /** @var TimeTracker $timeTracker */ $timeTracker = GeneralUtility::makeInstance(TimeTracker::class); $timeTracker->setTSlogMessage($message); } } }