8 * This source file is subject to the new BSD license that is bundled
9 * with this package in the file LICENSE.txt.
10 * It is also available through the world-wide-web at this URL:
11 * http://framework.zend.com/license/new-bsd
12 * If you did not receive a copy of the license and are unable to
13 * obtain it through the world-wide-web, please send an email
14 * to license@zend.com so we can send you a copy immediately.
17 * @package Zend_OpenId
18 * @subpackage Zend_OpenId_Provider
19 * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
20 * @license http://framework.zend.com/license/new-bsd New BSD License
21 * @version $Id: Provider.php 16212 2009-06-21 19:24:49Z thomas $
27 require_once "Zend/OpenId.php";
30 * @see Zend_OpenId_Extension
32 require_once "Zend/OpenId/Extension.php";
35 * OpenID provider (server) implementation
38 * @package Zend_OpenId
39 * @subpackage Zend_OpenId_Provider
40 * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
41 * @license http://framework.zend.com/license/new-bsd New BSD License
43 class Zend_OpenId_Provider
47 * Reference to an implementation of storage object
49 * @var Zend_OpenId_Provider_Storage $_storage
54 * Reference to an implementation of user object
56 * @var Zend_OpenId_Provider_User $_user
61 * Time to live of association session in secconds
63 * @var integer $_sessionTtl
68 * URL to peform interactive user login
70 * @var string $_loginUrl
75 * URL to peform interactive validation of consumer by user
77 * @var string $_trustUrl
84 * @var string $_opEndpoint
89 * Constructs a Zend_OpenId_Provider object with given parameters.
91 * @param string $loginUrl is an URL that provides login screen for
92 * end-user (by default it is the same URL with additional GET variable
93 * openid.action=login)
94 * @param string $trustUrl is an URL that shows a question if end-user
95 * trust to given consumer (by default it is the same URL with additional
96 * GET variable openid.action=trust)
97 * @param Zend_OpenId_Provider_User $user is an object for communication
98 * with User-Agent and store information about logged-in user (it is a
99 * Zend_OpenId_Provider_User_Session object by default)
100 * @param Zend_OpenId_Provider_Storage $storage is an object for keeping
101 * persistent database (it is a Zend_OpenId_Provider_Storage_File object
103 * @param integer $sessionTtl is a default time to live for association
104 * session in seconds (1 hour by default). Consumer must reestablish
105 * association after that time.
107 public function __construct($loginUrl = null,
109 Zend_OpenId_Provider_User
$user = null,
110 Zend_OpenId_Provider_Storage
$storage = null,
113 if ($loginUrl === null) {
114 $loginUrl = Zend_OpenId
::selfUrl() . '?openid.action=login';
116 $loginUrl = Zend_OpenId
::absoluteUrl($loginUrl);
118 $this->_loginUrl
= $loginUrl;
119 if ($trustUrl === null) {
120 $trustUrl = Zend_OpenId
::selfUrl() . '?openid.action=trust';
122 $trustUrl = Zend_OpenId
::absoluteUrl($trustUrl);
124 $this->_trustUrl
= $trustUrl;
125 if ($user === null) {
126 require_once "Zend/OpenId/Provider/User/Session.php";
127 $this->_user
= new Zend_OpenId_Provider_User_Session();
129 $this->_user
= $user;
131 if ($storage === null) {
132 require_once "Zend/OpenId/Provider/Storage/File.php";
133 $this->_storage
= new Zend_OpenId_Provider_Storage_File();
135 $this->_storage
= $storage;
137 $this->_sessionTtl
= $sessionTtl;
141 * Sets the OP Endpoint URL
143 * @param string $url the OP Endpoint URL
146 public function setOpEndpoint($url)
148 $this->_opEndpoint
= $url;
152 * Registers a new user with given $id and $password
153 * Returns true in case of success and false if user with given $id already
156 * @param string $id user identity URL
157 * @param string $password encoded user password
160 public function register($id, $password)
162 if (!Zend_OpenId
::normalize($id) ||
empty($id)) {
165 return $this->_storage
->addUser($id, md5($id.$password));
169 * Returns true if user with given $id exists and false otherwise
171 * @param string $id user identity URL
174 public function hasUser($id) {
175 if (!Zend_OpenId
::normalize($id)) {
178 return $this->_storage
->hasUser($id);
182 * Performs login of user with given $id and $password
183 * Returns true in case of success and false otherwise
185 * @param string $id user identity URL
186 * @param string $password user password
189 public function login($id, $password)
191 if (!Zend_OpenId
::normalize($id)) {
194 if (!$this->_storage
->checkUser($id, md5($id.$password))) {
197 $this->_user
->setLoggedInUser($id);
202 * Performs logout. Clears information about logged in user.
206 public function logout()
208 $this->_user
->delLoggedInUser();
213 * Returns identity URL of current logged in user or false
217 public function getLoggedInUser() {
218 return $this->_user
->getLoggedInUser();
222 * Retrieve consumer's root URL from request query.
223 * Returns URL or false in case of failure
225 * @param array $params query arguments
228 public function getSiteRoot($params)
231 if (isset($params['openid_ns']) &&
232 $params['openid_ns'] == Zend_OpenId
::NS_2_0
) {
235 if ($version >= 2.0 && isset($params['openid_realm'])) {
236 $root = $params['openid_realm'];
237 } else if ($version < 2.0 && isset($params['openid_trust_root'])) {
238 $root = $params['openid_trust_root'];
239 } else if (isset($params['openid_return_to'])) {
240 $root = $params['openid_return_to'];
244 if (Zend_OpenId
::normalizeUrl($root) && !empty($root)) {
251 * Allows consumer with given root URL to authenticate current logged
252 * in user. Returns true on success and false on error.
254 * @param string $root root URL
255 * @param mixed $extensions extension object or array of extensions objects
258 public function allowSite($root, $extensions=null)
260 $id = $this->getLoggedInUser();
264 if ($extensions !== null) {
266 Zend_OpenId_Extension
::forAll($extensions, 'getTrustData', $data);
270 $this->_storage
->addSite($id, $root, $data);
275 * Prohibit consumer with given root URL to authenticate current logged
276 * in user. Returns true on success and false on error.
278 * @param string $root root URL
281 public function denySite($root)
283 $id = $this->getLoggedInUser();
287 $this->_storage
->addSite($id, $root, false);
292 * Delete consumer with given root URL from known sites of current logged
293 * in user. Next time this consumer will try to authenticate the user,
294 * Provider will ask user's confirmation.
295 * Returns true on success and false on error.
297 * @param string $root root URL
300 public function delSite($root)
302 $id = $this->getLoggedInUser();
306 $this->_storage
->addSite($id, $root, null);
311 * Returns list of known consumers for current logged in user or false
312 * if he is not logged in.
316 public function getTrustedSites()
318 $id = $this->getLoggedInUser();
322 return $this->_storage
->getTrustedSites($id);
326 * Handles HTTP request from consumer
328 * @param array $params GET or POST variables. If this parameter is omited
329 * or set to null, then $_GET or $_POST superglobal variable is used
330 * according to REQUEST_METHOD.
331 * @param mixed $extensions extension object or array of extensions objects
332 * @param Zend_Controller_Response_Abstract $response an optional response
333 * object to perform HTTP or HTML form redirection
336 public function handle($params=null, $extensions=null,
337 Zend_Controller_Response_Abstract
$response = null)
339 if ($params === null) {
340 if ($_SERVER["REQUEST_METHOD"] == "GET") {
342 } else if ($_SERVER["REQUEST_METHOD"] == "POST") {
349 if (isset($params['openid_ns']) &&
350 $params['openid_ns'] == Zend_OpenId
::NS_2_0
) {
353 if (isset($params['openid_mode'])) {
354 if ($params['openid_mode'] == 'associate') {
355 $response = $this->_associate($version, $params);
357 foreach ($response as $key => $val) {
358 $ret .= $key . ':' . $val . "\n";
361 } else if ($params['openid_mode'] == 'checkid_immediate') {
362 $ret = $this->_checkId($version, $params, 1, $extensions, $response);
363 if (is_bool($ret)) return $ret;
364 if (!empty($params['openid_return_to'])) {
365 Zend_OpenId
::redirect($params['openid_return_to'], $ret, $response);
368 } else if ($params['openid_mode'] == 'checkid_setup') {
369 $ret = $this->_checkId($version, $params, 0, $extensions, $response);
370 if (is_bool($ret)) return $ret;
371 if (!empty($params['openid_return_to'])) {
372 Zend_OpenId
::redirect($params['openid_return_to'], $ret, $response);
375 } else if ($params['openid_mode'] == 'check_authentication') {
376 $response = $this->_checkAuthentication($version, $params);
378 foreach ($response as $key => $val) {
379 $ret .= $key . ':' . $val . "\n";
388 * Generates a secret key for given hash function, returns RAW key or false
389 * if function is not supported
391 * @param string $func hash function (sha1 or sha256)
394 protected function _genSecret($func)
396 if ($func == 'sha1') {
397 $macLen = 20; /* 160 bit */
398 } else if ($func == 'sha256') {
399 $macLen = 32; /* 256 bit */
403 return Zend_OpenId
::randomBytes($macLen);
407 * Processes association request from OpenID consumerm generates secret
408 * shared key and send it back using Diffie-Hellman encruption.
409 * Returns array of variables to push back to consumer.
411 * @param float $version OpenID version
412 * @param array $params GET or POST request variables
415 protected function _associate($version, $params)
419 if ($version >= 2.0) {
420 $ret['ns'] = Zend_OpenId
::NS_2_0
;
423 if (isset($params['openid_assoc_type']) &&
424 $params['openid_assoc_type'] == 'HMAC-SHA1') {
426 } else if (isset($params['openid_assoc_type']) &&
427 $params['openid_assoc_type'] == 'HMAC-SHA256' &&
431 $ret['error'] = 'Wrong "openid.assoc_type"';
432 $ret['error-code'] = 'unsupported-type';
436 $ret['assoc_type'] = $params['openid_assoc_type'];
438 $secret = $this->_genSecret($macFunc);
440 if (empty($params['openid_session_type']) ||
441 $params['openid_session_type'] == 'no-encryption') {
442 $ret['mac_key'] = base64_encode($secret);
443 } else if (isset($params['openid_session_type']) &&
444 $params['openid_session_type'] == 'DH-SHA1') {
446 } else if (isset($params['openid_session_type']) &&
447 $params['openid_session_type'] == 'DH-SHA256' &&
451 $ret['error'] = 'Wrong "openid.session_type"';
452 $ret['error-code'] = 'unsupported-type';
456 if (isset($params['openid_session_type'])) {
457 $ret['session_type'] = $params['openid_session_type'];
460 if (isset($dhFunc)) {
461 if (empty($params['openid_dh_consumer_public'])) {
462 $ret['error'] = 'Wrong "openid.dh_consumer_public"';
465 if (empty($params['openid_dh_gen'])) {
466 $g = pack('H*', Zend_OpenId
::DH_G
);
468 $g = base64_decode($params['openid_dh_gen']);
470 if (empty($params['openid_dh_modulus'])) {
471 $p = pack('H*', Zend_OpenId
::DH_P
);
473 $p = base64_decode($params['openid_dh_modulus']);
476 $dh = Zend_OpenId
::createDhKey($p, $g);
477 $dh_details = Zend_OpenId
::getDhKeyDetails($dh);
479 $sec = Zend_OpenId
::computeDhSecret(
480 base64_decode($params['openid_dh_consumer_public']), $dh);
481 if ($sec === false) {
482 $ret['error'] = 'Wrong "openid.session_type"';
483 $ret['error-code'] = 'unsupported-type';
486 $sec = Zend_OpenId
::digest($dhFunc, $sec);
487 $ret['dh_server_public'] = base64_encode(
488 Zend_OpenId
::btwoc($dh_details['pub_key']));
489 $ret['enc_mac_key'] = base64_encode($secret ^
$sec);
493 $expiresIn = $this->_sessionTtl
;
495 $ret['assoc_handle'] = $handle;
496 $ret['expires_in'] = $expiresIn;
498 $this->_storage
->addAssociation($handle,
499 $macFunc, $secret, time() +
$expiresIn);
505 * Performs authentication (or authentication check).
507 * @param float $version OpenID version
508 * @param array $params GET or POST request variables
509 * @param bool $immediate enables or disables interaction with user
510 * @param mixed $extensions extension object or array of extensions objects
511 * @param Zend_Controller_Response_Abstract $response
514 protected function _checkId($version, $params, $immediate, $extensions=null,
515 Zend_Controller_Response_Abstract
$response = null)
519 if ($version >= 2.0) {
520 $ret['openid.ns'] = Zend_OpenId
::NS_2_0
;
522 $root = $this->getSiteRoot($params);
523 if ($root === false) {
527 if (isset($params['openid_identity']) &&
528 !$this->_storage
->hasUser($params['openid_identity'])) {
529 $ret['openid.mode'] = ($immediate && $version >= 2.0) ?
'setup_needed': 'cancel';
533 /* Check if user already logged in into the server */
534 if (!isset($params['openid_identity']) ||
535 $this->_user
->getLoggedInUser() !== $params['openid_identity']) {
537 foreach ($params as $key => $val) {
538 if (strpos($key, 'openid_ns_') === 0) {
539 $key = 'openid.ns.' . substr($key, strlen('openid_ns_'));
540 } else if (strpos($key, 'openid_sreg_') === 0) {
541 $key = 'openid.sreg.' . substr($key, strlen('openid_sreg_'));
542 } else if (strpos($key, 'openid_') === 0) {
543 $key = 'openid.' . substr($key, strlen('openid_'));
545 $params2[$key] = $val;
548 $params2['openid.mode'] = 'checkid_setup';
549 $ret['openid.mode'] = ($version >= 2.0) ?
'setup_needed': 'id_res';
550 $ret['openid.user_setup_url'] = $this->_loginUrl
551 . (strpos($this->_loginUrl
, '?') === false ?
'?' : '&')
552 . Zend_OpenId
::paramsToQuery($params2);
555 /* Redirect to Server Login Screen */
556 Zend_OpenId
::redirect($this->_loginUrl
, $params2, $response);
561 if (!Zend_OpenId_Extension
::forAll($extensions, 'parseRequest', $params)) {
562 $ret['openid.mode'] = ($immediate && $version >= 2.0) ?
'setup_needed': 'cancel';
566 /* Check if user trusts to the consumer */
568 $sites = $this->_storage
->getTrustedSites($params['openid_identity']);
569 if (isset($params['openid_return_to'])) {
570 $root = $params['openid_return_to'];
572 if (isset($sites[$root])) {
573 $trusted = $sites[$root];
575 foreach ($sites as $site => $t) {
576 if (strpos($root, $site) === 0) {
580 /* OpenID 2.0 (9.2) check for realm wild-card matching */
581 $n = strpos($site, '://*.');
584 . preg_quote(substr($site, 0, $n+
3), '/')
586 . preg_quote(substr($site, $n+
4), '/')
588 if (preg_match($regex, $root)) {
597 if (is_array($trusted)) {
598 if (!Zend_OpenId_Extension
::forAll($extensions, 'checkTrustData', $trusted)) {
603 if ($trusted === false) {
604 $ret['openid.mode'] = 'cancel';
606 } else if ($trusted === null) {
607 /* Redirect to Server Trust Screen */
609 foreach ($params as $key => $val) {
610 if (strpos($key, 'openid_ns_') === 0) {
611 $key = 'openid.ns.' . substr($key, strlen('openid_ns_'));
612 } else if (strpos($key, 'openid_sreg_') === 0) {
613 $key = 'openid.sreg.' . substr($key, strlen('openid_sreg_'));
614 } else if (strpos($key, 'openid_') === 0) {
615 $key = 'openid.' . substr($key, strlen('openid_'));
617 $params2[$key] = $val;
620 $params2['openid.mode'] = 'checkid_setup';
621 $ret['openid.mode'] = ($version >= 2.0) ?
'setup_needed': 'id_res';
622 $ret['openid.user_setup_url'] = $this->_trustUrl
623 . (strpos($this->_trustUrl
, '?') === false ?
'?' : '&')
624 . Zend_OpenId
::paramsToQuery($params2);
627 Zend_OpenId
::redirect($this->_trustUrl
, $params2, $response);
632 return $this->_respond($version, $ret, $params, $extensions);
636 * Perepares information to send back to consumer's authentication request,
637 * signs it using shared secret and send back through HTTP redirection
639 * @param array $params GET or POST request variables
640 * @param mixed $extensions extension object or array of extensions objects
641 * @param Zend_Controller_Response_Abstract $response an optional response
642 * object to perform HTTP or HTML form redirection
645 public function respondToConsumer($params, $extensions=null,
646 Zend_Controller_Response_Abstract
$response = null)
649 if (isset($params['openid_ns']) &&
650 $params['openid_ns'] == Zend_OpenId
::NS_2_0
) {
654 if ($version >= 2.0) {
655 $ret['openid.ns'] = Zend_OpenId
::NS_2_0
;
657 $ret = $this->_respond($version, $ret, $params, $extensions);
658 if (!empty($params['openid_return_to'])) {
659 Zend_OpenId
::redirect($params['openid_return_to'], $ret, $response);
665 * Perepares information to send back to consumer's authentication request
666 * and signs it using shared secret.
668 * @param float $version OpenID protcol version
669 * @param array $ret arguments to be send back to consumer
670 * @param array $params GET or POST request variables
671 * @param mixed $extensions extension object or array of extensions objects
674 protected function _respond($version, $ret, $params, $extensions=null)
676 if (empty($params['openid_assoc_handle']) ||
677 !$this->_storage
->getAssociation($params['openid_assoc_handle'],
678 $macFunc, $secret, $expires)) {
680 if (!empty($params['openid_assoc_handle'])) {
681 $ret['openid.invalidate_handle'] = $params['openid_assoc_handle'];
683 $macFunc = $version >= 2.0 ?
'sha256' : 'sha1';
684 $secret = $this->_genSecret($macFunc);
686 $expiresIn = $this->_sessionTtl
;
687 $this->_storage
->addAssociation($handle,
688 $macFunc, $secret, time() +
$expiresIn);
689 $ret['openid.assoc_handle'] = $handle;
691 $ret['openid.assoc_handle'] = $params['openid_assoc_handle'];
693 if (isset($params['openid_return_to'])) {
694 $ret['openid.return_to'] = $params['openid_return_to'];
696 if (isset($params['openid_claimed_id'])) {
697 $ret['openid.claimed_id'] = $params['openid_claimed_id'];
699 if (isset($params['openid_identity'])) {
700 $ret['openid.identity'] = $params['openid_identity'];
703 if ($version >= 2.0) {
704 if (!empty($this->_opEndpoint
)) {
705 $ret['openid.op_endpoint'] = $this->_opEndpoint
;
707 $ret['openid.op_endpoint'] = Zend_OpenId
::selfUrl();
710 $ret['openid.response_nonce'] = gmdate('Y-m-d\TH:i:s\Z') . uniqid();
711 $ret['openid.mode'] = 'id_res';
713 Zend_OpenId_Extension
::forAll($extensions, 'prepareResponse', $ret);
717 foreach ($ret as $key => $val) {
718 if (strpos($key, 'openid.') === 0) {
719 $key = substr($key, strlen('openid.'));
720 if (!empty($signed)) {
724 $data .= $key . ':' . $val . "\n";
727 $signed .= ',signed';
728 $data .= 'signed:' . $signed . "\n";
729 $ret['openid.signed'] = $signed;
731 $ret['openid.sig'] = base64_encode(
732 Zend_OpenId
::hashHmac($macFunc, $data, $secret));
738 * Performs authentication validation for dumb consumers
739 * Returns array of variables to push back to consumer.
740 * It MUST contain 'is_valid' variable with value 'true' or 'false'.
742 * @param float $version OpenID version
743 * @param array $params GET or POST request variables
746 protected function _checkAuthentication($version, $params)
749 if ($version >= 2.0) {
750 $ret['ns'] = Zend_OpenId
::NS_2_0
;
752 $ret['openid.mode'] = 'id_res';
754 if (empty($params['openid_assoc_handle']) ||
755 empty($params['openid_signed']) ||
756 empty($params['openid_sig']) ||
757 !$this->_storage
->getAssociation($params['openid_assoc_handle'],
758 $macFunc, $secret, $expires)) {
759 $ret['is_valid'] = 'false';
763 $signed = explode(',', $params['openid_signed']);
765 foreach ($signed as $key) {
767 if ($key == 'mode') {
770 $data .= $params['openid_' . strtr($key,'.','_')]."\n";
773 if (base64_decode($params['openid_sig']) ===
774 Zend_OpenId
::hashHmac($macFunc, $data, $secret)) {
775 $ret['is_valid'] = 'true';
777 $ret['is_valid'] = 'false';