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