[TASK] Re-work/simplify copyright header in PHP files - Part 2
[Packages/TYPO3.CMS.git] / typo3 / sysext / openid / Classes / OpenidService.php
1 <?php
2 namespace TYPO3\CMS\Openid;
3
4 /**
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Utility\GeneralUtility;
18 use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
19
20 require_once \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('openid') . 'lib/php-openid/Auth/OpenID/Interface.php';
21
22 /**
23 * Service "OpenID Authentication" for the "openid" extension.
24 *
25 * @author Dmitry Dulepov <dmitry@typo3.org>
26 */
27 class OpenidService extends \TYPO3\CMS\Core\Service\AbstractService {
28
29 /**
30 * The extension key
31 */
32 public $extKey = 'openid';
33
34 /**
35 * Login data as passed to initAuth()
36 */
37 protected $loginData = array();
38
39 /**
40 * Additional authentication information provided by AbstractUserAuthentication.
41 * We use it to decide what database table contains user records.
42 */
43 protected $authenticationInformation = array();
44
45 /**
46 * OpenID response object. It is initialized when OpenID provider returns
47 * with success/failure response to us.
48 *
49 * @var \Auth_OpenID_ConsumerResponse
50 */
51 protected $openIDResponse = NULL;
52
53 /**
54 * A reference to the calling object
55 *
56 * @var AbstractUserAuthentication
57 */
58 protected $parentObject;
59
60 /**
61 * @var \TYPO3\CMS\Core\Database\DatabaseConnection
62 */
63 protected $databaseConnection;
64
65 /**
66 * If set to TRUE, than libraries are already included.
67 */
68 static protected $openIDLibrariesIncluded = FALSE;
69
70 /**
71 * Contructs the OpenID authentication service.
72 */
73 public function __construct() {
74 // Auth_Yadis_Yadis::getHTTPFetcher() will use a cURL fetcher if the functionality
75 // is available in PHP, however the TYPO3 setting is not considered here:
76 if (!defined('Auth_Yadis_CURL_OVERRIDE')) {
77 if (!$GLOBALS['TYPO3_CONF_VARS']['SYS']['curlUse']) {
78 define('Auth_Yadis_CURL_OVERRIDE', TRUE);
79 }
80 }
81
82 $this->injectDatabaseConnection();
83 }
84
85 /**
86 * @param \TYPO3\CMS\Core\Database\DatabaseConnection $databaseConnection
87 */
88 protected function injectDatabaseConnection(\TYPO3\CMS\Core\Database\DatabaseConnection $databaseConnection = NULL) {
89 $this->databaseConnection = $databaseConnection ?: $GLOBALS['TYPO3_DB'];
90 }
91
92 /**
93 * Checks if service is available,. In case of this service we check that
94 * prerequesties for "PHP OpenID" libraries are fulfilled:
95 * - GMP or BCMATH PHP extensions are installed and functional
96 * - set_include_path() PHP function is available
97 *
98 * @return boolean TRUE if service is available
99 */
100 public function init() {
101 $available = FALSE;
102 if (extension_loaded('gmp')) {
103 $available = is_callable('gmp_init');
104 } elseif (extension_loaded('bcmath')) {
105 $available = is_callable('bcadd');
106 } else {
107 $this->writeLog('Neither bcmath, nor gmp PHP extension found. OpenID authentication will not be available.');
108 }
109 // We also need set_include_path() PHP function
110 if (!is_callable('set_include_path')) {
111 $available = FALSE;
112 $this->writeLog('set_include_path() PHP function is not available. OpenID authentication is disabled.');
113 }
114 return $available ? parent::init() : FALSE;
115 }
116
117 /**
118 * Initializes authentication for this service.
119 *
120 * @param string $subType: Subtype for authentication (either "getUserFE" or "getUserBE")
121 * @param array $loginData: Login data submitted by user and preprocessed by AbstractUserAuthentication
122 * @param array $authenticationInformation: Additional TYPO3 information for authentication services (unused here)
123 * @param AbstractUserAuthentication $parentObject Calling object
124 * @return void
125 */
126 public function initAuth($subType, array $loginData, array $authenticationInformation, AbstractUserAuthentication &$parentObject) {
127 // Store login and authetication data
128 $this->loginData = $loginData;
129 $this->authenticationInformation = $authenticationInformation;
130 // If we are here after authentication by the OpenID server, get its response.
131 if (\TYPO3\CMS\Core\Utility\GeneralUtility::_GP('tx_openid_mode') === 'finish' && $this->openIDResponse === NULL) {
132 $this->includePHPOpenIDLibrary();
133 $openIDConsumer = $this->getOpenIDConsumer();
134 $this->openIDResponse = $openIDConsumer->complete($this->getReturnURL(GeneralUtility::_GP('tx_openid_claimed')));
135 }
136 $this->parentObject = $parentObject;
137 }
138
139 /**
140 * Process the submitted OpenID URL if valid.
141 *
142 * @param array $loginData Credentials that are submitted and potentially modified by other services
143 * @param string $passwordTransmissionStrategy Keyword of how the password has been hashed or encrypted before submission
144 * @return boolean
145 */
146 public function processLoginData(array &$loginData, $passwordTransmissionStrategy) {
147 $isProcessed = FALSE;
148 // Pre-process the login only if no password has been submitted
149 if (empty($loginData['uident_text'])) {
150 try {
151 $openIdUrl = GeneralUtility::_POST('openid_url');
152 if (!empty($openIdUrl)) {
153 $loginData['uident_openid'] = $this->normalizeOpenID($openIdUrl);
154 $isProcessed = TRUE;
155 } elseif (!empty($loginData['uname'])) {
156 // It might be the case that during frontend login the OpenID URL is submitted in the username field
157 // Since we are a low priority service, and no password has been submitted it is OK to just assume
158 // we might have gotten an OpenID URL
159 $loginData['uident_openid'] = $this->normalizeOpenID($loginData['uname']);
160 $isProcessed = TRUE;
161 }
162 } catch (Exception $e) {
163 $this->writeLog($e->getMessage());
164 }
165 }
166 return $isProcessed;
167 }
168
169 /**
170 * This function returns the user record back to the AbstractUserAuthentication.
171 * It does not mean that user is authenticated, it means only that user is found. This
172 * function makes sure that user cannot be authenticated by any other service
173 * if user tries to use OpenID to authenticate.
174 *
175 * @return mixed User record (content of fe_users/be_users as appropriate for the current mode)
176 */
177 public function getUser() {
178 if ($this->loginData['status'] !== 'login') {
179 return NULL;
180 }
181 $userRecord = NULL;
182 if ($this->openIDResponse instanceof \Auth_OpenID_ConsumerResponse) {
183 $GLOBALS['BACK_PATH'] = $this->getBackPath();
184 // We are running inside the OpenID return script
185 // Note: we cannot use $this->openIDResponse->getDisplayIdentifier()
186 // because it may return a different identifier. For example,
187 // LiveJournal server converts all underscore characters in the
188 // original identfier to dashes.
189 if ($this->openIDResponse->status === Auth_OpenID_SUCCESS) {
190 $openIDIdentifier = $this->getFinalOpenIDIdentifier();
191 if ($openIDIdentifier) {
192 $userRecord = $this->getUserRecord($openIDIdentifier);
193 if (!empty($userRecord) && is_array($userRecord)) {
194 // The above function will return user record from the OpenID. It means that
195 // user actually tried to authenticate using his OpenID. In this case
196 // we must change the password in the record to a long random string so
197 // that this user cannot be authenticated with other service.
198 $userRecord[$this->authenticationInformation['db_user']['userident_column']] = GeneralUtility::getRandomHexString(42);
199 $this->writeLog('User \'%s\' logged in with OpenID \'%s\'', $userRecord[$this->parentObject->formfield_uname], $openIDIdentifier);
200 } else {
201 $this->writeLog('Failed to login user using OpenID \'%s\'', $openIDIdentifier);
202 }
203 }
204 }
205 } elseif (!empty($this->loginData['uident_openid'])) {
206 $this->sendOpenIDRequest($this->loginData['uident_openid']);
207 }
208 return $userRecord;
209 }
210
211 /**
212 * Authenticates user using OpenID.
213 *
214 * @param array $userRecord User record
215 * @return integer Code that shows if user is really authenticated.
216 */
217 public function authUser(array $userRecord) {
218 $result = 100;
219 // 100 means "we do not know, continue"
220 if ($userRecord['tx_openid_openid'] !== '') {
221 // Check if user is identified by the OpenID
222 if ($this->openIDResponse instanceof \Auth_OpenID_ConsumerResponse) {
223 // If we have a response, it means OpenID server tried to authenticate
224 // the user. Now we just look what is the status and provide
225 // corresponding response to the caller
226 if ($this->openIDResponse->status === Auth_OpenID_SUCCESS) {
227 // Success (code 200)
228 $result = 200;
229 } else {
230 $this->writeLog('OpenID authentication failed with code \'%s\'.', $this->openIDResponse->status);
231 }
232 }
233 }
234 return $result;
235 }
236
237 /**
238 * Includes necessary files for the PHP OpenID library
239 *
240 * @return void
241 */
242 protected function includePHPOpenIDLibrary() {
243 if (!self::$openIDLibrariesIncluded) {
244 // Prevent further calls
245 self::$openIDLibrariesIncluded = TRUE;
246 // PHP OpenID libraries requires adjustments of path settings
247 $oldIncludePath = get_include_path();
248 $phpOpenIDLibPath = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('openid') . 'lib/php-openid';
249 @set_include_path(($phpOpenIDLibPath . PATH_SEPARATOR . $phpOpenIDLibPath . PATH_SEPARATOR . 'Auth' . PATH_SEPARATOR . $oldIncludePath));
250 // Make sure that random generator is properly set up. Constant could be
251 // defined by the previous inclusion of the file
252 if (!defined('Auth_OpenID_RAND_SOURCE')) {
253 if (TYPO3_OS === 'WIN') {
254 // No random generator on Windows!
255 define('Auth_OpenID_RAND_SOURCE', NULL);
256 } elseif (!is_readable('/dev/urandom')) {
257 if (is_readable('/dev/random')) {
258 define('Auth_OpenID_RAND_SOURCE', '/dev/random');
259 } else {
260 define('Auth_OpenID_RAND_SOURCE', NULL);
261 }
262 }
263 }
264 // Include files
265 require_once $phpOpenIDLibPath . '/Auth/OpenID/Consumer.php';
266 // Restore path
267 @set_include_path($oldIncludePath);
268 if (!is_array($_SESSION)) {
269 // Yadis requires session but session is not initialized when
270 // processing Backend authentication
271 @session_start();
272 $this->writeLog('Session is initialized');
273 }
274 }
275 }
276
277 /**
278 * Gets user record for the user with the OpenID provided by the user
279 *
280 * @param string $openIDIdentifier OpenID identifier to search for
281 * @return array Database fields from the table that corresponds to the current login mode (FE/BE)
282 */
283 protected function getUserRecord($openIDIdentifier) {
284 $record = NULL;
285 try {
286 $openIDIdentifier = $this->normalizeOpenID($openIDIdentifier);
287 // $openIDIdentifier always has a trailing slash
288 // but tx_openid_openid field possibly not so check for both alternatives in database
289 $record = $this->databaseConnection->exec_SELECTgetSingleRow(
290 '*',
291 $this->authenticationInformation['db_user']['table'],
292 'tx_openid_openid IN ('
293 . $this->databaseConnection->fullQuoteStr($openIDIdentifier, $this->authenticationInformation['db_user']['table'])
294 . ',' . $this->databaseConnection->fullQuoteStr(rtrim($openIDIdentifier, '/'),
295 $this->authenticationInformation['db_user']['table']) . ')'
296 . $this->authenticationInformation['db_user']['check_pid_clause']
297 . $this->authenticationInformation['db_user']['enable_clause']
298 );
299 if ($record) {
300 // Make sure to work only with normalized OpenID during the whole process
301 $record['tx_openid_openid'] = $this->normalizeOpenID($record['tx_openid_openid']);
302 }
303 } catch (Exception $e) {
304 // This should never happen and generally means hack attempt.
305 // We just log it and do not return any records.
306 $this->writeLog($e->getMessage());
307 }
308 return $record;
309 }
310
311 /**
312 * Creates OpenID Consumer object with a TYPO3-specific store. This function
313 * is almost identical to the example from the PHP OpenID library.
314 *
315 * @return \Auth_OpenID_Consumer Consumer instance
316 */
317 protected function getOpenIDConsumer() {
318 /* @var $openIDStore OpenidStore */
319 $openIDStore = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Openid\\OpenidStore');
320 $openIDStore->cleanup();
321 return new \Auth_OpenID_Consumer($openIDStore);
322 }
323
324 /**
325 * Sends request to the OpenID server to authenticate the user with the
326 * given ID. This function is almost identical to the example from the PHP
327 * OpenID library. Due to the OpenID specification we cannot do a slient login.
328 * Sometimes we have to redirect to the OpenID provider web site so that
329 * user can enter his password there. In this case we will redirect and provide
330 * a return adress to the special script inside this directory, which will
331 * handle the result appropriately.
332 *
333 * This function does not return on success. If it returns, it means something
334 * went totally wrong with OpenID.
335 *
336 * @param string $openIDIdentifier The OpenID identifier for discovery and auth request
337 * @return void
338 */
339 protected function sendOpenIDRequest($openIDIdentifier) {
340 $this->includePHPOpenIDLibrary();
341 // Initialize OpenID client system, get the consumer
342 $openIDConsumer = $this->getOpenIDConsumer();
343 // Begin the OpenID authentication process
344 $authenticationRequest = $openIDConsumer->begin($openIDIdentifier);
345 if (!$authenticationRequest) {
346 // Not a valid OpenID. Since it can be some other ID, we just return
347 // and let other service handle it.
348 $this->writeLog('Could not create authentication request for OpenID identifier \'%s\'', $openIDIdentifier);
349 return;
350 }
351 // Redirect the user to the OpenID server for authentication.
352 // Store the token for this authentication so we can verify the
353 // response.
354 // For OpenID version 1, we *should* send a redirect. For OpenID version 2,
355 // we should use a Javascript form to send a POST request to the server.
356 $returnURL = $this->getReturnURL($openIDIdentifier);
357 $trustedRoot = GeneralUtility::getIndpEnv('TYPO3_SITE_URL');
358 if ($authenticationRequest->shouldSendRedirect()) {
359 $redirectURL = $authenticationRequest->redirectURL($trustedRoot, $returnURL);
360 // If the redirect URL can't be built, return. We can only return.
361 if (\Auth_OpenID::isFailure($redirectURL)) {
362 $this->writeLog('Authentication request could not create redirect URL for OpenID identifier \'%s\'', $openIDIdentifier);
363 return;
364 }
365 // Send redirect. We use 303 code because it allows to redirect POST
366 // requests without resending the form. This is exactly what we need here.
367 // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4
368 @ob_end_clean();
369 \TYPO3\CMS\Core\Utility\HttpUtility::redirect($redirectURL, \TYPO3\CMS\Core\Utility\HttpUtility::HTTP_STATUS_303);
370 } else {
371 $formHtml = $authenticationRequest->htmlMarkup($trustedRoot, $returnURL, FALSE, array('id' => 'openid_message'));
372 // Display an error if the form markup couldn't be generated;
373 // otherwise, render the HTML.
374 if (\Auth_OpenID::isFailure($formHtml)) {
375 // Form markup cannot be generated
376 $this->writeLog('Could not create form markup for OpenID identifier \'%s\'', $openIDIdentifier);
377 return;
378 } else {
379 @ob_end_clean();
380 echo $formHtml;
381 }
382 }
383 // If we reached this point, we must not return!
384 die;
385 }
386
387 /**
388 * Creates return URL for the OpenID server. When a user is authenticated by
389 * the OpenID server, the user will be sent to this URL to complete
390 * authentication process with the current site. We send it to our script.
391 *
392 * @param string $claimedIdentifier The OpenID identifier for discovery and auth request
393 * @return string Return URL
394 */
395 protected function getReturnURL($claimedIdentifier) {
396 if ($this->authenticationInformation['loginType'] === 'FE') {
397 // We will use eID to send user back, create session data and
398 // return to the calling page.
399 // Notice: 'pid' and 'logintype' parameter names cannot be changed!
400 // They are essential for FE user authentication.
401 $returnURL = 'index.php?eID=tx_openid&' . 'pid=' . $this->authenticationInformation['db_user']['checkPidList'] . '&' . 'logintype=login&';
402 } else {
403 // In the Backend we will use dedicated script to create session.
404 // It is much easier for the Backend to manage users.
405 // Notice: 'login_status' parameter name cannot be changed!
406 // It is essential for BE user authentication.
407 $absoluteSiteURL = substr(GeneralUtility::getIndpEnv('TYPO3_SITE_URL'), strlen(GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST')));
408 $returnURL = $absoluteSiteURL . TYPO3_mainDir . 'sysext/' . $this->extKey . '/class.tx_openid_return.php?login_status=login&';
409 }
410 if (GeneralUtility::_GP('tx_openid_mode') === 'finish') {
411 $requestURL = GeneralUtility::_GP('tx_openid_location');
412 } else {
413 $requestURL = GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL');
414 }
415 $returnURL .= 'tx_openid_location=' . rawurlencode($requestURL) . '&tx_openid_location_signature=' . $this->getSignature($requestURL) . '&tx_openid_mode=finish&tx_openid_claimed=' . rawurlencode($claimedIdentifier) . '&tx_openid_signature=' . $this->getSignature($claimedIdentifier);
416 return GeneralUtility::locationHeaderUrl($returnURL);
417 }
418
419 /**
420 * Signs a GET parameter.
421 *
422 * @param string $parameter
423 * @return string
424 */
425 protected function getSignature($parameter) {
426 return GeneralUtility::hmac($parameter, $this->extKey);
427 }
428
429 /**
430 * Implement normalization according to OpenID 2.0 specification
431 * See http://openid.net/specs/openid-authentication-2_0.html#normalization
432 *
433 * @param string $openIDIdentifier OpenID identifier to normalize
434 * @return string Normalized OpenID identifier
435 * @throws Exception
436 */
437 protected function normalizeOpenID($openIDIdentifier) {
438 if (empty($openIDIdentifier)) {
439 throw new Exception('Empty OpenID Identifier given.', 1381922460);
440 }
441 // Strip everything with and behind the fragment delimiter character "#"
442 if (strpos($openIDIdentifier, '#') !== FALSE) {
443 $openIDIdentifier = preg_replace('/#.*$/', '', $openIDIdentifier);
444 }
445 // A URI with a missing scheme is normalized to a http URI
446 if (!preg_match('#^https?://#', $openIDIdentifier)) {
447 $escapedIdentifier = $this->databaseConnection->quoteStr($openIDIdentifier, $this->authenticationInformation['db_user']['table']);
448 $condition = 'tx_openid_openid IN ('
449 . '\'http://' . $escapedIdentifier . '\','
450 . '\'http://' . $escapedIdentifier . '/\','
451 . '\'https://' . $escapedIdentifier . '\','
452 . '\'https://' . $escapedIdentifier . '/\''
453 . ')';
454 $row = $this->databaseConnection->exec_SELECTgetSingleRow(
455 'tx_openid_openid',
456 $this->authenticationInformation['db_user']['table'],
457 $condition
458 );
459 if (is_array($row)) {
460 $openIDIdentifier = $row['tx_openid_openid'];
461 } else {
462 // This only happens when the OpenID provider will select the final OpenID identity
463 // In this case we require a valid URL as we cannot guess the scheme
464 // So we throw an Exception and do not start the OpenID handshake at all
465 throw new Exception('Trying to authenticate with OpenID but identifier is neither found in a user record nor it is a valid URL.', 1381922465);
466 }
467 }
468 // An empty path component is normalized to a slash
469 // (e.g. "http://domain.org" -> "http://domain.org/")
470 if (preg_match('#^https?://[^/]+$#', $openIDIdentifier)) {
471 $openIDIdentifier .= '/';
472 }
473 return $openIDIdentifier;
474 }
475
476 /**
477 * Calculates the path to the TYPO3 directory from the current directory
478 *
479 * @return string
480 */
481 protected function getBackPath() {
482 $extPath = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::siteRelPath('openid');
483 $segmentCount = count(explode('/', $extPath));
484 $path = str_pad('', $segmentCount * 3, '../') . TYPO3_mainDir;
485 return $path;
486 }
487
488 /**
489 * Obtains a real identifier for the user
490 *
491 * @return string
492 */
493 protected function getFinalOpenIDIdentifier() {
494 $result = $this->getSignedParameter('openid_identity');
495 if (!$result) {
496 $result = $this->getSignedParameter('openid_claimed_id');
497 }
498 if (!$result) {
499 $result = $this->getSignedClaimedOpenIDIdentifier();
500 }
501 return $result;
502 }
503
504 /**
505 * Gets the signed OpenID that was sent back to this service.
506 *
507 * @return string The signed OpenID, if signature did not match this is empty
508 */
509 protected function getSignedClaimedOpenIDIdentifier() {
510 $result = GeneralUtility::_GP('tx_openid_claimed');
511 $signature = $this->getSignature($result);
512 if ($signature !== GeneralUtility::_GP('tx_openid_signature')) {
513 $result = '';
514 }
515 return $result;
516 }
517
518 /**
519 * Obtains a value of the parameter if it is signed. If not signed, then
520 * empty string is returned.
521 *
522 * @param string $parameterName Must start with 'openid_'
523 * @return string
524 */
525 protected function getSignedParameter($parameterName) {
526 $signedParametersList = GeneralUtility::_GP('openid_signed');
527 if (GeneralUtility::inList($signedParametersList, substr($parameterName, 7))) {
528 $result = GeneralUtility::_GP($parameterName);
529 } else {
530 $result = '';
531 }
532 return $result;
533 }
534
535 /**
536 * Writes log message. Destination log depends on the current system mode.
537 * For FE the function writes to the admin panel log. For BE messages are
538 * sent to the system log. If developer log is enabled, messages are also
539 * sent there.
540 *
541 * This function accepts variable number of arguments and can format
542 * parameters. The syntax is the same as for sprintf()
543 *
544 * @param string $message Message to output
545 * @return void
546 * @see GeneralUtility::sysLog()
547 * @see \TYPO3\CMS\Core\TimeTracker\TimeTracker::setTSlogMessage()
548 */
549 protected function writeLog($message) {
550 if (func_num_args() > 1) {
551 $params = func_get_args();
552 array_shift($params);
553 $message = vsprintf($message, $params);
554 }
555 if (TYPO3_MODE === 'BE') {
556 GeneralUtility::sysLog($message, $this->extKey, GeneralUtility::SYSLOG_SEVERITY_NOTICE);
557 } else {
558 $GLOBALS['TT']->setTSlogMessage($message);
559 }
560 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['enable_DLOG']) {
561 GeneralUtility::devLog($message, $this->extKey, GeneralUtility::SYSLOG_SEVERITY_NOTICE);
562 }
563 }
564
565 }