1<?php 2 3/** 4 * OpenID server protocol and logic. 5 * 6 * Overview 7 * 8 * An OpenID server must perform three tasks: 9 * 10 * 1. Examine the incoming request to determine its nature and validity. 11 * 2. Make a decision about how to respond to this request. 12 * 3. Format the response according to the protocol. 13 * 14 * The first and last of these tasks may performed by the {@link 15 * Auth_OpenID_Server::decodeRequest()} and {@link 16 * Auth_OpenID_Server::encodeResponse} methods. Who gets to do the 17 * intermediate task -- deciding how to respond to the request -- will 18 * depend on what type of request it is. 19 * 20 * If it's a request to authenticate a user (a 'checkid_setup' or 21 * 'checkid_immediate' request), you need to decide if you will assert 22 * that this user may claim the identity in question. Exactly how you 23 * do that is a matter of application policy, but it generally 24 * involves making sure the user has an account with your system and 25 * is logged in, checking to see if that identity is hers to claim, 26 * and verifying with the user that she does consent to releasing that 27 * information to the party making the request. 28 * 29 * Examine the properties of the {@link Auth_OpenID_CheckIDRequest} 30 * object, and if and when you've come to a decision, form a response 31 * by calling {@link Auth_OpenID_CheckIDRequest::answer()}. 32 * 33 * Other types of requests relate to establishing associations between 34 * client and server and verifing the authenticity of previous 35 * communications. {@link Auth_OpenID_Server} contains all the logic 36 * and data necessary to respond to such requests; just pass it to 37 * {@link Auth_OpenID_Server::handleRequest()}. 38 * 39 * OpenID Extensions 40 * 41 * Do you want to provide other information for your users in addition 42 * to authentication? Version 1.2 of the OpenID protocol allows 43 * consumers to add extensions to their requests. For example, with 44 * sites using the Simple Registration 45 * Extension 46 * (http://openid.net/specs/openid-simple-registration-extension-1_0.html), 47 * a user can agree to have their nickname and e-mail address sent to 48 * a site when they sign up. 49 * 50 * Since extensions do not change the way OpenID authentication works, 51 * code to handle extension requests may be completely separate from 52 * the {@link Auth_OpenID_Request} class here. But you'll likely want 53 * data sent back by your extension to be signed. {@link 54 * Auth_OpenID_ServerResponse} provides methods with which you can add 55 * data to it which can be signed with the other data in the OpenID 56 * signature. 57 * 58 * For example: 59 * 60 * <pre> // when request is a checkid_* request 61 * $response = $request->answer(true); 62 * // this will a signed 'openid.sreg.timezone' parameter to the response 63 * response.addField('sreg', 'timezone', 'America/Los_Angeles')</pre> 64 * 65 * Stores 66 * 67 * The OpenID server needs to maintain state between requests in order 68 * to function. Its mechanism for doing this is called a store. The 69 * store interface is defined in Interface.php. Additionally, several 70 * concrete store implementations are provided, so that most sites 71 * won't need to implement a custom store. For a store backed by flat 72 * files on disk, see {@link Auth_OpenID_FileStore}. For stores based 73 * on MySQL, SQLite, or PostgreSQL, see the {@link 74 * Auth_OpenID_SQLStore} subclasses. 75 * 76 * Upgrading 77 * 78 * The keys by which a server looks up associations in its store have 79 * changed in version 1.2 of this library. If your store has entries 80 * created from version 1.0 code, you should empty it. 81 * 82 * PHP versions 4 and 5 83 * 84 * LICENSE: See the COPYING file included in this distribution. 85 * 86 * @package OpenID 87 * @author JanRain, Inc. <openid@janrain.com> 88 * @copyright 2005-2008 Janrain, Inc. 89 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache 90 */ 91 92/** 93 * Required imports 94 */ 95require_once "Auth/OpenID.php"; 96require_once "Auth/OpenID/Association.php"; 97require_once "Auth/OpenID/CryptUtil.php"; 98require_once "Auth/OpenID/BigMath.php"; 99require_once "Auth/OpenID/DiffieHellman.php"; 100require_once "Auth/OpenID/KVForm.php"; 101require_once "Auth/OpenID/TrustRoot.php"; 102require_once "Auth/OpenID/ServerRequest.php"; 103require_once "Auth/OpenID/Message.php"; 104require_once "Auth/OpenID/Nonce.php"; 105 106define('AUTH_OPENID_HTTP_OK', 200); 107define('AUTH_OPENID_HTTP_REDIRECT', 302); 108define('AUTH_OPENID_HTTP_ERROR', 400); 109 110/** 111 * @access private 112 */ 113global $_Auth_OpenID_Request_Modes; 114$_Auth_OpenID_Request_Modes = [ 115 'checkid_setup', 116 'checkid_immediate', 117]; 118 119/** 120 * @access private 121 */ 122define('Auth_OpenID_ENCODE_KVFORM', 'kfvorm'); 123 124/** 125 * @access private 126 */ 127define('Auth_OpenID_ENCODE_URL', 'URL/redirect'); 128 129/** 130 * @access private 131 */ 132define('Auth_OpenID_ENCODE_HTML_FORM', 'HTML form'); 133 134/** 135 * @access private 136 * @param object|string $obj 137 * @param string $cls 138 * @return bool 139 */ 140function Auth_OpenID_isError($obj, $cls = 'Auth_OpenID_ServerError') 141{ 142 return is_a($obj, $cls); 143} 144 145/** 146 * An error class which gets instantiated and returned whenever an 147 * OpenID protocol error occurs. Be prepared to use this in place of 148 * an ordinary server response. 149 * 150 * @package OpenID 151 */ 152class Auth_OpenID_ServerError { 153 154 /** @var Auth_OpenID_Message|null */ 155 private $message = null; 156 157 /** @var null|string */ 158 private $text; 159 160 /** @var null|string */ 161 private $contact; 162 163 /** @var null|string */ 164 private $reference; 165 166 /** 167 * Auth_OpenID_ServerError constructor. 168 * 169 * @param Auth_OpenID_Message $message 170 * @param string $text 171 * @param string $reference 172 * @param string $contact 173 */ 174 function __construct($message = null, $text = null, 175 $reference = null, $contact = null) 176 { 177 $this->message = $message; 178 $this->text = $text; 179 $this->contact = $contact; 180 $this->reference = $reference; 181 } 182 183 function getReturnTo() 184 { 185 if ($this->message && 186 $this->message->hasKey(Auth_OpenID_OPENID_NS, 'return_to')) { 187 return $this->message->getArg(Auth_OpenID_OPENID_NS, 188 'return_to'); 189 } else { 190 return null; 191 } 192 } 193 194 /** 195 * Returns the return_to URL for the request which caused this 196 * error. 197 */ 198 function hasReturnTo() 199 { 200 return $this->getReturnTo() !== null; 201 } 202 203 /** 204 * Encodes this error's response as a URL suitable for 205 * redirection. If the response has no return_to, another 206 * Auth_OpenID_ServerError is returned. 207 */ 208 function encodeToURL() 209 { 210 if (!$this->message) { 211 return null; 212 } 213 214 $msg = $this->toMessage(); 215 return $msg->toURL($this->getReturnTo()); 216 } 217 218 /** 219 * Encodes the response to key-value form. This is a 220 * machine-readable format used to respond to messages which came 221 * directly from the consumer and not through the user-agent. See 222 * the OpenID specification. 223 */ 224 function encodeToKVForm() 225 { 226 return Auth_OpenID_KVForm::fromArray( 227 [ 228 'mode' => 'error', 229 'error' => $this->toString(), 230 ] 231 ); 232 } 233 234 function toFormMarkup($form_tag_attrs=null) 235 { 236 $msg = $this->toMessage(); 237 return $msg->toFormMarkup($this->getReturnTo(), $form_tag_attrs); 238 } 239 240 function toHTML($form_tag_attrs=null) 241 { 242 return Auth_OpenID::autoSubmitHTML( 243 $this->toFormMarkup($form_tag_attrs)); 244 } 245 246 function toMessage() 247 { 248 // Generate a Message object for sending to the relying party, 249 // after encoding. 250 $namespace = $this->message->getOpenIDNamespace(); 251 $reply = new Auth_OpenID_Message($namespace); 252 $reply->setArg(Auth_OpenID_OPENID_NS, 'mode', 'error'); 253 $reply->setArg(Auth_OpenID_OPENID_NS, 'error', $this->toString()); 254 255 if ($this->contact !== null) { 256 $reply->setArg(Auth_OpenID_OPENID_NS, 'contact', $this->contact); 257 } 258 259 if ($this->reference !== null) { 260 $reply->setArg(Auth_OpenID_OPENID_NS, 'reference', 261 $this->reference); 262 } 263 264 return $reply; 265 } 266 267 /** 268 * Returns one of Auth_OpenID_ENCODE_URL, 269 * Auth_OpenID_ENCODE_KVFORM, or null, depending on the type of 270 * encoding expected for this error's payload. 271 */ 272 function whichEncoding() 273 { 274 global $_Auth_OpenID_Request_Modes; 275 276 if ($this->hasReturnTo()) { 277 if ($this->message->isOpenID2() && 278 (strlen($this->encodeToURL()) > 279 Auth_OpenID_OPENID1_URL_LIMIT)) { 280 return Auth_OpenID_ENCODE_HTML_FORM; 281 } else { 282 return Auth_OpenID_ENCODE_URL; 283 } 284 } 285 286 if (!$this->message) { 287 return null; 288 } 289 290 $mode = $this->message->getArg(Auth_OpenID_OPENID_NS, 291 'mode'); 292 293 if ($mode) { 294 if (!in_array($mode, $_Auth_OpenID_Request_Modes)) { 295 return Auth_OpenID_ENCODE_KVFORM; 296 } 297 } 298 return null; 299 } 300 301 /** 302 * Returns this error message. 303 */ 304 function toString() 305 { 306 if ($this->text) { 307 return $this->text; 308 } else { 309 return get_class($this) . " error"; 310 } 311 } 312} 313 314/** 315 * Error returned by the server code when a return_to is absent from a 316 * request. 317 * 318 * @package OpenID 319 */ 320class Auth_OpenID_NoReturnToError extends Auth_OpenID_ServerError { 321 function __construct($message = null, 322 $text = "No return_to URL available") 323 { 324 parent::__construct($message, $text); 325 } 326 327 function toString() 328 { 329 return "No return_to available"; 330 } 331} 332 333/** 334 * An error indicating that the return_to URL is malformed. 335 * 336 * @package OpenID 337 */ 338class Auth_OpenID_MalformedReturnURL extends Auth_OpenID_ServerError { 339 340 private $return_to; 341 342 function __construct($message, $return_to) 343 { 344 $this->return_to = $return_to; 345 parent::__construct($message, "malformed return_to URL"); 346 } 347} 348 349/** 350 * This error is returned when the trust_root value is malformed. 351 * 352 * @package OpenID 353 */ 354class Auth_OpenID_MalformedTrustRoot extends Auth_OpenID_ServerError { 355 function __construct($message = null, 356 $text = "Malformed trust root") 357 { 358 parent::__construct($message, $text); 359 } 360 361 function toString() 362 { 363 return "Malformed trust root"; 364 } 365} 366 367/** 368 * The base class for all server request classes. 369 * 370 * @package OpenID 371 */ 372class Auth_OpenID_Request { 373 374 public $mode = null; 375 376 /** @var Auth_OpenID_Message|null */ 377 public $message = null; 378 379 /** 380 * The OpenID namespace for this request. 381 * deprecated since version 2.0.2 382 */ 383 public $namespace = ''; 384 385 /** @var string */ 386 public $return_to = ''; 387} 388 389/** 390 * A request to verify the validity of a previous response. 391 * 392 * @package OpenID 393 */ 394class Auth_OpenID_CheckAuthRequest extends Auth_OpenID_Request { 395 public $mode = "check_authentication"; 396 public $invalidate_handle = null; 397 398 private $sig = ''; 399 private $assoc_handle = ''; 400 private $signed = ''; 401 402 403 function __construct($assoc_handle, $signed, $invalidate_handle = null) 404 { 405 $this->assoc_handle = $assoc_handle; 406 $this->signed = $signed; 407 if ($invalidate_handle !== null) { 408 $this->invalidate_handle = $invalidate_handle; 409 } 410 $this->namespace = Auth_OpenID_OPENID2_NS; 411 } 412 413 /** 414 * @param Auth_OpenID_Message $message 415 * @return Auth_OpenID_CheckAuthRequest|Auth_OpenID_ServerError 416 */ 417 static function fromMessage($message) 418 { 419 $required_keys = ['assoc_handle', 'sig', 'signed']; 420 421 foreach ($required_keys as $k) { 422 if (!$message->getArg(Auth_OpenID_OPENID_NS, $k)) { 423 return new Auth_OpenID_ServerError($message, 424 sprintf("%s request missing required parameter %s from query", "check_authentication", $k)); 425 } 426 } 427 428 $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS, 'assoc_handle'); 429 $sig = $message->getArg(Auth_OpenID_OPENID_NS, 'sig'); 430 431 $signed = $message; 432 if ($signed->hasKey(Auth_OpenID_OPENID_NS, 'mode')) { 433 $signed->setArg(Auth_OpenID_OPENID_NS, 'mode', 'id_res'); 434 } 435 436 $result = new Auth_OpenID_CheckAuthRequest($assoc_handle, $signed); 437 $result->message = $message; 438 $result->sig = $sig; 439 $result->invalidate_handle = $message->getArg(Auth_OpenID_OPENID_NS, 440 'invalidate_handle'); 441 return $result; 442 } 443 444 /** 445 * @param Auth_OpenID_Signatory $signatory 446 * @return Auth_OpenID_ServerResponse 447 */ 448 function answer($signatory) 449 { 450 $is_valid = $signatory->verify($this->assoc_handle, $this->signed); 451 452 // Now invalidate that assoc_handle so it this checkAuth 453 // message cannot be replayed. 454 $signatory->invalidate($this->assoc_handle, true); 455 $response = new Auth_OpenID_ServerResponse($this); 456 457 $response->fields->setArg(Auth_OpenID_OPENID_NS, 458 'is_valid', 459 ($is_valid ? "true" : "false")); 460 461 if ($this->invalidate_handle) { 462 $assoc = $signatory->getAssociation($this->invalidate_handle, 463 false); 464 if (!$assoc) { 465 $response->fields->setArg(Auth_OpenID_OPENID_NS, 466 'invalidate_handle', 467 $this->invalidate_handle); 468 } 469 } 470 return $response; 471 } 472} 473 474/** 475 * A class implementing plaintext server sessions. 476 * 477 * @package OpenID 478 */ 479class Auth_OpenID_PlainTextServerSession { 480 /** 481 * An object that knows how to handle association requests with no 482 * session type. 483 */ 484 public $session_type = 'no-encryption'; 485 public $needs_math = false; 486 public $allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']; 487 488 static function fromMessage() 489 { 490 return new Auth_OpenID_PlainTextServerSession(); 491 } 492 493 function answer($secret) 494 { 495 return ['mac_key' => base64_encode($secret)]; 496 } 497} 498 499/** 500 * A class implementing DH-SHA1 server sessions. 501 * 502 * @package OpenID 503 */ 504class Auth_OpenID_DiffieHellmanSHA1ServerSession { 505 /** 506 * An object that knows how to handle association requests with 507 * the Diffie-Hellman session type. 508 */ 509 510 public $session_type = 'DH-SHA1'; 511 public $needs_math = true; 512 public $allowed_assoc_types = ['HMAC-SHA1']; 513 public $hash_func = 'Auth_OpenID_SHA1'; 514 515 /** @var Auth_OpenID_DiffieHellman */ 516 private $dh; 517 518 private $consumer_pubkey = ''; 519 520 /** 521 * Auth_OpenID_DiffieHellmanSHA1ServerSession constructor. 522 * 523 * @param Auth_OpenID_DiffieHellman $dh 524 * @param string $consumer_pubkey 525 */ 526 function __construct($dh, $consumer_pubkey) 527 { 528 $this->dh = $dh; 529 $this->consumer_pubkey = $consumer_pubkey; 530 } 531 532 /** 533 * @param Auth_OpenID_Message $message 534 * @return array|Auth_OpenID_ServerError 535 */ 536 static function getDH($message) 537 { 538 $dh_modulus = $message->getArg(Auth_OpenID_OPENID_NS, 'dh_modulus'); 539 $dh_gen = $message->getArg(Auth_OpenID_OPENID_NS, 'dh_gen'); 540 541 if ((($dh_modulus === null) && ($dh_gen !== null)) || 542 (($dh_gen === null) && ($dh_modulus !== null))) { 543 544 if ($dh_modulus === null) { 545 $missing = 'modulus'; 546 } else { 547 $missing = 'generator'; 548 } 549 550 return new Auth_OpenID_ServerError($message, 551 'If non-default modulus or generator is '. 552 'supplied, both must be supplied. Missing '. 553 $missing); 554 } 555 556 $lib = Auth_OpenID_getMathLib(); 557 558 if ($dh_modulus || $dh_gen) { 559 $dh_modulus = $lib->base64ToLong($dh_modulus); 560 $dh_gen = $lib->base64ToLong($dh_gen); 561 if ($lib->cmp($dh_modulus, 0) == 0 || 562 $lib->cmp($dh_gen, 0) == 0) { 563 return new Auth_OpenID_ServerError( 564 $message, "Failed to parse dh_mod or dh_gen"); 565 } 566 $dh = new Auth_OpenID_DiffieHellman($dh_modulus, $dh_gen); 567 } else { 568 $dh = new Auth_OpenID_DiffieHellman(); 569 } 570 571 $consumer_pubkey = $message->getArg(Auth_OpenID_OPENID_NS, 572 'dh_consumer_public'); 573 if ($consumer_pubkey === null) { 574 return new Auth_OpenID_ServerError($message, 575 'Public key for DH-SHA1 session '. 576 'not found in query'); 577 } 578 579 $consumer_pubkey = 580 $lib->base64ToLong($consumer_pubkey); 581 582 if ($consumer_pubkey === false) { 583 return new Auth_OpenID_ServerError($message, 584 "dh_consumer_public is not base64"); 585 } 586 587 return [$dh, $consumer_pubkey]; 588 } 589 590 static function fromMessage($message) 591 { 592 $result = Auth_OpenID_DiffieHellmanSHA1ServerSession::getDH($message); 593 594 if (is_a($result, 'Auth_OpenID_ServerError')) { 595 return $result; 596 } else { 597 list($dh, $consumer_pubkey) = $result; 598 return new Auth_OpenID_DiffieHellmanSHA1ServerSession($dh, 599 $consumer_pubkey); 600 } 601 } 602 603 function answer($secret) 604 { 605 $lib = Auth_OpenID_getMathLib(); 606 $mac_key = $this->dh->xorSecret($this->consumer_pubkey, $secret, 607 $this->hash_func); 608 return [ 609 'dh_server_public' => 610 $lib->longToBase64($this->dh->public), 611 'enc_mac_key' => base64_encode($mac_key) 612 ]; 613 } 614} 615 616/** 617 * A class implementing DH-SHA256 server sessions. 618 * 619 * @package OpenID 620 */ 621class Auth_OpenID_DiffieHellmanSHA256ServerSession 622 extends Auth_OpenID_DiffieHellmanSHA1ServerSession { 623 624 public $session_type = 'DH-SHA256'; 625 public $hash_func = 'Auth_OpenID_SHA256'; 626 public $allowed_assoc_types = ['HMAC-SHA256']; 627 628 static function fromMessage($message) 629 { 630 $result = Auth_OpenID_DiffieHellmanSHA1ServerSession::getDH($message); 631 632 if (is_a($result, 'Auth_OpenID_ServerError')) { 633 return $result; 634 } else { 635 list($dh, $consumer_pubkey) = $result; 636 return new Auth_OpenID_DiffieHellmanSHA256ServerSession($dh, 637 $consumer_pubkey); 638 } 639 } 640} 641 642/** 643 * A request to associate with the server. 644 * 645 * @package OpenID 646 */ 647class Auth_OpenID_AssociateRequest extends Auth_OpenID_Request { 648 public $mode = "associate"; 649 650 /** @var Auth_OpenID_PlainTextServerSession */ 651 public $session; 652 653 public $assoc_type = ''; 654 655 static function getSessionClasses() 656 { 657 return [ 658 'no-encryption' => 'Auth_OpenID_PlainTextServerSession', 659 'DH-SHA1' => 'Auth_OpenID_DiffieHellmanSHA1ServerSession', 660 'DH-SHA256' => 'Auth_OpenID_DiffieHellmanSHA256ServerSession' 661 ]; 662 } 663 664 /** 665 * Auth_OpenID_AssociateRequest constructor. 666 * 667 * @param Auth_OpenID_PlainTextServerSession $session 668 * @param string $assoc_type 669 */ 670 function __construct($session, $assoc_type) 671 { 672 $this->session = $session; 673 $this->namespace = Auth_OpenID_OPENID2_NS; 674 $this->assoc_type = $assoc_type; 675 } 676 677 /** 678 * @param Auth_OpenID_Message $message 679 * @return Auth_OpenID_AssociateRequest|Auth_OpenID_ServerError|mixed 680 */ 681 static function fromMessage($message) 682 { 683 if ($message->isOpenID1()) { 684 $session_type = $message->getArg(Auth_OpenID_OPENID_NS, 685 'session_type'); 686 687 if ($session_type == 'no-encryption') { 688 // oidutil.log('Received OpenID 1 request with a no-encryption ' 689 // 'assocaition session type. Continuing anyway.') 690 } else if (!$session_type) { 691 $session_type = 'no-encryption'; 692 } 693 } else { 694 $session_type = $message->getArg(Auth_OpenID_OPENID_NS, 695 'session_type'); 696 if ($session_type === null) { 697 return new Auth_OpenID_ServerError($message, 698 "session_type missing from request"); 699 } 700 } 701 702 $session_class = Auth_OpenID::arrayGet( 703 Auth_OpenID_AssociateRequest::getSessionClasses(), 704 $session_type); 705 706 if ($session_class === null) { 707 return new Auth_OpenID_ServerError($message, 708 "Unknown session type " . 709 $session_type); 710 } 711 712 $session = call_user_func([$session_class, 'fromMessage'], 713 $message); 714 if (is_a($session, 'Auth_OpenID_ServerError')) { 715 return $session; 716 } 717 718 $assoc_type = $message->getArg(Auth_OpenID_OPENID_NS, 719 'assoc_type', 'HMAC-SHA1'); 720 721 if (!in_array($assoc_type, $session->allowed_assoc_types)) { 722 $fmt = "Session type %s does not support association type %s"; 723 return new Auth_OpenID_ServerError($message, 724 sprintf($fmt, $session_type, $assoc_type)); 725 } 726 727 $obj = new Auth_OpenID_AssociateRequest($session, $assoc_type); 728 $obj->message = $message; 729 $obj->namespace = $message->getOpenIDNamespace(); 730 return $obj; 731 } 732 733 /** 734 * @param Auth_OpenID_Association $assoc 735 * @return Auth_OpenID_ServerResponse 736 */ 737 function answer($assoc) 738 { 739 $response = new Auth_OpenID_ServerResponse($this); 740 $response->fields->updateArgs(Auth_OpenID_OPENID_NS, 741 [ 742 'expires_in' => sprintf('%d', $assoc->getExpiresIn()), 743 'assoc_type' => $this->assoc_type, 744 'assoc_handle' => $assoc->handle, 745 ] 746 ); 747 748 $response->fields->updateArgs(Auth_OpenID_OPENID_NS, 749 $this->session->answer($assoc->secret)); 750 751 if (! ($this->session->session_type == 'no-encryption' 752 && $this->message->isOpenID1())) { 753 $response->fields->setArg(Auth_OpenID_OPENID_NS, 754 'session_type', 755 $this->session->session_type); 756 } 757 758 return $response; 759 } 760 761 function answerUnsupported($text_message, 762 $preferred_association_type=null, 763 $preferred_session_type=null) 764 { 765 if ($this->message->isOpenID1()) { 766 return new Auth_OpenID_ServerError($this->message); 767 } 768 769 $response = new Auth_OpenID_ServerResponse($this); 770 $response->fields->setArg(Auth_OpenID_OPENID_NS, 771 'error_code', 'unsupported-type'); 772 $response->fields->setArg(Auth_OpenID_OPENID_NS, 773 'error', $text_message); 774 775 if ($preferred_association_type) { 776 $response->fields->setArg(Auth_OpenID_OPENID_NS, 777 'assoc_type', 778 $preferred_association_type); 779 } 780 781 if ($preferred_session_type) { 782 $response->fields->setArg(Auth_OpenID_OPENID_NS, 783 'session_type', 784 $preferred_session_type); 785 } 786 $response->code = AUTH_OPENID_HTTP_ERROR; 787 return $response; 788 } 789} 790 791/** 792 * A request to confirm the identity of a user. 793 * 794 * @package OpenID 795 */ 796class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { 797 /** 798 * Return-to verification callback. Default is 799 * Auth_OpenID_verifyReturnTo from TrustRoot.php. 800 */ 801 public $verifyReturnTo = 'Auth_OpenID_verifyReturnTo'; 802 803 /** 804 * The mode of this request. 805 */ 806 public $mode = "checkid_setup"; // or "checkid_immediate" 807 808 /** 809 * Whether this request is for immediate mode. 810 */ 811 public $immediate = false; 812 813 /** 814 * The trust_root value for this request. 815 */ 816 public $trust_root = null; 817 818 public $assoc_handle = ''; 819 820 /** @var Auth_OpenID_Server */ 821 private $server; 822 823 private $claimed_id = ''; 824 825 public $identity = ''; 826 827 /** 828 * @param Auth_OpenID_Message $message 829 * @param string $identity 830 * @param string $return_to 831 * @param string $trust_root 832 * @param bool $immediate 833 * @param string $assoc_handle 834 * @param Auth_OpenID_Server $server 835 * @return Auth_OpenID_CheckIDRequest|Auth_OpenID_MalformedReturnURL|Auth_OpenID_ServerError|Auth_OpenID_UntrustedReturnURL 836 */ 837 static function make($message, $identity, $return_to, $trust_root = null, 838 $immediate = false, $assoc_handle = null, $server = null) 839 { 840 if ($server === null) { 841 return new Auth_OpenID_ServerError($message, 842 "server must not be null"); 843 } 844 845 if ($return_to && 846 !Auth_OpenID_TrustRoot::_parse($return_to)) { 847 return new Auth_OpenID_MalformedReturnURL($message, $return_to); 848 } 849 850 $r = new Auth_OpenID_CheckIDRequest($identity, $return_to, 851 $trust_root, $immediate, 852 $assoc_handle, $server); 853 854 $r->namespace = $message->getOpenIDNamespace(); 855 $r->message = $message; 856 857 if (!$r->trustRootValid()) { 858 return new Auth_OpenID_UntrustedReturnURL($message, 859 $return_to, 860 $trust_root); 861 } else { 862 return $r; 863 } 864 } 865 866 /** 867 * Auth_OpenID_CheckIDRequest constructor. 868 * 869 * @param $identity 870 * @param $return_to 871 * @param string $trust_root 872 * @param bool $immediate 873 * @param string $assoc_handle 874 * @param Auth_OpenID_Server $server 875 * @param string $claimed_id 876 */ 877 function __construct($identity, $return_to, 878 $trust_root = null, $immediate = false, 879 $assoc_handle = null, $server = null, 880 $claimed_id = null) 881 { 882 $this->namespace = Auth_OpenID_OPENID2_NS; 883 $this->assoc_handle = $assoc_handle; 884 $this->identity = $identity; 885 if ($claimed_id === null) { 886 $this->claimed_id = $identity; 887 } else { 888 $this->claimed_id = $claimed_id; 889 } 890 $this->return_to = $return_to; 891 $this->trust_root = $trust_root; 892 $this->server = $server; 893 894 if ($immediate) { 895 $this->immediate = true; 896 $this->mode = "checkid_immediate"; 897 } else { 898 $this->immediate = false; 899 $this->mode = "checkid_setup"; 900 } 901 } 902 903 /** 904 * @param Auth_OpenID_CheckIDRequest $other 905 * @return bool 906 */ 907 function equals($other) 908 { 909 return ( 910 (is_a($other, 'Auth_OpenID_CheckIDRequest')) && 911 ($this->namespace == $other->namespace) && 912 ($this->assoc_handle == $other->assoc_handle) && 913 ($this->identity == $other->identity) && 914 ($this->claimed_id == $other->claimed_id) && 915 ($this->return_to == $other->return_to) && 916 ($this->trust_root == $other->trust_root)); 917 } 918 919 /* 920 * Does the relying party publish the return_to URL for this 921 * response under the realm? It is up to the provider to set a 922 * policy for what kinds of realms should be allowed. This 923 * return_to URL verification reduces vulnerability to data-theft 924 * attacks based on open proxies, corss-site-scripting, or open 925 * redirectors. 926 * 927 * This check should only be performed after making sure that the 928 * return_to URL matches the realm. 929 * 930 * @return true if the realm publishes a document with the 931 * return_to URL listed, false if not or if discovery fails 932 */ 933 function returnToVerified() 934 { 935 $fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); 936 return call_user_func_array($this->verifyReturnTo, 937 [$this->trust_root, $this->return_to, $fetcher]); 938 } 939 940 /** 941 * @param Auth_OpenID_Message $message 942 * @param Auth_OpenID_Server $server 943 * @return Auth_OpenID_CheckIDRequest|Auth_OpenID_MalformedReturnURL|Auth_OpenID_ServerError|Auth_OpenID_UntrustedReturnURL 944 */ 945 static function fromMessage($message, $server) 946 { 947 $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode'); 948 if ($mode == "checkid_immediate") { 949 $immediate = true; 950 } else { 951 $immediate = false; 952 } 953 954 $return_to = $message->getArg(Auth_OpenID_OPENID_NS, 955 'return_to'); 956 957 if (($message->isOpenID1()) && 958 (!$return_to)) { 959 $fmt = "Missing required field 'return_to' from checkid request"; 960 return new Auth_OpenID_ServerError($message, $fmt); 961 } 962 963 $identity = $message->getArg(Auth_OpenID_OPENID_NS, 964 'identity'); 965 $claimed_id = $message->getArg(Auth_OpenID_OPENID_NS, 'claimed_id'); 966 if ($message->isOpenID1()) { 967 if ($identity === null) { 968 $s = "OpenID 1 message did not contain openid.identity"; 969 return new Auth_OpenID_ServerError($message, $s); 970 } 971 } else { 972 if ($identity && !$claimed_id) { 973 $s = "OpenID 2.0 message contained openid.identity but not " . 974 "claimed_id"; 975 return new Auth_OpenID_ServerError($message, $s); 976 } else if ($claimed_id && !$identity) { 977 $s = "OpenID 2.0 message contained openid.claimed_id " . 978 "but not identity"; 979 return new Auth_OpenID_ServerError($message, $s); 980 } 981 } 982 983 // There's a case for making self.trust_root be a TrustRoot 984 // here. But if TrustRoot isn't currently part of the 985 // "public" API, I'm not sure it's worth doing. 986 if ($message->isOpenID1()) { 987 $trust_root_param = 'trust_root'; 988 } else { 989 $trust_root_param = 'realm'; 990 } 991 $trust_root = $message->getArg(Auth_OpenID_OPENID_NS, 992 $trust_root_param); 993 if (! $trust_root) { 994 $trust_root = $return_to; 995 } 996 997 if (! $message->isOpenID1() && 998 ($return_to === null) && 999 ($trust_root === null)) { 1000 return new Auth_OpenID_ServerError($message, 1001 "openid.realm required when openid.return_to absent"); 1002 } 1003 1004 $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS, 1005 'assoc_handle'); 1006 1007 $obj = Auth_OpenID_CheckIDRequest::make($message, 1008 $identity, 1009 $return_to, 1010 $trust_root, 1011 $immediate, 1012 $assoc_handle, 1013 $server); 1014 1015 if (is_a($obj, 'Auth_OpenID_ServerError')) { 1016 return $obj; 1017 } 1018 1019 $obj->claimed_id = $claimed_id; 1020 1021 return $obj; 1022 } 1023 1024 function idSelect() 1025 { 1026 // Is the identifier to be selected by the IDP? 1027 // So IDPs don't have to import the constant 1028 return $this->identity == Auth_OpenID_IDENTIFIER_SELECT; 1029 } 1030 1031 function trustRootValid() 1032 { 1033 if (!$this->trust_root) { 1034 return true; 1035 } 1036 1037 $tr = Auth_OpenID_TrustRoot::_parse($this->trust_root); 1038 if ($tr === false) { 1039 return new Auth_OpenID_MalformedTrustRoot($this->message, 1040 $this->trust_root); 1041 } 1042 1043 if ($this->return_to !== null) { 1044 return Auth_OpenID_TrustRoot::match($this->trust_root, 1045 $this->return_to); 1046 } else { 1047 return true; 1048 } 1049 } 1050 1051 /** 1052 * Respond to this request. Return either an 1053 * {@link Auth_OpenID_ServerResponse} or 1054 * {@link Auth_OpenID_ServerError}. 1055 * 1056 * @param bool $allow Allow this user to claim this identity, and 1057 * allow the consumer to have this information? 1058 * 1059 * @param string $server_url DEPRECATED. Passing $op_endpoint to 1060 * the {@link Auth_OpenID_Server} constructor makes this optional. 1061 * 1062 * When an OpenID 1.x immediate mode request does not succeed, it 1063 * gets back a URL where the request may be carried out in a 1064 * not-so-immediate fashion. Pass my URL in here (the fully 1065 * qualified address of this server's endpoint, i.e. 1066 * http://example.com/server), and I will use it as a base for the 1067 * URL for a new request. 1068 * 1069 * Optional for requests where {@link $immediate} is false or 1070 * $allow is true. 1071 * 1072 * @param string $identity The OP-local identifier to answer with. 1073 * Only for use when the relying party requested identifier 1074 * selection. 1075 * 1076 * @param string $claimed_id The claimed identifier to answer 1077 * with, for use with identifier selection in the case where the 1078 * claimed identifier and the OP-local identifier differ, 1079 * i.e. when the claimed_id uses delegation. 1080 * 1081 * If $identity is provided but this is not, $claimed_id will 1082 * default to the value of $identity. When answering requests 1083 * that did not ask for identifier selection, the response 1084 * $claimed_id will default to that of the request. 1085 * 1086 * This parameter is new in OpenID 2.0. 1087 * 1088 * @return mixed 1089 */ 1090 function answer($allow, $server_url = null, $identity = null, 1091 $claimed_id = null) 1092 { 1093 if (!$this->return_to) { 1094 return new Auth_OpenID_NoReturnToError(); 1095 } 1096 1097 if (!$server_url) { 1098 if ((!$this->message->isOpenID1()) && 1099 (!$this->server->op_endpoint)) { 1100 return new Auth_OpenID_ServerError(null, 1101 "server should be constructed with op_endpoint to " . 1102 "respond to OpenID 2.0 messages."); 1103 } 1104 1105 $server_url = $this->server->op_endpoint; 1106 } 1107 1108 if ($allow) { 1109 $mode = 'id_res'; 1110 } else if ($this->message->isOpenID1()) { 1111 if ($this->immediate) { 1112 $mode = 'id_res'; 1113 } else { 1114 $mode = 'cancel'; 1115 } 1116 } else { 1117 if ($this->immediate) { 1118 $mode = 'setup_needed'; 1119 } else { 1120 $mode = 'cancel'; 1121 } 1122 } 1123 1124 if (!$this->trustRootValid()) { 1125 return new Auth_OpenID_UntrustedReturnURL(null, 1126 $this->return_to, 1127 $this->trust_root); 1128 } 1129 1130 $response = new Auth_OpenID_ServerResponse($this); 1131 1132 if ($claimed_id && 1133 ($this->message->isOpenID1())) { 1134 return new Auth_OpenID_ServerError(null, 1135 "claimed_id is new in OpenID 2.0 and not " . 1136 "available for ".$this->namespace); 1137 } 1138 1139 if ($identity && !$claimed_id) { 1140 $claimed_id = $identity; 1141 } 1142 1143 if ($allow) { 1144 $response_claimed_id = ''; 1145 if ($this->identity == Auth_OpenID_IDENTIFIER_SELECT) { 1146 if (!$identity) { 1147 return new Auth_OpenID_ServerError(null, 1148 "This request uses IdP-driven identifier selection. " . 1149 "You must supply an identifier in the response."); 1150 } 1151 1152 $response_identity = $identity; 1153 $response_claimed_id = $claimed_id; 1154 1155 } else if ($this->identity) { 1156 if ($identity && 1157 ($this->identity != $identity)) { 1158 $fmt = "Request was for %s, cannot reply with identity %s"; 1159 return new Auth_OpenID_ServerError(null, 1160 sprintf($fmt, $this->identity, $identity)); 1161 } 1162 1163 $response_identity = $this->identity; 1164 $response_claimed_id = $this->claimed_id; 1165 } else { 1166 if ($identity) { 1167 return new Auth_OpenID_ServerError(null, 1168 "This request specified no identity and " . 1169 "you supplied ".$identity); 1170 } 1171 1172 $response_identity = null; 1173 } 1174 1175 if (($this->message->isOpenID1()) && 1176 ($response_identity === null)) { 1177 return new Auth_OpenID_ServerError(null, 1178 "Request was an OpenID 1 request, so response must " . 1179 "include an identifier."); 1180 } 1181 1182 $response->fields->updateArgs(Auth_OpenID_OPENID_NS, 1183 [ 1184 'mode' => $mode, 1185 'return_to' => $this->return_to, 1186 'response_nonce' => Auth_OpenID_mkNonce(), 1187 ] 1188 ); 1189 1190 if (!$this->message->isOpenID1()) { 1191 $response->fields->setArg(Auth_OpenID_OPENID_NS, 1192 'op_endpoint', $server_url); 1193 } 1194 1195 if ($response_identity !== null) { 1196 $response->fields->setArg( 1197 Auth_OpenID_OPENID_NS, 1198 'identity', 1199 $response_identity); 1200 if ($this->message->isOpenID2()) { 1201 $response->fields->setArg( 1202 Auth_OpenID_OPENID_NS, 1203 'claimed_id', 1204 $response_claimed_id); 1205 } 1206 } 1207 1208 } else { 1209 $response->fields->setArg(Auth_OpenID_OPENID_NS, 1210 'mode', $mode); 1211 1212 if ($this->immediate) { 1213 if (($this->message->isOpenID1()) && 1214 (!$server_url)) { 1215 return new Auth_OpenID_ServerError(null, 1216 'setup_url is required for $allow=false \ 1217 in OpenID 1.x immediate mode.'); 1218 } 1219 1220 $setup_request = new Auth_OpenID_CheckIDRequest( 1221 $this->identity, 1222 $this->return_to, 1223 $this->trust_root, 1224 false, 1225 $this->assoc_handle, 1226 $this->server, 1227 $this->claimed_id); 1228 $setup_request->message = $this->message; 1229 1230 $setup_url = $setup_request->encodeToURL($server_url); 1231 1232 if ($setup_url === null) { 1233 return new Auth_OpenID_NoReturnToError(); 1234 } 1235 1236 $response->fields->setArg(Auth_OpenID_OPENID_NS, 1237 'user_setup_url', 1238 $setup_url); 1239 } 1240 } 1241 1242 return $response; 1243 } 1244 1245 function encodeToURL($server_url) 1246 { 1247 if (!$this->return_to) { 1248 return new Auth_OpenID_NoReturnToError(); 1249 } 1250 1251 // Imported from the alternate reality where these classes are 1252 // used in both the client and server code, so Requests are 1253 // Encodable too. That's right, code imported from alternate 1254 // realities all for the love of you, id_res/user_setup_url. 1255 1256 $q = [ 1257 'mode' => $this->mode, 1258 'identity' => $this->identity, 1259 'claimed_id' => $this->claimed_id, 1260 'return_to' => $this->return_to, 1261 ]; 1262 1263 if ($this->trust_root) { 1264 if ($this->message->isOpenID1()) { 1265 $q['trust_root'] = $this->trust_root; 1266 } else { 1267 $q['realm'] = $this->trust_root; 1268 } 1269 } 1270 1271 if ($this->assoc_handle) { 1272 $q['assoc_handle'] = $this->assoc_handle; 1273 } 1274 1275 $response = new Auth_OpenID_Message( 1276 $this->message->getOpenIDNamespace()); 1277 $response->updateArgs(Auth_OpenID_OPENID_NS, $q); 1278 return $response->toURL($server_url); 1279 } 1280 1281 function getCancelURL() 1282 { 1283 if (!$this->return_to) { 1284 return new Auth_OpenID_NoReturnToError(); 1285 } 1286 1287 if ($this->immediate) { 1288 return new Auth_OpenID_ServerError(null, 1289 "Cancel is not an appropriate response to immediate mode requests."); 1290 } 1291 1292 $response = new Auth_OpenID_Message( 1293 $this->message->getOpenIDNamespace()); 1294 $response->setArg(Auth_OpenID_OPENID_NS, 'mode', 'cancel'); 1295 return $response->toURL($this->return_to); 1296 } 1297} 1298 1299/** 1300 * This class encapsulates the response to an OpenID server request. 1301 * 1302 * @package OpenID 1303 */ 1304class Auth_OpenID_ServerResponse { 1305 1306 public $code; 1307 1308 /** @var Auth_OpenID_Request */ 1309 public $request; 1310 1311 /** 1312 * Auth_OpenID_ServerResponse constructor. 1313 * 1314 * @param Auth_OpenID_Request $request 1315 */ 1316 function __construct($request) 1317 { 1318 $this->request = $request; 1319 $this->fields = new Auth_OpenID_Message($this->request->namespace); 1320 } 1321 1322 function whichEncoding() 1323 { 1324 global $_Auth_OpenID_Request_Modes; 1325 1326 if (in_array($this->request->mode, $_Auth_OpenID_Request_Modes)) { 1327 if ($this->fields->isOpenID2() && 1328 (strlen($this->encodeToURL()) > 1329 Auth_OpenID_OPENID1_URL_LIMIT)) { 1330 return Auth_OpenID_ENCODE_HTML_FORM; 1331 } else { 1332 return Auth_OpenID_ENCODE_URL; 1333 } 1334 } else { 1335 return Auth_OpenID_ENCODE_KVFORM; 1336 } 1337 } 1338 1339 /* 1340 * Returns the form markup for this response. 1341 * 1342 * @return str 1343 */ 1344 function toFormMarkup($form_tag_attrs=null) 1345 { 1346 return $this->fields->toFormMarkup($this->request->return_to, 1347 $form_tag_attrs); 1348 } 1349 1350 /* 1351 * Returns an HTML document containing the form markup for this 1352 * response that autosubmits with javascript. 1353 */ 1354 function toHTML() 1355 { 1356 return Auth_OpenID::autoSubmitHTML($this->toFormMarkup()); 1357 } 1358 1359 /* 1360 * Returns True if this response's encoding is ENCODE_HTML_FORM. 1361 * Convenience method for server authors. 1362 * 1363 * @return bool 1364 */ 1365 function renderAsForm() 1366 { 1367 return $this->whichEncoding() == Auth_OpenID_ENCODE_HTML_FORM; 1368 } 1369 1370 1371 function encodeToURL() 1372 { 1373 return $this->fields->toURL($this->request->return_to); 1374 } 1375 1376 /** 1377 * @param Auth_OpenID_Extension $extension_response 1378 */ 1379 function addExtension($extension_response) 1380 { 1381 $extension_response->toMessage($this->fields); 1382 } 1383 1384 function needsSigning() 1385 { 1386 return $this->fields->getArg(Auth_OpenID_OPENID_NS, 1387 'mode') == 'id_res'; 1388 } 1389 1390 function encodeToKVForm() 1391 { 1392 return $this->fields->toKVForm(); 1393 } 1394} 1395 1396/** 1397 * A web-capable response object which you can use to generate a 1398 * user-agent response. 1399 * 1400 * @package OpenID 1401 */ 1402class Auth_OpenID_WebResponse { 1403 public $code = AUTH_OPENID_HTTP_OK; 1404 public $body = ""; 1405 1406 function __construct($code = null, $headers = null, 1407 $body = null) 1408 { 1409 if ($code) { 1410 $this->code = $code; 1411 } 1412 1413 if ($headers !== null) { 1414 $this->headers = $headers; 1415 } else { 1416 $this->headers = []; 1417 } 1418 1419 if ($body !== null) { 1420 $this->body = $body; 1421 } 1422 } 1423} 1424 1425/** 1426 * Responsible for the signature of query data and the verification of 1427 * OpenID signature values. 1428 * 1429 * @package OpenID 1430 */ 1431class Auth_OpenID_Signatory { 1432 1433 // = 14 * 24 * 60 * 60; # 14 days, in seconds 1434 public $SECRET_LIFETIME = 1209600; 1435 1436 // keys have a bogus server URL in them because the filestore 1437 // really does expect that key to be a URL. This seems a little 1438 // silly for the server store, since I expect there to be only one 1439 // server URL. 1440 public $normal_key = 'http://localhost/|normal'; 1441 public $dumb_key = 'http://localhost/|dumb'; 1442 1443 /** @var Auth_OpenID_OpenIDStore */ 1444 private $store; 1445 1446 /** 1447 * Create a new signatory using a given store. 1448 * 1449 * @param Auth_OpenID_OpenIDStore $store 1450 */ 1451 function __construct($store) 1452 { 1453 // assert store is not None 1454 $this->store = $store; 1455 } 1456 1457 /** 1458 * Verify, using a given association handle, a signature with 1459 * signed key-value pairs from an HTTP request. 1460 * 1461 * @param string $assoc_handle 1462 * @param Auth_OpenID_Message $message 1463 * @return bool 1464 */ 1465 function verify($assoc_handle, $message) 1466 { 1467 $assoc = $this->getAssociation($assoc_handle, true); 1468 if (!$assoc) { 1469 // oidutil.log("failed to get assoc with handle %r to verify sig %r" 1470 // % (assoc_handle, sig)) 1471 return false; 1472 } 1473 1474 return $assoc->checkMessageSignature($message); 1475 } 1476 1477 /** 1478 * Given a response, sign the fields in the response's 'signed' 1479 * list, and insert the signature into the response. 1480 * 1481 * @param Auth_OpenID_ServerResponse $response 1482 * @return mixed 1483 */ 1484 function sign($response) 1485 { 1486 $signed_response = $response; 1487 /** @var Auth_OpenID_CheckIDRequest $request */ 1488 $request = $response->request; 1489 $assoc_handle = $request->assoc_handle; 1490 1491 if ($assoc_handle) { 1492 // normal mode 1493 $assoc = $this->getAssociation($assoc_handle, false, false); 1494 if (!$assoc || ($assoc->getExpiresIn() <= 0)) { 1495 // fall back to dumb mode 1496 $signed_response->fields->setArg(Auth_OpenID_OPENID_NS, 1497 'invalidate_handle', $assoc_handle); 1498 $assoc_type = ($assoc ? $assoc->assoc_type : 'HMAC-SHA1'); 1499 1500 if ($assoc && ($assoc->getExpiresIn() <= 0)) { 1501 $this->invalidate($assoc_handle, false); 1502 } 1503 1504 $assoc = $this->createAssociation(true, $assoc_type); 1505 } 1506 } else { 1507 // dumb mode. 1508 $assoc = $this->createAssociation(true); 1509 } 1510 1511 $signed_response->fields = $assoc->signMessage( 1512 $signed_response->fields); 1513 return $signed_response; 1514 } 1515 1516 /** 1517 * Make a new association. 1518 * 1519 * @param bool $dumb 1520 * @param string $assoc_type 1521 * @return Auth_OpenID_Association 1522 */ 1523 function createAssociation($dumb = true, $assoc_type = 'HMAC-SHA1') 1524 { 1525 $secret = Auth_OpenID_CryptUtil::getBytes( 1526 Auth_OpenID_getSecretSize($assoc_type)); 1527 1528 $uniq = base64_encode(Auth_OpenID_CryptUtil::getBytes(4)); 1529 $handle = sprintf('{%s}{%x}{%s}', $assoc_type, intval(time()), $uniq); 1530 1531 $assoc = Auth_OpenID_Association::fromExpiresIn( 1532 $this->SECRET_LIFETIME, $handle, $secret, $assoc_type); 1533 1534 if ($dumb) { 1535 $key = $this->dumb_key; 1536 } else { 1537 $key = $this->normal_key; 1538 } 1539 1540 $this->store->storeAssociation($key, $assoc); 1541 return $assoc; 1542 } 1543 1544 /** 1545 * Given an association handle, get the association from the 1546 * store, or return a ServerError or null if something goes wrong. 1547 * 1548 * @param string $assoc_handle 1549 * @param bool $dumb 1550 * @param bool $check_expiration 1551 * @return Auth_OpenID_Association|Auth_OpenID_ServerError|null 1552 */ 1553 function getAssociation($assoc_handle, $dumb, $check_expiration=true) 1554 { 1555 if ($assoc_handle === null) { 1556 return new Auth_OpenID_ServerError(null, 1557 "assoc_handle must not be null"); 1558 } 1559 1560 if ($dumb) { 1561 $key = $this->dumb_key; 1562 } else { 1563 $key = $this->normal_key; 1564 } 1565 1566 $assoc = $this->store->getAssociation($key, $assoc_handle); 1567 1568 if (($assoc !== null) && ($assoc->getExpiresIn() <= 0)) { 1569 if ($check_expiration) { 1570 $this->store->removeAssociation($key, $assoc_handle); 1571 $assoc = null; 1572 } 1573 } 1574 1575 return $assoc; 1576 } 1577 1578 /** 1579 * Invalidate a given association handle. 1580 * 1581 * @param string $assoc_handle 1582 * @param bool $dumb 1583 */ 1584 function invalidate($assoc_handle, $dumb) 1585 { 1586 if ($dumb) { 1587 $key = $this->dumb_key; 1588 } else { 1589 $key = $this->normal_key; 1590 } 1591 $this->store->removeAssociation($key, $assoc_handle); 1592 } 1593} 1594 1595/** 1596 * Encode an {@link Auth_OpenID_ServerResponse} to an 1597 * {@link Auth_OpenID_WebResponse}. 1598 * 1599 * @package OpenID 1600 */ 1601class Auth_OpenID_Encoder { 1602 1603 public $responseFactory = 'Auth_OpenID_WebResponse'; 1604 1605 /** 1606 * Encode an {@link Auth_OpenID_ServerResponse} and return an 1607 * {@link Auth_OpenID_WebResponse}. 1608 * 1609 * @param Auth_OpenID_ServerResponse $response 1610 * @return Auth_OpenID_EncodingError 1611 */ 1612 function encode($response) 1613 { 1614 $cls = $this->responseFactory; 1615 1616 $encode_as = $response->whichEncoding(); 1617 if ($encode_as == Auth_OpenID_ENCODE_KVFORM) { 1618 $wr = new $cls(null, null, $response->encodeToKVForm()); 1619 if (is_a($response, 'Auth_OpenID_ServerError')) { 1620 $wr->code = AUTH_OPENID_HTTP_ERROR; 1621 } 1622 } else if ($encode_as == Auth_OpenID_ENCODE_URL) { 1623 $location = $response->encodeToURL(); 1624 $wr = new $cls(AUTH_OPENID_HTTP_REDIRECT, 1625 ['location' => $location]); 1626 } else if ($encode_as == Auth_OpenID_ENCODE_HTML_FORM) { 1627 $wr = new $cls(AUTH_OPENID_HTTP_OK, [], 1628 $response->toHTML()); 1629 } else { 1630 return new Auth_OpenID_EncodingError($response); 1631 } 1632 /* Allow the response to carry a custom error code (ex: for Association errors) */ 1633 if(isset($response->code)) { 1634 $wr->code = $response->code; 1635 } 1636 return $wr; 1637 } 1638} 1639 1640/** 1641 * An encoder which also takes care of signing fields when required. 1642 * 1643 * @package OpenID 1644 */ 1645class Auth_OpenID_SigningEncoder extends Auth_OpenID_Encoder { 1646 1647 /** @var Auth_OpenID_Signatory */ 1648 private $signatory; 1649 1650 /** 1651 * Auth_OpenID_SigningEncoder constructor. 1652 * 1653 * @param Auth_OpenID_Signatory $signatory 1654 */ 1655 function __construct($signatory) 1656 { 1657 $this->signatory = $signatory; 1658 } 1659 1660 /** 1661 * Sign an {@link Auth_OpenID_ServerResponse} and return an 1662 * {@link Auth_OpenID_WebResponse}. 1663 * 1664 * @param Auth_OpenID_ServerResponse $response 1665 * @return Auth_OpenID_AlreadySigned|Auth_OpenID_EncodingError|Auth_OpenID_ServerError 1666 */ 1667 function encode($response) 1668 { 1669 // the isinstance is a bit of a kludge... it means there isn't 1670 // really an adapter to make the interfaces quite match. 1671 if (!is_a($response, 'Auth_OpenID_ServerError') && 1672 $response->needsSigning()) { 1673 1674 if (!$this->signatory) { 1675 return new Auth_OpenID_ServerError(null, 1676 "Must have a store to sign request"); 1677 } 1678 1679 if ($response->fields->hasKey(Auth_OpenID_OPENID_NS, 'sig')) { 1680 return new Auth_OpenID_AlreadySigned($response); 1681 } 1682 $response = $this->signatory->sign($response); 1683 } 1684 1685 return parent::encode($response); 1686 } 1687} 1688 1689/** 1690 * Decode an incoming query into an Auth_OpenID_Request. 1691 * 1692 * @package OpenID 1693 */ 1694class Auth_OpenID_Decoder { 1695 1696 /** @var Auth_OpenID_Server */ 1697 private $server; 1698 1699 private $handlers = [ 1700 'checkid_setup' => 'Auth_OpenID_CheckIDRequest', 1701 'checkid_immediate' => 'Auth_OpenID_CheckIDRequest', 1702 'check_authentication' => 'Auth_OpenID_CheckAuthRequest', 1703 'associate' => 'Auth_OpenID_AssociateRequest' 1704 ]; 1705 1706 /** 1707 * Auth_OpenID_Decoder constructor. 1708 * 1709 * @param Auth_OpenID_Server $server 1710 */ 1711 function __construct($server) 1712 { 1713 $this->server = $server; 1714 } 1715 1716 /** 1717 * Given an HTTP query in an array (key-value pairs), decode it 1718 * into an Auth_OpenID_Request object. 1719 * 1720 * @param array $query 1721 * @return Auth_OpenID_ServerError|mixed 1722 */ 1723 function decode($query) 1724 { 1725 if (!$query) { 1726 return null; 1727 } 1728 1729 $message = Auth_OpenID_Message::fromPostArgs($query); 1730 1731 if ($message === null) { 1732 /* 1733 * It's useful to have a Message attached to a 1734 * ProtocolError, so we override the bad ns value to build 1735 * a Message out of it. Kinda kludgy, since it's made of 1736 * lies, but the parts that aren't lies are more useful 1737 * than a 'None'. 1738 */ 1739 $old_ns = $query['openid.ns']; 1740 1741 $query['openid.ns'] = Auth_OpenID_OPENID2_NS; 1742 $message = Auth_OpenID_Message::fromPostArgs($query); 1743 return new Auth_OpenID_ServerError( 1744 $message, 1745 sprintf("Invalid OpenID namespace URI: %s", $old_ns)); 1746 } 1747 1748 $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode'); 1749 if (!$mode) { 1750 return new Auth_OpenID_ServerError($message, 1751 "No mode value in message"); 1752 } 1753 1754 if (Auth_OpenID::isFailure($mode)) { 1755 return new Auth_OpenID_ServerError($message, 1756 $mode->message); 1757 } 1758 1759 $handlerCls = Auth_OpenID::arrayGet($this->handlers, $mode, 1760 $this->defaultDecoder($message)); 1761 1762 if (!is_a($handlerCls, 'Auth_OpenID_ServerError')) { 1763 return call_user_func_array([$handlerCls, 'fromMessage'], 1764 [$message, $this->server]); 1765 } else { 1766 return $handlerCls; 1767 } 1768 } 1769 1770 /** 1771 * @param Auth_OpenID_Message $message 1772 * @return Auth_OpenID_ServerError 1773 */ 1774 function defaultDecoder($message) 1775 { 1776 $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode'); 1777 1778 if (Auth_OpenID::isFailure($mode)) { 1779 return new Auth_OpenID_ServerError($message, 1780 $mode->message); 1781 } 1782 1783 return new Auth_OpenID_ServerError($message, 1784 sprintf("Unrecognized OpenID mode %s", $mode)); 1785 } 1786} 1787 1788/** 1789 * An error that indicates an encoding problem occurred. 1790 * 1791 * @package OpenID 1792 */ 1793class Auth_OpenID_EncodingError { 1794 function __construct($response = null) 1795 { 1796 if ($response !== null) { 1797 $this->response = $response; 1798 } 1799 } 1800} 1801 1802/** 1803 * An error that indicates that a response was already signed. 1804 * 1805 * @package OpenID 1806 */ 1807class Auth_OpenID_AlreadySigned extends Auth_OpenID_EncodingError { 1808 1809} 1810 1811/** 1812 * An error that indicates that the given return_to is not under the 1813 * given trust_root. 1814 * 1815 * @package OpenID 1816 */ 1817class Auth_OpenID_UntrustedReturnURL extends Auth_OpenID_ServerError { 1818 1819 private $return_to = ''; 1820 private $trust_root = ''; 1821 1822 /** 1823 * Auth_OpenID_UntrustedReturnURL constructor. 1824 * 1825 * @param Auth_OpenID_Message|null $message 1826 * @param null|string $return_to 1827 * @param null|string $trust_root 1828 */ 1829 function __construct($message, $return_to, $trust_root) 1830 { 1831 parent::__construct($message, "Untrusted return_to URL"); 1832 $this->return_to = $return_to; 1833 $this->trust_root = $trust_root; 1834 } 1835 1836 function toString() 1837 { 1838 return sprintf("return_to %s not under trust_root %s", 1839 $this->return_to, $this->trust_root); 1840 } 1841} 1842 1843/** 1844 * I handle requests for an OpenID server. 1845 * 1846 * Some types of requests (those which are not checkid requests) may 1847 * be handed to my {@link handleRequest} method, and I will take care 1848 * of it and return a response. 1849 * 1850 * For your convenience, I also provide an interface to {@link 1851 * Auth_OpenID_Decoder::decode()} and {@link 1852 * Auth_OpenID_SigningEncoder::encode()} through my methods {@link 1853 * decodeRequest} and {@link encodeResponse}. 1854 * 1855 * All my state is encapsulated in an {@link Auth_OpenID_OpenIDStore}. 1856 * 1857 * Example: 1858 * 1859 * <pre> $oserver = new Auth_OpenID_Server(Auth_OpenID_FileStore($data_path), 1860 * "http://example.com/op"); 1861 * $request = $oserver->decodeRequest(); 1862 * if (in_array($request->mode, array('checkid_immediate', 1863 * 'checkid_setup'))) { 1864 * if ($app->isAuthorized($request->identity, $request->trust_root)) { 1865 * $response = $request->answer(true); 1866 * } else if ($request->immediate) { 1867 * $response = $request->answer(false); 1868 * } else { 1869 * $app->showDecidePage($request); 1870 * return; 1871 * } 1872 * } else { 1873 * $response = $oserver->handleRequest($request); 1874 * } 1875 * 1876 * $webresponse = $oserver->encode($response);</pre> 1877 * 1878 * @package OpenID 1879 */ 1880class Auth_OpenID_Server { 1881 1882 /** @var Auth_OpenID_OpenIDStore */ 1883 private $store; 1884 /** @var Auth_OpenID_Signatory */ 1885 private $signatory; 1886 /** @var Auth_OpenID_SigningEncoder */ 1887 private $encoder; 1888 /** @var Auth_OpenID_Decoder */ 1889 private $decoder; 1890 /** @var Auth_OpenID_SessionNegotiator */ 1891 private $negotiator; 1892 1893 /** @var Auth_OpenID_ServiceEndpoint|null */ 1894 public $op_endpoint; 1895 1896 /** 1897 * Auth_OpenID_Server constructor. 1898 * 1899 * @param Auth_OpenID_OpenIDStore $store 1900 * @param Auth_OpenID_ServiceEndpoint|null $op_endpoint 1901 */ 1902 function __construct($store, $op_endpoint=null) 1903 { 1904 $this->store = $store; 1905 $this->signatory = new Auth_OpenID_Signatory($this->store); 1906 $this->encoder = new Auth_OpenID_SigningEncoder($this->signatory); 1907 $this->decoder = new Auth_OpenID_Decoder($this); 1908 $this->op_endpoint = $op_endpoint; 1909 $this->negotiator = Auth_OpenID_getDefaultNegotiator(); 1910 } 1911 1912 /** 1913 * Handle a request. Given an {@link Auth_OpenID_Request} object, 1914 * call the appropriate {@link Auth_OpenID_Server} method to 1915 * process the request and generate a response. 1916 * 1917 * @param Auth_OpenID_Request $request An {@link Auth_OpenID_Request} 1918 * returned by {@link Auth_OpenID_Server::decodeRequest()}. 1919 * 1920 * @return Auth_OpenID_ServerResponse $response A response object 1921 * capable of generating a user-agent reply. 1922 */ 1923 function handleRequest($request) 1924 { 1925 if (method_exists($this, "openid_" . $request->mode)) { 1926 $handler = [$this, "openid_" . $request->mode]; 1927 return call_user_func_array($handler, [$request]); 1928 } 1929 return null; 1930 } 1931 1932 /** 1933 * The callback for 'check_authentication' messages. 1934 * 1935 * @param Auth_OpenID_CheckAuthRequest $request 1936 * @return mixed 1937 */ 1938 function openid_check_authentication($request) 1939 { 1940 return $request->answer($this->signatory); 1941 } 1942 1943 /** 1944 * The callback for 'associate' messages. 1945 * 1946 * @param Auth_OpenID_AssociateRequest $request 1947 * @return mixed 1948 */ 1949 function openid_associate($request) 1950 { 1951 $assoc_type = $request->assoc_type; 1952 $session_type = $request->session->session_type; 1953 if ($this->negotiator->isAllowed($assoc_type, $session_type)) { 1954 $assoc = $this->signatory->createAssociation(false, 1955 $assoc_type); 1956 return $request->answer($assoc); 1957 } else { 1958 $message = sprintf('Association type %s is not supported with '. 1959 'session type %s', $assoc_type, $session_type); 1960 list($preferred_assoc_type, $preferred_session_type) = 1961 $this->negotiator->getAllowedType(); 1962 return $request->answerUnsupported($message, 1963 $preferred_assoc_type, 1964 $preferred_session_type); 1965 } 1966 } 1967 1968 /** 1969 * Encodes as response in the appropriate format suitable for 1970 * sending to the user agent. 1971 * 1972 * @param Auth_OpenID_ServerResponse $response 1973 * @return Auth_OpenID_AlreadySigned|Auth_OpenID_EncodingError|Auth_OpenID_ServerError 1974 */ 1975 function encodeResponse($response) 1976 { 1977 return $this->encoder->encode($response); 1978 } 1979 1980 /** 1981 * Decodes a query args array into the appropriate 1982 * {@link Auth_OpenID_Request} object. 1983 * 1984 * @param array|null $query 1985 * @return Auth_OpenID_ServerError|mixed 1986 */ 1987 function decodeRequest($query=null) 1988 { 1989 if ($query === null) { 1990 $query = Auth_OpenID::getQuery(); 1991 } 1992 1993 return $this->decoder->decode($query); 1994 } 1995} 1996 1997 1998