4 * OpenID server protocol and logic.
8 * An OpenID server must perform three tasks:
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.
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.
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.
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()}.
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()}.
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
46 * (http://www.openidenabled.com/openid/simple-registration-extension/),
47 * a user can agree to have their nickname and e-mail address sent to
48 * a site when they sign up.
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
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>
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.
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.
82 * PHP versions 4 and 5
84 * LICENSE: See the COPYING file included in this distribution.
87 * @author JanRain, Inc. <openid@janrain.com>
88 * @copyright 2005-2008 Janrain, Inc.
89 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
95 require_once "Auth/OpenID.php";
96 require_once "Auth/OpenID/Association.php";
97 require_once "Auth/OpenID/CryptUtil.php";
98 require_once "Auth/OpenID/BigMath.php";
99 require_once "Auth/OpenID/DiffieHellman.php";
100 require_once "Auth/OpenID/KVForm.php";
101 require_once "Auth/OpenID/TrustRoot.php";
102 require_once "Auth/OpenID/ServerRequest.php";
103 require_once "Auth/OpenID/Message.php";
104 require_once "Auth/OpenID/Nonce.php";
106 define('AUTH_OPENID_HTTP_OK', 200);
107 define('AUTH_OPENID_HTTP_REDIRECT', 302);
108 define('AUTH_OPENID_HTTP_ERROR', 400);
113 global $_Auth_OpenID_Request_Modes;
114 $_Auth_OpenID_Request_Modes = array('checkid_setup',
115 'checkid_immediate');
120 define('Auth_OpenID_ENCODE_KVFORM', 'kfvorm');
125 define('Auth_OpenID_ENCODE_URL', 'URL/redirect');
130 define('Auth_OpenID_ENCODE_HTML_FORM', 'HTML form');
135 function Auth_OpenID_isError($obj, $cls = 'Auth_OpenID_ServerError')
137 return is_a($obj, $cls);
141 * An error class which gets instantiated and returned whenever an
142 * OpenID protocol error occurs. Be prepared to use this in place of
143 * an ordinary server response.
147 class Auth_OpenID_ServerError {
151 function Auth_OpenID_ServerError($message = null, $text = null,
152 $reference = null, $contact = null)
154 $this->message = $message;
156 $this->contact = $contact;
157 $this->reference = $reference;
160 function getReturnTo()
162 if ($this->message &&
163 $this->message->hasKey(Auth_OpenID_OPENID_NS, 'return_to')) {
164 return $this->message->getArg(Auth_OpenID_OPENID_NS,
172 * Returns the return_to URL for the request which caused this
175 function hasReturnTo()
177 return $this->getReturnTo() !== null;
181 * Encodes this error's response as a URL suitable for
182 * redirection. If the response has no return_to, another
183 * Auth_OpenID_ServerError is returned.
185 function encodeToURL()
187 if (!$this->message) {
191 $msg = $this->toMessage();
192 return $msg->toURL($this->getReturnTo());
196 * Encodes the response to key-value form. This is a
197 * machine-readable format used to respond to messages which came
198 * directly from the consumer and not through the user-agent. See
199 * the OpenID specification.
201 function encodeToKVForm()
203 return Auth_OpenID_KVForm::fromArray(
204 array('mode' => 'error',
205 'error' => $this->toString()));
208 function toFormMarkup($form_tag_attrs=null)
210 $msg = $this->toMessage();
211 return $msg->toFormMarkup($this->getReturnTo(), $form_tag_attrs);
214 function toHTML($form_tag_attrs=null)
216 return Auth_OpenID::autoSubmitHTML(
217 $this->toFormMarkup($form_tag_attrs));
222 // Generate a Message object for sending to the relying party,
224 $namespace = $this->message->getOpenIDNamespace();
225 $reply = new Auth_OpenID_Message($namespace);
226 $reply->setArg(Auth_OpenID_OPENID_NS, 'mode', 'error');
227 $reply->setArg(Auth_OpenID_OPENID_NS, 'error', $this->toString());
229 if ($this->contact !== null) {
230 $reply->setArg(Auth_OpenID_OPENID_NS, 'contact', $this->contact);
233 if ($this->reference !== null) {
234 $reply->setArg(Auth_OpenID_OPENID_NS, 'reference',
242 * Returns one of Auth_OpenID_ENCODE_URL,
243 * Auth_OpenID_ENCODE_KVFORM, or null, depending on the type of
244 * encoding expected for this error's payload.
246 function whichEncoding()
248 global $_Auth_OpenID_Request_Modes;
250 if ($this->hasReturnTo()) {
251 if ($this->message->isOpenID2() &&
252 (strlen($this->encodeToURL()) >
253 Auth_OpenID_OPENID1_URL_LIMIT)) {
254 return Auth_OpenID_ENCODE_HTML_FORM;
256 return Auth_OpenID_ENCODE_URL;
260 if (!$this->message) {
264 $mode = $this->message->getArg(Auth_OpenID_OPENID_NS,
268 if (!in_array($mode, $_Auth_OpenID_Request_Modes)) {
269 return Auth_OpenID_ENCODE_KVFORM;
276 * Returns this error message.
283 return get_class($this) . " error";
289 * Error returned by the server code when a return_to is absent from a
294 class Auth_OpenID_NoReturnToError extends Auth_OpenID_ServerError {
295 function Auth_OpenID_NoReturnToError($message = null,
296 $text = "No return_to URL available")
298 parent::Auth_OpenID_ServerError($message, $text);
303 return "No return_to available";
308 * An error indicating that the return_to URL is malformed.
312 class Auth_OpenID_MalformedReturnURL extends Auth_OpenID_ServerError {
313 function Auth_OpenID_MalformedReturnURL($message, $return_to)
315 $this->return_to = $return_to;
316 parent::Auth_OpenID_ServerError($message, "malformed return_to URL");
321 * This error is returned when the trust_root value is malformed.
325 class Auth_OpenID_MalformedTrustRoot extends Auth_OpenID_ServerError {
326 function Auth_OpenID_MalformedTrustRoot($message = null,
327 $text = "Malformed trust root")
329 parent::Auth_OpenID_ServerError($message, $text);
334 return "Malformed trust root";
339 * The base class for all server request classes.
343 class Auth_OpenID_Request {
348 * A request to verify the validity of a previous response.
352 class Auth_OpenID_CheckAuthRequest extends Auth_OpenID_Request {
353 var $mode = "check_authentication";
354 var $invalidate_handle = null;
356 function Auth_OpenID_CheckAuthRequest($assoc_handle, $signed,
357 $invalidate_handle = null)
359 $this->assoc_handle = $assoc_handle;
360 $this->signed = $signed;
361 if ($invalidate_handle !== null) {
362 $this->invalidate_handle = $invalidate_handle;
364 $this->namespace = Auth_OpenID_OPENID2_NS;
365 $this->message = null;
368 function fromMessage($message, $server=null)
370 $required_keys = array('assoc_handle', 'sig', 'signed');
372 foreach ($required_keys as $k) {
373 if (!$message->getArg(Auth_OpenID_OPENID_NS, $k)) {
374 return new Auth_OpenID_ServerError($message,
375 sprintf("%s request missing required parameter %s from \
376 query", "check_authentication", $k));
380 $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS, 'assoc_handle');
381 $sig = $message->getArg(Auth_OpenID_OPENID_NS, 'sig');
383 $signed_list = $message->getArg(Auth_OpenID_OPENID_NS, 'signed');
384 $signed_list = explode(",", $signed_list);
387 if ($signed->hasKey(Auth_OpenID_OPENID_NS, 'mode')) {
388 $signed->setArg(Auth_OpenID_OPENID_NS, 'mode', 'id_res');
391 $result = new Auth_OpenID_CheckAuthRequest($assoc_handle, $signed);
392 $result->message = $message;
394 $result->invalidate_handle = $message->getArg(Auth_OpenID_OPENID_NS,
395 'invalidate_handle');
399 function answer(&$signatory)
401 $is_valid = $signatory->verify($this->assoc_handle, $this->signed);
403 // Now invalidate that assoc_handle so it this checkAuth
404 // message cannot be replayed.
405 $signatory->invalidate($this->assoc_handle, true);
406 $response = new Auth_OpenID_ServerResponse($this);
408 $response->fields->setArg(Auth_OpenID_OPENID_NS,
410 ($is_valid ? "true" : "false"));
412 if ($this->invalidate_handle) {
413 $assoc = $signatory->getAssociation($this->invalidate_handle,
416 $response->fields->setArg(Auth_OpenID_OPENID_NS,
418 $this->invalidate_handle);
426 * A class implementing plaintext server sessions.
430 class Auth_OpenID_PlainTextServerSession {
432 * An object that knows how to handle association requests with no
435 var $session_type = 'no-encryption';
436 var $needs_math = false;
437 var $allowed_assoc_types = array('HMAC-SHA1', 'HMAC-SHA256');
439 function fromMessage($unused_request)
441 return new Auth_OpenID_PlainTextServerSession();
444 function answer($secret)
446 return array('mac_key' => base64_encode($secret));
451 * A class implementing DH-SHA1 server sessions.
455 class Auth_OpenID_DiffieHellmanSHA1ServerSession {
457 * An object that knows how to handle association requests with
458 * the Diffie-Hellman session type.
461 var $session_type = 'DH-SHA1';
462 var $needs_math = true;
463 var $allowed_assoc_types = array('HMAC-SHA1');
464 var $hash_func = 'Auth_OpenID_SHA1';
466 function Auth_OpenID_DiffieHellmanSHA1ServerSession($dh, $consumer_pubkey)
469 $this->consumer_pubkey = $consumer_pubkey;
472 function getDH($message)
474 $dh_modulus = $message->getArg(Auth_OpenID_OPENID_NS, 'dh_modulus');
475 $dh_gen = $message->getArg(Auth_OpenID_OPENID_NS, 'dh_gen');
477 if ((($dh_modulus === null) && ($dh_gen !== null)) ||
478 (($dh_gen === null) && ($dh_modulus !== null))) {
480 if ($dh_modulus === null) {
481 $missing = 'modulus';
483 $missing = 'generator';
486 return new Auth_OpenID_ServerError($message,
487 'If non-default modulus or generator is '.
488 'supplied, both must be supplied. Missing '.
492 $lib =& Auth_OpenID_getMathLib();
494 if ($dh_modulus || $dh_gen) {
495 $dh_modulus = $lib->base64ToLong($dh_modulus);
496 $dh_gen = $lib->base64ToLong($dh_gen);
497 if ($lib->cmp($dh_modulus, 0) == 0 ||
498 $lib->cmp($dh_gen, 0) == 0) {
499 return new Auth_OpenID_ServerError(
500 $message, "Failed to parse dh_mod or dh_gen");
502 $dh = new Auth_OpenID_DiffieHellman($dh_modulus, $dh_gen);
504 $dh = new Auth_OpenID_DiffieHellman();
507 $consumer_pubkey = $message->getArg(Auth_OpenID_OPENID_NS,
508 'dh_consumer_public');
509 if ($consumer_pubkey === null) {
510 return new Auth_OpenID_ServerError($message,
511 'Public key for DH-SHA1 session '.
512 'not found in query');
516 $lib->base64ToLong($consumer_pubkey);
518 if ($consumer_pubkey === false) {
519 return new Auth_OpenID_ServerError($message,
520 "dh_consumer_public is not base64");
523 return array($dh, $consumer_pubkey);
526 function fromMessage($message)
528 $result = Auth_OpenID_DiffieHellmanSHA1ServerSession::getDH($message);
530 if (is_a($result, 'Auth_OpenID_ServerError')) {
533 list($dh, $consumer_pubkey) = $result;
534 return new Auth_OpenID_DiffieHellmanSHA1ServerSession($dh,
539 function answer($secret)
541 $lib =& Auth_OpenID_getMathLib();
542 $mac_key = $this->dh->xorSecret($this->consumer_pubkey, $secret,
545 'dh_server_public' =>
546 $lib->longToBase64($this->dh->public),
547 'enc_mac_key' => base64_encode($mac_key));
552 * A class implementing DH-SHA256 server sessions.
556 class Auth_OpenID_DiffieHellmanSHA256ServerSession
557 extends Auth_OpenID_DiffieHellmanSHA1ServerSession {
559 var $session_type = 'DH-SHA256';
560 var $hash_func = 'Auth_OpenID_SHA256';
561 var $allowed_assoc_types = array('HMAC-SHA256');
563 function fromMessage($message)
565 $result = Auth_OpenID_DiffieHellmanSHA1ServerSession::getDH($message);
567 if (is_a($result, 'Auth_OpenID_ServerError')) {
570 list($dh, $consumer_pubkey) = $result;
571 return new Auth_OpenID_DiffieHellmanSHA256ServerSession($dh,
578 * A request to associate with the server.
582 class Auth_OpenID_AssociateRequest extends Auth_OpenID_Request {
583 var $mode = "associate";
585 function getSessionClasses()
588 'no-encryption' => 'Auth_OpenID_PlainTextServerSession',
589 'DH-SHA1' => 'Auth_OpenID_DiffieHellmanSHA1ServerSession',
590 'DH-SHA256' => 'Auth_OpenID_DiffieHellmanSHA256ServerSession');
593 function Auth_OpenID_AssociateRequest(&$session, $assoc_type)
595 $this->session =& $session;
596 $this->namespace = Auth_OpenID_OPENID2_NS;
597 $this->assoc_type = $assoc_type;
600 function fromMessage($message, $server=null)
602 if ($message->isOpenID1()) {
603 $session_type = $message->getArg(Auth_OpenID_OPENID_NS,
606 if ($session_type == 'no-encryption') {
607 // oidutil.log('Received OpenID 1 request with a no-encryption '
608 // 'assocaition session type. Continuing anyway.')
609 } else if (!$session_type) {
610 $session_type = 'no-encryption';
613 $session_type = $message->getArg(Auth_OpenID_OPENID_NS,
615 if ($session_type === null) {
616 return new Auth_OpenID_ServerError($message,
617 "session_type missing from request");
621 $session_class = Auth_OpenID::arrayGet(
622 Auth_OpenID_AssociateRequest::getSessionClasses(),
625 if ($session_class === null) {
626 return new Auth_OpenID_ServerError($message,
627 "Unknown session type " .
631 $session = call_user_func(array($session_class, 'fromMessage'),
633 if (is_a($session, 'Auth_OpenID_ServerError')) {
637 $assoc_type = $message->getArg(Auth_OpenID_OPENID_NS,
638 'assoc_type', 'HMAC-SHA1');
640 if (!in_array($assoc_type, $session->allowed_assoc_types)) {
641 $fmt = "Session type %s does not support association type %s";
642 return new Auth_OpenID_ServerError($message,
643 sprintf($fmt, $session_type, $assoc_type));
646 $obj = new Auth_OpenID_AssociateRequest($session, $assoc_type);
647 $obj->message = $message;
648 $obj->namespace = $message->getOpenIDNamespace();
652 function answer($assoc)
654 $response = new Auth_OpenID_ServerResponse($this);
655 $response->fields->updateArgs(Auth_OpenID_OPENID_NS,
657 'expires_in' => sprintf('%d', $assoc->getExpiresIn()),
658 'assoc_type' => $this->assoc_type,
659 'assoc_handle' => $assoc->handle));
661 $response->fields->updateArgs(Auth_OpenID_OPENID_NS,
662 $this->session->answer($assoc->secret));
664 if (! ($this->session->session_type == 'no-encryption'
665 && $this->message->isOpenID1())) {
666 $response->fields->setArg(Auth_OpenID_OPENID_NS,
668 $this->session->session_type);
674 function answerUnsupported($text_message,
675 $preferred_association_type=null,
676 $preferred_session_type=null)
678 if ($this->message->isOpenID1()) {
679 return new Auth_OpenID_ServerError($this->message);
682 $response = new Auth_OpenID_ServerResponse($this);
683 $response->fields->setArg(Auth_OpenID_OPENID_NS,
684 'error_code', 'unsupported-type');
685 $response->fields->setArg(Auth_OpenID_OPENID_NS,
686 'error', $text_message);
688 if ($preferred_association_type) {
689 $response->fields->setArg(Auth_OpenID_OPENID_NS,
691 $preferred_association_type);
694 if ($preferred_session_type) {
695 $response->fields->setArg(Auth_OpenID_OPENID_NS,
697 $preferred_session_type);
705 * A request to confirm the identity of a user.
709 class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request {
711 * Return-to verification callback. Default is
712 * Auth_OpenID_verifyReturnTo from TrustRoot.php.
714 var $verifyReturnTo = 'Auth_OpenID_verifyReturnTo';
717 * The mode of this request.
719 var $mode = "checkid_setup"; // or "checkid_immediate"
722 * Whether this request is for immediate mode.
724 var $immediate = false;
727 * The trust_root value for this request.
729 var $trust_root = null;
732 * The OpenID namespace for this request.
733 * deprecated since version 2.0.2
737 function make(&$message, $identity, $return_to, $trust_root = null,
738 $immediate = false, $assoc_handle = null, $server = null)
740 if ($server === null) {
741 return new Auth_OpenID_ServerError($message,
742 "server must not be null");
746 !Auth_OpenID_TrustRoot::_parse($return_to)) {
747 return new Auth_OpenID_MalformedReturnURL($message, $return_to);
750 $r = new Auth_OpenID_CheckIDRequest($identity, $return_to,
751 $trust_root, $immediate,
752 $assoc_handle, $server);
754 $r->namespace = $message->getOpenIDNamespace();
755 $r->message =& $message;
757 if (!$r->trustRootValid()) {
758 return new Auth_OpenID_UntrustedReturnURL($message,
766 function Auth_OpenID_CheckIDRequest($identity, $return_to,
767 $trust_root = null, $immediate = false,
768 $assoc_handle = null, $server = null,
771 $this->namespace = Auth_OpenID_OPENID2_NS;
772 $this->assoc_handle = $assoc_handle;
773 $this->identity = $identity;
774 if ($claimed_id === null) {
775 $this->claimed_id = $identity;
777 $this->claimed_id = $claimed_id;
779 $this->return_to = $return_to;
780 $this->trust_root = $trust_root;
781 $this->server =& $server;
784 $this->immediate = true;
785 $this->mode = "checkid_immediate";
787 $this->immediate = false;
788 $this->mode = "checkid_setup";
792 function equals($other)
795 (is_a($other, 'Auth_OpenID_CheckIDRequest')) &&
796 ($this->namespace == $other->namespace) &&
797 ($this->assoc_handle == $other->assoc_handle) &&
798 ($this->identity == $other->identity) &&
799 ($this->claimed_id == $other->claimed_id) &&
800 ($this->return_to == $other->return_to) &&
801 ($this->trust_root == $other->trust_root));
805 * Does the relying party publish the return_to URL for this
806 * response under the realm? It is up to the provider to set a
807 * policy for what kinds of realms should be allowed. This
808 * return_to URL verification reduces vulnerability to data-theft
809 * attacks based on open proxies, corss-site-scripting, or open
812 * This check should only be performed after making sure that the
813 * return_to URL matches the realm.
815 * @return true if the realm publishes a document with the
816 * return_to URL listed, false if not or if discovery fails
818 function returnToVerified()
820 return call_user_func_array($this->verifyReturnTo,
821 array($this->trust_root, $this->return_to));
824 function fromMessage(&$message, $server)
826 $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode');
829 if ($mode == "checkid_immediate") {
831 $mode = "checkid_immediate";
834 $mode = "checkid_setup";
837 $return_to = $message->getArg(Auth_OpenID_OPENID_NS,
840 if (($message->isOpenID1()) &&
842 $fmt = "Missing required field 'return_to' from checkid request";
843 return new Auth_OpenID_ServerError($message, $fmt);
846 $identity = $message->getArg(Auth_OpenID_OPENID_NS,
848 $claimed_id = $message->getArg(Auth_OpenID_OPENID_NS, 'claimed_id');
849 if ($message->isOpenID1()) {
850 if ($identity === null) {
851 $s = "OpenID 1 message did not contain openid.identity";
852 return new Auth_OpenID_ServerError($message, $s);
855 if ($identity && !$claimed_id) {
856 $s = "OpenID 2.0 message contained openid.identity but not " .
858 return new Auth_OpenID_ServerError($message, $s);
859 } else if ($claimed_id && !$identity) {
860 $s = "OpenID 2.0 message contained openid.claimed_id " .
862 return new Auth_OpenID_ServerError($message, $s);
866 // There's a case for making self.trust_root be a TrustRoot
867 // here. But if TrustRoot isn't currently part of the
868 // "public" API, I'm not sure it's worth doing.
869 if ($message->isOpenID1()) {
870 $trust_root_param = 'trust_root';
872 $trust_root_param = 'realm';
874 $trust_root = $message->getArg(Auth_OpenID_OPENID_NS,
877 $trust_root = $return_to;
880 if (! $message->isOpenID1() &&
881 ($return_to === null) &&
882 ($trust_root === null)) {
883 return new Auth_OpenID_ServerError($message,
884 "openid.realm required when openid.return_to absent");
887 $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS,
890 $obj = Auth_OpenID_CheckIDRequest::make($message,
898 if (is_a($obj, 'Auth_OpenID_ServerError')) {
902 $obj->claimed_id = $claimed_id;
909 // Is the identifier to be selected by the IDP?
910 // So IDPs don't have to import the constant
911 return $this->identity == Auth_OpenID_IDENTIFIER_SELECT;
914 function trustRootValid()
916 if (!$this->trust_root) {
920 $tr = Auth_OpenID_TrustRoot::_parse($this->trust_root);
922 return new Auth_OpenID_MalformedTrustRoot($this->message,
926 if ($this->return_to !== null) {
927 return Auth_OpenID_TrustRoot::match($this->trust_root,
935 * Respond to this request. Return either an
936 * {@link Auth_OpenID_ServerResponse} or
937 * {@link Auth_OpenID_ServerError}.
939 * @param bool $allow Allow this user to claim this identity, and
940 * allow the consumer to have this information?
942 * @param string $server_url DEPRECATED. Passing $op_endpoint to
943 * the {@link Auth_OpenID_Server} constructor makes this optional.
945 * When an OpenID 1.x immediate mode request does not succeed, it
946 * gets back a URL where the request may be carried out in a
947 * not-so-immediate fashion. Pass my URL in here (the fully
948 * qualified address of this server's endpoint, i.e.
949 * http://example.com/server), and I will use it as a base for the
950 * URL for a new request.
952 * Optional for requests where {@link $immediate} is false or
955 * @param string $identity The OP-local identifier to answer with.
956 * Only for use when the relying party requested identifier
959 * @param string $claimed_id The claimed identifier to answer
960 * with, for use with identifier selection in the case where the
961 * claimed identifier and the OP-local identifier differ,
962 * i.e. when the claimed_id uses delegation.
964 * If $identity is provided but this is not, $claimed_id will
965 * default to the value of $identity. When answering requests
966 * that did not ask for identifier selection, the response
967 * $claimed_id will default to that of the request.
969 * This parameter is new in OpenID 2.0.
973 function answer($allow, $server_url = null, $identity = null,
976 if (!$this->return_to) {
977 return new Auth_OpenID_NoReturnToError();
981 if ((!$this->message->isOpenID1()) &&
982 (!$this->server->op_endpoint)) {
983 return new Auth_OpenID_ServerError(null,
984 "server should be constructed with op_endpoint to " .
985 "respond to OpenID 2.0 messages.");
988 $server_url = $this->server->op_endpoint;
993 } else if ($this->message->isOpenID1()) {
994 if ($this->immediate) {
1000 if ($this->immediate) {
1001 $mode = 'setup_needed';
1007 if (!$this->trustRootValid()) {
1008 return new Auth_OpenID_UntrustedReturnURL(null,
1013 $response = new Auth_OpenID_ServerResponse($this);
1016 ($this->message->isOpenID1())) {
1017 return new Auth_OpenID_ServerError(null,
1018 "claimed_id is new in OpenID 2.0 and not " .
1019 "available for ".$this->namespace);
1022 if ($identity && !$claimed_id) {
1023 $claimed_id = $identity;
1028 if ($this->identity == Auth_OpenID_IDENTIFIER_SELECT) {
1030 return new Auth_OpenID_ServerError(null,
1031 "This request uses IdP-driven identifier selection. " .
1032 "You must supply an identifier in the response.");
1035 $response_identity = $identity;
1036 $response_claimed_id = $claimed_id;
1038 } else if ($this->identity) {
1040 ($this->identity != $identity)) {
1041 $fmt = "Request was for %s, cannot reply with identity %s";
1042 return new Auth_OpenID_ServerError(null,
1043 sprintf($fmt, $this->identity, $identity));
1046 $response_identity = $this->identity;
1047 $response_claimed_id = $this->claimed_id;
1050 return new Auth_OpenID_ServerError(null,
1051 "This request specified no identity and " .
1052 "you supplied ".$identity);
1055 $response_identity = null;
1058 if (($this->message->isOpenID1()) &&
1059 ($response_identity === null)) {
1060 return new Auth_OpenID_ServerError(null,
1061 "Request was an OpenID 1 request, so response must " .
1062 "include an identifier.");
1065 $response->fields->updateArgs(Auth_OpenID_OPENID_NS,
1066 array('mode' => $mode,
1067 'return_to' => $this->return_to,
1068 'response_nonce' => Auth_OpenID_mkNonce()));
1070 if (!$this->message->isOpenID1()) {
1071 $response->fields->setArg(Auth_OpenID_OPENID_NS,
1072 'op_endpoint', $server_url);
1075 if ($response_identity !== null) {
1076 $response->fields->setArg(
1077 Auth_OpenID_OPENID_NS,
1079 $response_identity);
1080 if ($this->message->isOpenID2()) {
1081 $response->fields->setArg(
1082 Auth_OpenID_OPENID_NS,
1084 $response_claimed_id);
1089 $response->fields->setArg(Auth_OpenID_OPENID_NS,
1092 if ($this->immediate) {
1093 if (($this->message->isOpenID1()) &&
1095 return new Auth_OpenID_ServerError(null,
1096 'setup_url is required for $allow=false \
1097 in OpenID 1.x immediate mode.');
1100 $setup_request =& new Auth_OpenID_CheckIDRequest(
1105 $this->assoc_handle,
1108 $setup_request->message = $this->message;
1110 $setup_url = $setup_request->encodeToURL($server_url);
1112 if ($setup_url === null) {
1113 return new Auth_OpenID_NoReturnToError();
1116 $response->fields->setArg(Auth_OpenID_OPENID_NS,
1125 function encodeToURL($server_url)
1127 if (!$this->return_to) {
1128 return new Auth_OpenID_NoReturnToError();
1131 // Imported from the alternate reality where these classes are
1132 // used in both the client and server code, so Requests are
1133 // Encodable too. That's right, code imported from alternate
1134 // realities all for the love of you, id_res/user_setup_url.
1136 $q = array('mode' => $this->mode,
1137 'identity' => $this->identity,
1138 'claimed_id' => $this->claimed_id,
1139 'return_to' => $this->return_to);
1141 if ($this->trust_root) {
1142 if ($this->message->isOpenID1()) {
1143 $q['trust_root'] = $this->trust_root;
1145 $q['realm'] = $this->trust_root;
1149 if ($this->assoc_handle) {
1150 $q['assoc_handle'] = $this->assoc_handle;
1153 $response = new Auth_OpenID_Message(
1154 $this->message->getOpenIDNamespace());
1155 $response->updateArgs(Auth_OpenID_OPENID_NS, $q);
1156 return $response->toURL($server_url);
1159 function getCancelURL()
1161 if (!$this->return_to) {
1162 return new Auth_OpenID_NoReturnToError();
1165 if ($this->immediate) {
1166 return new Auth_OpenID_ServerError(null,
1167 "Cancel is not an appropriate \
1168 response to immediate mode \
1172 $response = new Auth_OpenID_Message(
1173 $this->message->getOpenIDNamespace());
1174 $response->setArg(Auth_OpenID_OPENID_NS, 'mode', 'cancel');
1175 return $response->toURL($this->return_to);
1180 * This class encapsulates the response to an OpenID server request.
1184 class Auth_OpenID_ServerResponse {
1186 function Auth_OpenID_ServerResponse(&$request)
1188 $this->request =& $request;
1189 $this->fields = new Auth_OpenID_Message($this->request->namespace);
1192 function whichEncoding()
1194 global $_Auth_OpenID_Request_Modes;
1196 if (in_array($this->request->mode, $_Auth_OpenID_Request_Modes)) {
1197 if ($this->fields->isOpenID2() &&
1198 (strlen($this->encodeToURL()) >
1199 Auth_OpenID_OPENID1_URL_LIMIT)) {
1200 return Auth_OpenID_ENCODE_HTML_FORM;
1202 return Auth_OpenID_ENCODE_URL;
1205 return Auth_OpenID_ENCODE_KVFORM;
1210 * Returns the form markup for this response.
1214 function toFormMarkup($form_tag_attrs=null)
1216 return $this->fields->toFormMarkup($this->request->return_to,
1221 * Returns an HTML document containing the form markup for this
1222 * response that autosubmits with javascript.
1226 return Auth_OpenID::autoSubmitHTML($this->toFormMarkup());
1230 * Returns True if this response's encoding is ENCODE_HTML_FORM.
1231 * Convenience method for server authors.
1235 function renderAsForm()
1237 return $this->whichEncoding() == Auth_OpenID_ENCODE_HTML_FORM;
1241 function encodeToURL()
1243 return $this->fields->toURL($this->request->return_to);
1246 function addExtension($extension_response)
1248 $extension_response->toMessage($this->fields);
1251 function needsSigning()
1253 return $this->fields->getArg(Auth_OpenID_OPENID_NS,
1254 'mode') == 'id_res';
1257 function encodeToKVForm()
1259 return $this->fields->toKVForm();
1264 * A web-capable response object which you can use to generate a
1265 * user-agent response.
1269 class Auth_OpenID_WebResponse {
1270 var $code = AUTH_OPENID_HTTP_OK;
1273 function Auth_OpenID_WebResponse($code = null, $headers = null,
1277 $this->code = $code;
1280 if ($headers !== null) {
1281 $this->headers = $headers;
1283 $this->headers = array();
1286 if ($body !== null) {
1287 $this->body = $body;
1293 * Responsible for the signature of query data and the verification of
1294 * OpenID signature values.
1298 class Auth_OpenID_Signatory {
1300 // = 14 * 24 * 60 * 60; # 14 days, in seconds
1301 var $SECRET_LIFETIME = 1209600;
1303 // keys have a bogus server URL in them because the filestore
1304 // really does expect that key to be a URL. This seems a little
1305 // silly for the server store, since I expect there to be only one
1307 var $normal_key = 'http://localhost/|normal';
1308 var $dumb_key = 'http://localhost/|dumb';
1311 * Create a new signatory using a given store.
1313 function Auth_OpenID_Signatory(&$store)
1315 // assert store is not None
1316 $this->store =& $store;
1320 * Verify, using a given association handle, a signature with
1321 * signed key-value pairs from an HTTP request.
1323 function verify($assoc_handle, $message)
1325 $assoc = $this->getAssociation($assoc_handle, true);
1327 // oidutil.log("failed to get assoc with handle %r to verify sig %r"
1328 // % (assoc_handle, sig))
1332 return $assoc->checkMessageSignature($message);
1336 * Given a response, sign the fields in the response's 'signed'
1337 * list, and insert the signature into the response.
1339 function sign($response)
1341 $signed_response = $response;
1342 $assoc_handle = $response->request->assoc_handle;
1344 if ($assoc_handle) {
1346 $assoc = $this->getAssociation($assoc_handle, false, false);
1347 if (!$assoc || ($assoc->getExpiresIn() <= 0)) {
1348 // fall back to dumb mode
1349 $signed_response->fields->setArg(Auth_OpenID_OPENID_NS,
1350 'invalidate_handle', $assoc_handle);
1351 $assoc_type = ($assoc ? $assoc->assoc_type : 'HMAC-SHA1');
1353 if ($assoc && ($assoc->getExpiresIn() <= 0)) {
1354 $this->invalidate($assoc_handle, false);
1357 $assoc = $this->createAssociation(true, $assoc_type);
1361 $assoc = $this->createAssociation(true);
1364 $signed_response->fields = $assoc->signMessage(
1365 $signed_response->fields);
1366 return $signed_response;
1370 * Make a new association.
1372 function createAssociation($dumb = true, $assoc_type = 'HMAC-SHA1')
1374 $secret = Auth_OpenID_CryptUtil::getBytes(
1375 Auth_OpenID_getSecretSize($assoc_type));
1377 $uniq = base64_encode(Auth_OpenID_CryptUtil::getBytes(4));
1378 $handle = sprintf('{%s}{%x}{%s}', $assoc_type, intval(time()), $uniq);
1380 $assoc = Auth_OpenID_Association::fromExpiresIn(
1381 $this->SECRET_LIFETIME, $handle, $secret, $assoc_type);
1384 $key = $this->dumb_key;
1386 $key = $this->normal_key;
1389 $this->store->storeAssociation($key, $assoc);
1394 * Given an association handle, get the association from the
1395 * store, or return a ServerError or null if something goes wrong.
1397 function getAssociation($assoc_handle, $dumb, $check_expiration=true)
1399 if ($assoc_handle === null) {
1400 return new Auth_OpenID_ServerError(null,
1401 "assoc_handle must not be null");
1405 $key = $this->dumb_key;
1407 $key = $this->normal_key;
1410 $assoc = $this->store->getAssociation($key, $assoc_handle);
1412 if (($assoc !== null) && ($assoc->getExpiresIn() <= 0)) {
1413 if ($check_expiration) {
1414 $this->store->removeAssociation($key, $assoc_handle);
1423 * Invalidate a given association handle.
1425 function invalidate($assoc_handle, $dumb)
1428 $key = $this->dumb_key;
1430 $key = $this->normal_key;
1432 $this->store->removeAssociation($key, $assoc_handle);
1437 * Encode an {@link Auth_OpenID_ServerResponse} to an
1438 * {@link Auth_OpenID_WebResponse}.
1442 class Auth_OpenID_Encoder {
1444 var $responseFactory = 'Auth_OpenID_WebResponse';
1447 * Encode an {@link Auth_OpenID_ServerResponse} and return an
1448 * {@link Auth_OpenID_WebResponse}.
1450 function encode(&$response)
1452 $cls = $this->responseFactory;
1454 $encode_as = $response->whichEncoding();
1455 if ($encode_as == Auth_OpenID_ENCODE_KVFORM) {
1456 $wr = new $cls(null, null, $response->encodeToKVForm());
1457 if (is_a($response, 'Auth_OpenID_ServerError')) {
1458 $wr->code = AUTH_OPENID_HTTP_ERROR;
1460 } else if ($encode_as == Auth_OpenID_ENCODE_URL) {
1461 $location = $response->encodeToURL();
1462 $wr = new $cls(AUTH_OPENID_HTTP_REDIRECT,
1463 array('location' => $location));
1464 } else if ($encode_as == Auth_OpenID_ENCODE_HTML_FORM) {
1465 $wr = new $cls(AUTH_OPENID_HTTP_OK, array(),
1466 $response->toFormMarkup());
1468 return new Auth_OpenID_EncodingError($response);
1475 * An encoder which also takes care of signing fields when required.
1479 class Auth_OpenID_SigningEncoder extends Auth_OpenID_Encoder {
1481 function Auth_OpenID_SigningEncoder(&$signatory)
1483 $this->signatory =& $signatory;
1487 * Sign an {@link Auth_OpenID_ServerResponse} and return an
1488 * {@link Auth_OpenID_WebResponse}.
1490 function encode(&$response)
1492 // the isinstance is a bit of a kludge... it means there isn't
1493 // really an adapter to make the interfaces quite match.
1494 if (!is_a($response, 'Auth_OpenID_ServerError') &&
1495 $response->needsSigning()) {
1497 if (!$this->signatory) {
1498 return new Auth_OpenID_ServerError(null,
1499 "Must have a store to sign request");
1502 if ($response->fields->hasKey(Auth_OpenID_OPENID_NS, 'sig')) {
1503 return new Auth_OpenID_AlreadySigned($response);
1505 $response = $this->signatory->sign($response);
1508 return parent::encode($response);
1513 * Decode an incoming query into an Auth_OpenID_Request.
1517 class Auth_OpenID_Decoder {
1519 function Auth_OpenID_Decoder(&$server)
1521 $this->server =& $server;
1523 $this->handlers = array(
1524 'checkid_setup' => 'Auth_OpenID_CheckIDRequest',
1525 'checkid_immediate' => 'Auth_OpenID_CheckIDRequest',
1526 'check_authentication' => 'Auth_OpenID_CheckAuthRequest',
1527 'associate' => 'Auth_OpenID_AssociateRequest'
1532 * Given an HTTP query in an array (key-value pairs), decode it
1533 * into an Auth_OpenID_Request object.
1535 function decode($query)
1541 $message = Auth_OpenID_Message::fromPostArgs($query);
1543 if ($message === null) {
1545 * It's useful to have a Message attached to a
1546 * ProtocolError, so we override the bad ns value to build
1547 * a Message out of it. Kinda kludgy, since it's made of
1548 * lies, but the parts that aren't lies are more useful
1551 $old_ns = $query['openid.ns'];
1553 $query['openid.ns'] = Auth_OpenID_OPENID2_NS;
1554 $message = Auth_OpenID_Message::fromPostArgs($query);
1555 return new Auth_OpenID_ServerError(
1557 sprintf("Invalid OpenID namespace URI: %s", $old_ns));
1560 $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode');
1562 return new Auth_OpenID_ServerError($message,
1563 "No mode value in message");
1566 if (Auth_OpenID::isFailure($mode)) {
1567 return new Auth_OpenID_ServerError($message,
1571 $handlerCls = Auth_OpenID::arrayGet($this->handlers, $mode,
1572 $this->defaultDecoder($message));
1574 if (!is_a($handlerCls, 'Auth_OpenID_ServerError')) {
1575 return call_user_func_array(array($handlerCls, 'fromMessage'),
1576 array($message, $this->server));
1582 function defaultDecoder($message)
1584 $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode');
1586 if (Auth_OpenID::isFailure($mode)) {
1587 return new Auth_OpenID_ServerError($message,
1591 return new Auth_OpenID_ServerError($message,
1592 sprintf("Unrecognized OpenID mode %s", $mode));
1597 * An error that indicates an encoding problem occurred.
1601 class Auth_OpenID_EncodingError {
1602 function Auth_OpenID_EncodingError(&$response)
1604 $this->response =& $response;
1609 * An error that indicates that a response was already signed.
1613 class Auth_OpenID_AlreadySigned extends Auth_OpenID_EncodingError {
1614 // This response is already signed.
1618 * An error that indicates that the given return_to is not under the
1623 class Auth_OpenID_UntrustedReturnURL extends Auth_OpenID_ServerError {
1624 function Auth_OpenID_UntrustedReturnURL($message, $return_to,
1627 parent::Auth_OpenID_ServerError($message, "Untrusted return_to URL");
1628 $this->return_to = $return_to;
1629 $this->trust_root = $trust_root;
1634 return sprintf("return_to %s not under trust_root %s",
1635 $this->return_to, $this->trust_root);
1640 * I handle requests for an OpenID server.
1642 * Some types of requests (those which are not checkid requests) may
1643 * be handed to my {@link handleRequest} method, and I will take care
1644 * of it and return a response.
1646 * For your convenience, I also provide an interface to {@link
1647 * Auth_OpenID_Decoder::decode()} and {@link
1648 * Auth_OpenID_SigningEncoder::encode()} through my methods {@link
1649 * decodeRequest} and {@link encodeResponse}.
1651 * All my state is encapsulated in an {@link Auth_OpenID_OpenIDStore}.
1655 * <pre> $oserver = new Auth_OpenID_Server(Auth_OpenID_FileStore($data_path),
1656 * "http://example.com/op");
1657 * $request = $oserver->decodeRequest();
1658 * if (in_array($request->mode, array('checkid_immediate',
1659 * 'checkid_setup'))) {
1660 * if ($app->isAuthorized($request->identity, $request->trust_root)) {
1661 * $response = $request->answer(true);
1662 * } else if ($request->immediate) {
1663 * $response = $request->answer(false);
1665 * $app->showDecidePage($request);
1669 * $response = $oserver->handleRequest($request);
1672 * $webresponse = $oserver->encode($response);</pre>
1676 class Auth_OpenID_Server {
1677 function Auth_OpenID_Server(&$store, $op_endpoint=null)
1679 $this->store =& $store;
1680 $this->signatory =& new Auth_OpenID_Signatory($this->store);
1681 $this->encoder =& new Auth_OpenID_SigningEncoder($this->signatory);
1682 $this->decoder =& new Auth_OpenID_Decoder($this);
1683 $this->op_endpoint = $op_endpoint;
1684 $this->negotiator =& Auth_OpenID_getDefaultNegotiator();
1688 * Handle a request. Given an {@link Auth_OpenID_Request} object,
1689 * call the appropriate {@link Auth_OpenID_Server} method to
1690 * process the request and generate a response.
1692 * @param Auth_OpenID_Request $request An {@link Auth_OpenID_Request}
1693 * returned by {@link Auth_OpenID_Server::decodeRequest()}.
1695 * @return Auth_OpenID_ServerResponse $response A response object
1696 * capable of generating a user-agent reply.
1698 function handleRequest($request)
1700 if (method_exists($this, "openid_" . $request->mode)) {
1701 $handler = array($this, "openid_" . $request->mode);
1702 return call_user_func($handler, $request);
1708 * The callback for 'check_authentication' messages.
1710 function openid_check_authentication(&$request)
1712 return $request->answer($this->signatory);
1716 * The callback for 'associate' messages.
1718 function openid_associate(&$request)
1720 $assoc_type = $request->assoc_type;
1721 $session_type = $request->session->session_type;
1722 if ($this->negotiator->isAllowed($assoc_type, $session_type)) {
1723 $assoc = $this->signatory->createAssociation(false,
1725 return $request->answer($assoc);
1727 $message = sprintf('Association type %s is not supported with '.
1728 'session type %s', $assoc_type, $session_type);
1729 list($preferred_assoc_type, $preferred_session_type) =
1730 $this->negotiator->getAllowedType();
1731 return $request->answerUnsupported($message,
1732 $preferred_assoc_type,
1733 $preferred_session_type);
1738 * Encodes as response in the appropriate format suitable for
1739 * sending to the user agent.
1741 function encodeResponse(&$response)
1743 return $this->encoder->encode($response);
1747 * Decodes a query args array into the appropriate
1748 * {@link Auth_OpenID_Request} object.
1750 function decodeRequest($query=null)
1752 if ($query === null) {
1753 $query = Auth_OpenID::getQuery();
1756 return $this->decoder->decode($query);