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