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