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