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