]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - extlib/Auth/OpenID/Server.php
Merge remote-tracking branch 'upstream/master'
[quix0rs-gnu-social.git] / extlib / Auth / OpenID / Server.php
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  */
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";
105
106 define('AUTH_OPENID_HTTP_OK', 200);
107 define('AUTH_OPENID_HTTP_REDIRECT', 302);
108 define('AUTH_OPENID_HTTP_ERROR', 400);
109
110 /**
111  * @access private
112  */
113 global $_Auth_OpenID_Request_Modes;
114 $_Auth_OpenID_Request_Modes = array('checkid_setup',
115                                     'checkid_immediate');
116
117 /**
118  * @access private
119  */
120 define('Auth_OpenID_ENCODE_KVFORM', 'kfvorm');
121
122 /**
123  * @access private
124  */
125 define('Auth_OpenID_ENCODE_URL', 'URL/redirect');
126
127 /**
128  * @access private
129  */
130 define('Auth_OpenID_ENCODE_HTML_FORM', 'HTML form');
131
132 /**
133  * @access private
134  */
135 function Auth_OpenID_isError($obj, $cls = 'Auth_OpenID_ServerError')
136 {
137     return is_a($obj, $cls);
138 }
139
140 /**
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.
144  *
145  * @package OpenID
146  */
147 class Auth_OpenID_ServerError {
148     /**
149      * @access private
150      */
151     function Auth_OpenID_ServerError($message = null, $text = null,
152                                      $reference = null, $contact = null)
153     {
154         $this->message = $message;
155         $this->text = $text;
156         $this->contact = $contact;
157         $this->reference = $reference;
158     }
159
160     function getReturnTo()
161     {
162         if ($this->message &&
163             $this->message->hasKey(Auth_OpenID_OPENID_NS, 'return_to')) {
164             return $this->message->getArg(Auth_OpenID_OPENID_NS,
165                                           'return_to');
166         } else {
167             return null;
168         }
169     }
170
171     /**
172      * Returns the return_to URL for the request which caused this
173      * error.
174      */
175     function hasReturnTo()
176     {
177         return $this->getReturnTo() !== null;
178     }
179
180     /**
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.
184      */
185     function encodeToURL()
186     {
187         if (!$this->message) {
188             return null;
189         }
190
191         $msg = $this->toMessage();
192         return $msg->toURL($this->getReturnTo());
193     }
194
195     /**
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.
200      */
201     function encodeToKVForm()
202     {
203         return Auth_OpenID_KVForm::fromArray(
204                                       array('mode' => 'error',
205                                             'error' => $this->toString()));
206     }
207
208     function toFormMarkup($form_tag_attrs=null)
209     {
210         $msg = $this->toMessage();
211         return $msg->toFormMarkup($this->getReturnTo(), $form_tag_attrs);
212     }
213
214     function toHTML($form_tag_attrs=null)
215     {
216         return Auth_OpenID::autoSubmitHTML(
217                       $this->toFormMarkup($form_tag_attrs));
218     }
219
220     function toMessage()
221     {
222         // Generate a Message object for sending to the relying party,
223         // after encoding.
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());
228
229         if ($this->contact !== null) {
230             $reply->setArg(Auth_OpenID_OPENID_NS, 'contact', $this->contact);
231         }
232
233         if ($this->reference !== null) {
234             $reply->setArg(Auth_OpenID_OPENID_NS, 'reference',
235                            $this->reference);
236         }
237
238         return $reply;
239     }
240
241     /**
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.
245      */
246     function whichEncoding()
247     {
248         global $_Auth_OpenID_Request_Modes;
249
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;
255             } else {
256                 return Auth_OpenID_ENCODE_URL;
257             }
258         }
259
260         if (!$this->message) {
261             return null;
262         }
263
264         $mode = $this->message->getArg(Auth_OpenID_OPENID_NS,
265                                        'mode');
266
267         if ($mode) {
268             if (!in_array($mode, $_Auth_OpenID_Request_Modes)) {
269                 return Auth_OpenID_ENCODE_KVFORM;
270             }
271         }
272         return null;
273     }
274
275     /**
276      * Returns this error message.
277      */
278     function toString()
279     {
280         if ($this->text) {
281             return $this->text;
282         } else {
283             return get_class($this) . " error";
284         }
285     }
286 }
287
288 /**
289  * Error returned by the server code when a return_to is absent from a
290  * request.
291  *
292  * @package OpenID
293  */
294 class Auth_OpenID_NoReturnToError extends Auth_OpenID_ServerError {
295     function Auth_OpenID_NoReturnToError($message = null,
296                                          $text = "No return_to URL available")
297     {
298         parent::Auth_OpenID_ServerError($message, $text);
299     }
300
301     function toString()
302     {
303         return "No return_to available";
304     }
305 }
306
307 /**
308  * An error indicating that the return_to URL is malformed.
309  *
310  * @package OpenID
311  */
312 class Auth_OpenID_MalformedReturnURL extends Auth_OpenID_ServerError {
313     function Auth_OpenID_MalformedReturnURL($message, $return_to)
314     {
315         $this->return_to = $return_to;
316         parent::Auth_OpenID_ServerError($message, "malformed return_to URL");
317     }
318 }
319
320 /**
321  * This error is returned when the trust_root value is malformed.
322  *
323  * @package OpenID
324  */
325 class Auth_OpenID_MalformedTrustRoot extends Auth_OpenID_ServerError {
326     function Auth_OpenID_MalformedTrustRoot($message = null,
327                                             $text = "Malformed trust root")
328     {
329         parent::Auth_OpenID_ServerError($message, $text);
330     }
331
332     function toString()
333     {
334         return "Malformed trust root";
335     }
336 }
337
338 /**
339  * The base class for all server request classes.
340  *
341  * @package OpenID
342  */
343 class Auth_OpenID_Request {
344     var $mode = null;
345 }
346
347 /**
348  * A request to verify the validity of a previous response.
349  *
350  * @package OpenID
351  */
352 class Auth_OpenID_CheckAuthRequest extends Auth_OpenID_Request {
353     var $mode = "check_authentication";
354     var $invalidate_handle = null;
355
356     function Auth_OpenID_CheckAuthRequest($assoc_handle, $signed,
357                                           $invalidate_handle = null)
358     {
359         $this->assoc_handle = $assoc_handle;
360         $this->signed = $signed;
361         if ($invalidate_handle !== null) {
362             $this->invalidate_handle = $invalidate_handle;
363         }
364         $this->namespace = Auth_OpenID_OPENID2_NS;
365         $this->message = null;
366     }
367
368     static function fromMessage($message, $server=null)
369     {
370         $required_keys = array('assoc_handle', 'sig', 'signed');
371
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));
377             }
378         }
379
380         $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS, 'assoc_handle');
381         $sig = $message->getArg(Auth_OpenID_OPENID_NS, 'sig');
382
383         $signed_list = $message->getArg(Auth_OpenID_OPENID_NS, 'signed');
384         $signed_list = explode(",", $signed_list);
385
386         $signed = $message;
387         if ($signed->hasKey(Auth_OpenID_OPENID_NS, 'mode')) {
388             $signed->setArg(Auth_OpenID_OPENID_NS, 'mode', 'id_res');
389         }
390
391         $result = new Auth_OpenID_CheckAuthRequest($assoc_handle, $signed);
392         $result->message = $message;
393         $result->sig = $sig;
394         $result->invalidate_handle = $message->getArg(Auth_OpenID_OPENID_NS,
395                                                       'invalidate_handle');
396         return $result;
397     }
398
399     function answer($signatory)
400     {
401         $is_valid = $signatory->verify($this->assoc_handle, $this->signed);
402
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);
407
408         $response->fields->setArg(Auth_OpenID_OPENID_NS,
409                                   'is_valid',
410                                   ($is_valid ? "true" : "false"));
411
412         if ($this->invalidate_handle) {
413             $assoc = $signatory->getAssociation($this->invalidate_handle,
414                                                 false);
415             if (!$assoc) {
416                 $response->fields->setArg(Auth_OpenID_OPENID_NS,
417                                           'invalidate_handle',
418                                           $this->invalidate_handle);
419             }
420         }
421         return $response;
422     }
423 }
424
425 /**
426  * A class implementing plaintext server sessions.
427  *
428  * @package OpenID
429  */
430 class Auth_OpenID_PlainTextServerSession {
431     /**
432      * An object that knows how to handle association requests with no
433      * session type.
434      */
435     var $session_type = 'no-encryption';
436     var $needs_math = false;
437     var $allowed_assoc_types = array('HMAC-SHA1', 'HMAC-SHA256');
438
439     static function fromMessage($unused_request)
440     {
441         return new Auth_OpenID_PlainTextServerSession();
442     }
443
444     function answer($secret)
445     {
446         return array('mac_key' => base64_encode($secret));
447     }
448 }
449
450 /**
451  * A class implementing DH-SHA1 server sessions.
452  *
453  * @package OpenID
454  */
455 class Auth_OpenID_DiffieHellmanSHA1ServerSession {
456     /**
457      * An object that knows how to handle association requests with
458      * the Diffie-Hellman session type.
459      */
460
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';
465
466     function Auth_OpenID_DiffieHellmanSHA1ServerSession($dh, $consumer_pubkey)
467     {
468         $this->dh = $dh;
469         $this->consumer_pubkey = $consumer_pubkey;
470     }
471
472     static function getDH($message)
473     {
474         $dh_modulus = $message->getArg(Auth_OpenID_OPENID_NS, 'dh_modulus');
475         $dh_gen = $message->getArg(Auth_OpenID_OPENID_NS, 'dh_gen');
476
477         if ((($dh_modulus === null) && ($dh_gen !== null)) ||
478             (($dh_gen === null) && ($dh_modulus !== null))) {
479
480             if ($dh_modulus === null) {
481                 $missing = 'modulus';
482             } else {
483                 $missing = 'generator';
484             }
485
486             return new Auth_OpenID_ServerError($message,
487                                 'If non-default modulus or generator is '.
488                                 'supplied, both must be supplied.  Missing '.
489                                 $missing);
490         }
491
492         $lib = Auth_OpenID_getMathLib();
493
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");
501             }
502             $dh = new Auth_OpenID_DiffieHellman($dh_modulus, $dh_gen);
503         } else {
504             $dh = new Auth_OpenID_DiffieHellman();
505         }
506
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');
513         }
514
515         $consumer_pubkey =
516             $lib->base64ToLong($consumer_pubkey);
517
518         if ($consumer_pubkey === false) {
519             return new Auth_OpenID_ServerError($message,
520                                        "dh_consumer_public is not base64");
521         }
522
523         return array($dh, $consumer_pubkey);
524     }
525
526     static function fromMessage($message)
527     {
528         $result = Auth_OpenID_DiffieHellmanSHA1ServerSession::getDH($message);
529
530         if (is_a($result, 'Auth_OpenID_ServerError')) {
531             return $result;
532         } else {
533             list($dh, $consumer_pubkey) = $result;
534             return new Auth_OpenID_DiffieHellmanSHA1ServerSession($dh,
535                                                     $consumer_pubkey);
536         }
537     }
538
539     function answer($secret)
540     {
541         $lib = Auth_OpenID_getMathLib();
542         $mac_key = $this->dh->xorSecret($this->consumer_pubkey, $secret,
543                                         $this->hash_func);
544         return array(
545            'dh_server_public' =>
546                 $lib->longToBase64($this->dh->public),
547            'enc_mac_key' => base64_encode($mac_key));
548     }
549 }
550
551 /**
552  * A class implementing DH-SHA256 server sessions.
553  *
554  * @package OpenID
555  */
556 class Auth_OpenID_DiffieHellmanSHA256ServerSession
557       extends Auth_OpenID_DiffieHellmanSHA1ServerSession {
558
559     var $session_type = 'DH-SHA256';
560     var $hash_func = 'Auth_OpenID_SHA256';
561     var $allowed_assoc_types = array('HMAC-SHA256');
562
563     static function fromMessage($message)
564     {
565         $result = Auth_OpenID_DiffieHellmanSHA1ServerSession::getDH($message);
566
567         if (is_a($result, 'Auth_OpenID_ServerError')) {
568             return $result;
569         } else {
570             list($dh, $consumer_pubkey) = $result;
571             return new Auth_OpenID_DiffieHellmanSHA256ServerSession($dh,
572                                                       $consumer_pubkey);
573         }
574     }
575 }
576
577 /**
578  * A request to associate with the server.
579  *
580  * @package OpenID
581  */
582 class Auth_OpenID_AssociateRequest extends Auth_OpenID_Request {
583     var $mode = "associate";
584
585     static function getSessionClasses()
586     {
587         return array(
588           'no-encryption' => 'Auth_OpenID_PlainTextServerSession',
589           'DH-SHA1' => 'Auth_OpenID_DiffieHellmanSHA1ServerSession',
590           'DH-SHA256' => 'Auth_OpenID_DiffieHellmanSHA256ServerSession');
591     }
592
593     function Auth_OpenID_AssociateRequest($session, $assoc_type)
594     {
595         $this->session = $session;
596         $this->namespace = Auth_OpenID_OPENID2_NS;
597         $this->assoc_type = $assoc_type;
598     }
599
600     static function fromMessage($message, $server=null)
601     {
602         if ($message->isOpenID1()) {
603             $session_type = $message->getArg(Auth_OpenID_OPENID_NS,
604                                              'session_type');
605
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';
611             }
612         } else {
613             $session_type = $message->getArg(Auth_OpenID_OPENID_NS,
614                                              'session_type');
615             if ($session_type === null) {
616                 return new Auth_OpenID_ServerError($message,
617                   "session_type missing from request");
618             }
619         }
620
621         $session_class = Auth_OpenID::arrayGet(
622            Auth_OpenID_AssociateRequest::getSessionClasses(),
623            $session_type);
624
625         if ($session_class === null) {
626             return new Auth_OpenID_ServerError($message,
627                                                "Unknown session type " .
628                                                $session_type);
629         }
630
631         $session = call_user_func(array($session_class, 'fromMessage'),
632                                   $message);
633         if (is_a($session, 'Auth_OpenID_ServerError')) {
634             return $session;
635         }
636
637         $assoc_type = $message->getArg(Auth_OpenID_OPENID_NS,
638                                        'assoc_type', 'HMAC-SHA1');
639
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));
644         }
645
646         $obj = new Auth_OpenID_AssociateRequest($session, $assoc_type);
647         $obj->message = $message;
648         $obj->namespace = $message->getOpenIDNamespace();
649         return $obj;
650     }
651
652     function answer($assoc)
653     {
654         $response = new Auth_OpenID_ServerResponse($this);
655         $response->fields->updateArgs(Auth_OpenID_OPENID_NS,
656            array(
657                  'expires_in' => sprintf('%d', $assoc->getExpiresIn()),
658                  'assoc_type' => $this->assoc_type,
659                  'assoc_handle' => $assoc->handle));
660
661         $response->fields->updateArgs(Auth_OpenID_OPENID_NS,
662            $this->session->answer($assoc->secret));
663
664         if (! ($this->session->session_type == 'no-encryption' 
665                && $this->message->isOpenID1())) {
666             $response->fields->setArg(Auth_OpenID_OPENID_NS,
667                                       'session_type',
668                                       $this->session->session_type);
669         }
670
671         return $response;
672     }
673
674     function answerUnsupported($text_message,
675                                $preferred_association_type=null,
676                                $preferred_session_type=null)
677     {
678         if ($this->message->isOpenID1()) {
679             return new Auth_OpenID_ServerError($this->message);
680         }
681
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);
687
688         if ($preferred_association_type) {
689             $response->fields->setArg(Auth_OpenID_OPENID_NS,
690                                       'assoc_type',
691                                       $preferred_association_type);
692         }
693
694         if ($preferred_session_type) {
695             $response->fields->setArg(Auth_OpenID_OPENID_NS,
696                                       'session_type',
697                                       $preferred_session_type);
698         }
699         $response->code = AUTH_OPENID_HTTP_ERROR;
700         return $response;
701     }
702 }
703
704 /**
705  * A request to confirm the identity of a user.
706  *
707  * @package OpenID
708  */
709 class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request {
710     /**
711      * Return-to verification callback.  Default is
712      * Auth_OpenID_verifyReturnTo from TrustRoot.php.
713      */
714     var $verifyReturnTo = 'Auth_OpenID_verifyReturnTo';
715
716     /**
717      * The mode of this request.
718      */
719     var $mode = "checkid_setup"; // or "checkid_immediate"
720
721     /**
722      * Whether this request is for immediate mode.
723      */
724     var $immediate = false;
725
726     /**
727      * The trust_root value for this request.
728      */
729     var $trust_root = null;
730
731     /**
732      * The OpenID namespace for this request.
733      * deprecated since version 2.0.2
734      */
735     var $namespace;
736     
737     static function make($message, $identity, $return_to, $trust_root = null,
738                   $immediate = false, $assoc_handle = null, $server = null)
739     {
740         if ($server === null) {
741             return new Auth_OpenID_ServerError($message,
742                                                "server must not be null");
743         }
744
745         if ($return_to &&
746             !Auth_OpenID_TrustRoot::_parse($return_to)) {
747             return new Auth_OpenID_MalformedReturnURL($message, $return_to);
748         }
749
750         $r = new Auth_OpenID_CheckIDRequest($identity, $return_to,
751                                             $trust_root, $immediate,
752                                             $assoc_handle, $server);
753
754         $r->namespace = $message->getOpenIDNamespace();
755         $r->message = $message;
756
757         if (!$r->trustRootValid()) {
758             return new Auth_OpenID_UntrustedReturnURL($message,
759                                                       $return_to,
760                                                       $trust_root);
761         } else {
762             return $r;
763         }
764     }
765
766     function Auth_OpenID_CheckIDRequest($identity, $return_to,
767                                         $trust_root = null, $immediate = false,
768                                         $assoc_handle = null, $server = null,
769                                         $claimed_id = null)
770     {
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;
776         } else {
777             $this->claimed_id = $claimed_id;
778         }
779         $this->return_to = $return_to;
780         $this->trust_root = $trust_root;
781         $this->server = $server;
782
783         if ($immediate) {
784             $this->immediate = true;
785             $this->mode = "checkid_immediate";
786         } else {
787             $this->immediate = false;
788             $this->mode = "checkid_setup";
789         }
790     }
791
792     function equals($other)
793     {
794         return (
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));
802     }
803
804     /*
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
810      * redirectors.
811      *
812      * This check should only be performed after making sure that the
813      * return_to URL matches the realm.
814      *
815      * @return true if the realm publishes a document with the
816      * return_to URL listed, false if not or if discovery fails
817      */
818     function returnToVerified()
819     {
820         $fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
821         return call_user_func_array($this->verifyReturnTo,
822                                     array($this->trust_root, $this->return_to, $fetcher));
823     }
824
825     static function fromMessage($message, $server)
826     {
827         $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode');
828         $immediate = null;
829
830         if ($mode == "checkid_immediate") {
831             $immediate = true;
832             $mode = "checkid_immediate";
833         } else {
834             $immediate = false;
835             $mode = "checkid_setup";
836         }
837
838         $return_to = $message->getArg(Auth_OpenID_OPENID_NS,
839                                       'return_to');
840
841         if (($message->isOpenID1()) &&
842             (!$return_to)) {
843             $fmt = "Missing required field 'return_to' from checkid request";
844             return new Auth_OpenID_ServerError($message, $fmt);
845         }
846
847         $identity = $message->getArg(Auth_OpenID_OPENID_NS,
848                                      'identity');
849         $claimed_id = $message->getArg(Auth_OpenID_OPENID_NS, 'claimed_id');
850         if ($message->isOpenID1()) {
851             if ($identity === null) {
852                 $s = "OpenID 1 message did not contain openid.identity";
853                 return new Auth_OpenID_ServerError($message, $s);
854             }
855         } else {
856             if ($identity && !$claimed_id) {
857                 $s = "OpenID 2.0 message contained openid.identity but not " .
858                   "claimed_id";
859                 return new Auth_OpenID_ServerError($message, $s);
860             } else if ($claimed_id && !$identity) {
861                 $s = "OpenID 2.0 message contained openid.claimed_id " .
862                   "but not identity";
863                 return new Auth_OpenID_ServerError($message, $s);
864             }
865         }
866
867         // There's a case for making self.trust_root be a TrustRoot
868         // here.  But if TrustRoot isn't currently part of the
869         // "public" API, I'm not sure it's worth doing.
870         if ($message->isOpenID1()) {
871             $trust_root_param = 'trust_root';
872         } else {
873             $trust_root_param = 'realm';
874         }
875         $trust_root = $message->getArg(Auth_OpenID_OPENID_NS, 
876                                        $trust_root_param);
877         if (! $trust_root) {
878             $trust_root = $return_to;
879         }
880
881         if (! $message->isOpenID1() && 
882             ($return_to === null) &&
883             ($trust_root === null)) {
884             return new Auth_OpenID_ServerError($message,
885               "openid.realm required when openid.return_to absent");
886         }
887
888         $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS,
889                                          'assoc_handle');
890
891         $obj = Auth_OpenID_CheckIDRequest::make($message,
892                                                 $identity,
893                                                 $return_to,
894                                                 $trust_root,
895                                                 $immediate,
896                                                 $assoc_handle,
897                                                 $server);
898
899         if (is_a($obj, 'Auth_OpenID_ServerError')) {
900             return $obj;
901         }
902
903         $obj->claimed_id = $claimed_id;
904
905         return $obj;
906     }
907
908     function idSelect()
909     {
910         // Is the identifier to be selected by the IDP?
911         // So IDPs don't have to import the constant
912         return $this->identity == Auth_OpenID_IDENTIFIER_SELECT;
913     }
914
915     function trustRootValid()
916     {
917         if (!$this->trust_root) {
918             return true;
919         }
920
921         $tr = Auth_OpenID_TrustRoot::_parse($this->trust_root);
922         if ($tr === false) {
923             return new Auth_OpenID_MalformedTrustRoot($this->message,
924                                                       $this->trust_root);
925         }
926
927         if ($this->return_to !== null) {
928             return Auth_OpenID_TrustRoot::match($this->trust_root,
929                                                 $this->return_to);
930         } else {
931             return true;
932         }
933     }
934
935     /**
936      * Respond to this request.  Return either an
937      * {@link Auth_OpenID_ServerResponse} or
938      * {@link Auth_OpenID_ServerError}.
939      *
940      * @param bool $allow Allow this user to claim this identity, and
941      * allow the consumer to have this information?
942      *
943      * @param string $server_url DEPRECATED.  Passing $op_endpoint to
944      * the {@link Auth_OpenID_Server} constructor makes this optional.
945      *
946      * When an OpenID 1.x immediate mode request does not succeed, it
947      * gets back a URL where the request may be carried out in a
948      * not-so-immediate fashion.  Pass my URL in here (the fully
949      * qualified address of this server's endpoint, i.e.
950      * http://example.com/server), and I will use it as a base for the
951      * URL for a new request.
952      *
953      * Optional for requests where {@link $immediate} is false or
954      * $allow is true.
955      *
956      * @param string $identity The OP-local identifier to answer with.
957      * Only for use when the relying party requested identifier
958      * selection.
959      *
960      * @param string $claimed_id The claimed identifier to answer
961      * with, for use with identifier selection in the case where the
962      * claimed identifier and the OP-local identifier differ,
963      * i.e. when the claimed_id uses delegation.
964      *
965      * If $identity is provided but this is not, $claimed_id will
966      * default to the value of $identity.  When answering requests
967      * that did not ask for identifier selection, the response
968      * $claimed_id will default to that of the request.
969      *
970      * This parameter is new in OpenID 2.0.
971      *
972      * @return mixed
973      */
974     function answer($allow, $server_url = null, $identity = null,
975                     $claimed_id = null)
976     {
977         if (!$this->return_to) {
978             return new Auth_OpenID_NoReturnToError();
979         }
980
981         if (!$server_url) {
982             if ((!$this->message->isOpenID1()) &&
983                 (!$this->server->op_endpoint)) {
984                 return new Auth_OpenID_ServerError(null,
985                   "server should be constructed with op_endpoint to " .
986                   "respond to OpenID 2.0 messages.");
987             }
988
989             $server_url = $this->server->op_endpoint;
990         }
991
992         if ($allow) {
993             $mode = 'id_res';
994         } else if ($this->message->isOpenID1()) {
995             if ($this->immediate) {
996                 $mode = 'id_res';
997             } else {
998                 $mode = 'cancel';
999             }
1000         } else {
1001             if ($this->immediate) {
1002                 $mode = 'setup_needed';
1003             } else {
1004                 $mode = 'cancel';
1005             }
1006         }
1007
1008         if (!$this->trustRootValid()) {
1009             return new Auth_OpenID_UntrustedReturnURL(null,
1010                                                       $this->return_to,
1011                                                       $this->trust_root);
1012         }
1013
1014         $response = new Auth_OpenID_ServerResponse($this);
1015
1016         if ($claimed_id &&
1017             ($this->message->isOpenID1())) {
1018             return new Auth_OpenID_ServerError(null,
1019               "claimed_id is new in OpenID 2.0 and not " .
1020               "available for ".$this->namespace);
1021         }
1022
1023         if ($identity && !$claimed_id) {
1024             $claimed_id = $identity;
1025         }
1026
1027         if ($allow) {
1028
1029             if ($this->identity == Auth_OpenID_IDENTIFIER_SELECT) {
1030                 if (!$identity) {
1031                     return new Auth_OpenID_ServerError(null,
1032                       "This request uses IdP-driven identifier selection.  " .
1033                       "You must supply an identifier in the response.");
1034                 }
1035
1036                 $response_identity = $identity;
1037                 $response_claimed_id = $claimed_id;
1038
1039             } else if ($this->identity) {
1040                 if ($identity &&
1041                     ($this->identity != $identity)) {
1042                     $fmt = "Request was for %s, cannot reply with identity %s";
1043                     return new Auth_OpenID_ServerError(null,
1044                       sprintf($fmt, $this->identity, $identity));
1045                 }
1046
1047                 $response_identity = $this->identity;
1048                 $response_claimed_id = $this->claimed_id;
1049             } else {
1050                 if ($identity) {
1051                     return new Auth_OpenID_ServerError(null,
1052                       "This request specified no identity and " .
1053                       "you supplied ".$identity);
1054                 }
1055
1056                 $response_identity = null;
1057             }
1058
1059             if (($this->message->isOpenID1()) &&
1060                 ($response_identity === null)) {
1061                 return new Auth_OpenID_ServerError(null,
1062                   "Request was an OpenID 1 request, so response must " .
1063                   "include an identifier.");
1064             }
1065
1066             $response->fields->updateArgs(Auth_OpenID_OPENID_NS,
1067                    array('mode' => $mode,
1068                          'return_to' => $this->return_to,
1069                          'response_nonce' => Auth_OpenID_mkNonce()));
1070
1071             if (!$this->message->isOpenID1()) {
1072                 $response->fields->setArg(Auth_OpenID_OPENID_NS,
1073                                           'op_endpoint', $server_url);
1074             }
1075
1076             if ($response_identity !== null) {
1077                 $response->fields->setArg(
1078                                           Auth_OpenID_OPENID_NS,
1079                                           'identity',
1080                                           $response_identity);
1081                 if ($this->message->isOpenID2()) {
1082                     $response->fields->setArg(
1083                                               Auth_OpenID_OPENID_NS,
1084                                               'claimed_id',
1085                                               $response_claimed_id);
1086                 }
1087             }
1088
1089         } else {
1090             $response->fields->setArg(Auth_OpenID_OPENID_NS,
1091                                       'mode', $mode);
1092
1093             if ($this->immediate) {
1094                 if (($this->message->isOpenID1()) &&
1095                     (!$server_url)) {
1096                     return new Auth_OpenID_ServerError(null,
1097                                  'setup_url is required for $allow=false \
1098                                   in OpenID 1.x immediate mode.');
1099                 }
1100
1101                 $setup_request = new Auth_OpenID_CheckIDRequest(
1102                                                 $this->identity,
1103                                                 $this->return_to,
1104                                                 $this->trust_root,
1105                                                 false,
1106                                                 $this->assoc_handle,
1107                                                 $this->server,
1108                                                 $this->claimed_id);
1109                 $setup_request->message = $this->message;
1110
1111                 $setup_url = $setup_request->encodeToURL($server_url);
1112
1113                 if ($setup_url === null) {
1114                     return new Auth_OpenID_NoReturnToError();
1115                 }
1116
1117                 $response->fields->setArg(Auth_OpenID_OPENID_NS,
1118                                           'user_setup_url',
1119                                           $setup_url);
1120             }
1121         }
1122
1123         return $response;
1124     }
1125
1126     function encodeToURL($server_url)
1127     {
1128         if (!$this->return_to) {
1129             return new Auth_OpenID_NoReturnToError();
1130         }
1131
1132         // Imported from the alternate reality where these classes are
1133         // used in both the client and server code, so Requests are
1134         // Encodable too.  That's right, code imported from alternate
1135         // realities all for the love of you, id_res/user_setup_url.
1136
1137         $q = array('mode' => $this->mode,
1138                    'identity' => $this->identity,
1139                    'claimed_id' => $this->claimed_id,
1140                    'return_to' => $this->return_to);
1141
1142         if ($this->trust_root) {
1143             if ($this->message->isOpenID1()) {
1144                 $q['trust_root'] = $this->trust_root;
1145             } else {
1146                 $q['realm'] = $this->trust_root;
1147             }
1148         }
1149
1150         if ($this->assoc_handle) {
1151             $q['assoc_handle'] = $this->assoc_handle;
1152         }
1153
1154         $response = new Auth_OpenID_Message(
1155             $this->message->getOpenIDNamespace());
1156         $response->updateArgs(Auth_OpenID_OPENID_NS, $q);
1157         return $response->toURL($server_url);
1158     }
1159
1160     function getCancelURL()
1161     {
1162         if (!$this->return_to) {
1163             return new Auth_OpenID_NoReturnToError();
1164         }
1165
1166         if ($this->immediate) {
1167             return new Auth_OpenID_ServerError(null,
1168                                                "Cancel is not an appropriate \
1169                                                response to immediate mode \
1170                                                requests.");
1171         }
1172
1173         $response = new Auth_OpenID_Message(
1174             $this->message->getOpenIDNamespace());
1175         $response->setArg(Auth_OpenID_OPENID_NS, 'mode', 'cancel');
1176         return $response->toURL($this->return_to);
1177     }
1178 }
1179
1180 /**
1181  * This class encapsulates the response to an OpenID server request.
1182  *
1183  * @package OpenID
1184  */
1185 class Auth_OpenID_ServerResponse {
1186
1187     function Auth_OpenID_ServerResponse($request)
1188     {
1189         $this->request = $request;
1190         $this->fields = new Auth_OpenID_Message($this->request->namespace);
1191     }
1192
1193     function whichEncoding()
1194     {
1195       global $_Auth_OpenID_Request_Modes;
1196
1197         if (in_array($this->request->mode, $_Auth_OpenID_Request_Modes)) {
1198             if ($this->fields->isOpenID2() &&
1199                 (strlen($this->encodeToURL()) >
1200                    Auth_OpenID_OPENID1_URL_LIMIT)) {
1201                 return Auth_OpenID_ENCODE_HTML_FORM;
1202             } else {
1203                 return Auth_OpenID_ENCODE_URL;
1204             }
1205         } else {
1206             return Auth_OpenID_ENCODE_KVFORM;
1207         }
1208     }
1209
1210     /*
1211      * Returns the form markup for this response.
1212      *
1213      * @return str
1214      */
1215     function toFormMarkup($form_tag_attrs=null)
1216     {
1217         return $this->fields->toFormMarkup($this->request->return_to,
1218                                            $form_tag_attrs);
1219     }
1220
1221     /*
1222      * Returns an HTML document containing the form markup for this
1223      * response that autosubmits with javascript.
1224      */
1225     function toHTML()
1226     {
1227         return Auth_OpenID::autoSubmitHTML($this->toFormMarkup());
1228     }
1229
1230     /*
1231      * Returns True if this response's encoding is ENCODE_HTML_FORM.
1232      * Convenience method for server authors.
1233      *
1234      * @return bool
1235      */
1236     function renderAsForm()
1237     {
1238         return $this->whichEncoding() == Auth_OpenID_ENCODE_HTML_FORM;
1239     }
1240
1241
1242     function encodeToURL()
1243     {
1244         return $this->fields->toURL($this->request->return_to);
1245     }
1246
1247     function addExtension($extension_response)
1248     {
1249         $extension_response->toMessage($this->fields);
1250     }
1251
1252     function needsSigning()
1253     {
1254         return $this->fields->getArg(Auth_OpenID_OPENID_NS,
1255                                      'mode') == 'id_res';
1256     }
1257
1258     function encodeToKVForm()
1259     {
1260         return $this->fields->toKVForm();
1261     }
1262 }
1263
1264 /**
1265  * A web-capable response object which you can use to generate a
1266  * user-agent response.
1267  *
1268  * @package OpenID
1269  */
1270 class Auth_OpenID_WebResponse {
1271     var $code = AUTH_OPENID_HTTP_OK;
1272     var $body = "";
1273
1274     function Auth_OpenID_WebResponse($code = null, $headers = null,
1275                                      $body = null)
1276     {
1277         if ($code) {
1278             $this->code = $code;
1279         }
1280
1281         if ($headers !== null) {
1282             $this->headers = $headers;
1283         } else {
1284             $this->headers = array();
1285         }
1286
1287         if ($body !== null) {
1288             $this->body = $body;
1289         }
1290     }
1291 }
1292
1293 /**
1294  * Responsible for the signature of query data and the verification of
1295  * OpenID signature values.
1296  *
1297  * @package OpenID
1298  */
1299 class Auth_OpenID_Signatory {
1300
1301     // = 14 * 24 * 60 * 60; # 14 days, in seconds
1302     var $SECRET_LIFETIME = 1209600;
1303
1304     // keys have a bogus server URL in them because the filestore
1305     // really does expect that key to be a URL.  This seems a little
1306     // silly for the server store, since I expect there to be only one
1307     // server URL.
1308     var $normal_key = 'http://localhost/|normal';
1309     var $dumb_key = 'http://localhost/|dumb';
1310
1311     /**
1312      * Create a new signatory using a given store.
1313      */
1314     function Auth_OpenID_Signatory($store)
1315     {
1316         // assert store is not None
1317         $this->store = $store;
1318     }
1319
1320     /**
1321      * Verify, using a given association handle, a signature with
1322      * signed key-value pairs from an HTTP request.
1323      */
1324     function verify($assoc_handle, $message)
1325     {
1326         $assoc = $this->getAssociation($assoc_handle, true);
1327         if (!$assoc) {
1328             // oidutil.log("failed to get assoc with handle %r to verify sig %r"
1329             //             % (assoc_handle, sig))
1330             return false;
1331         }
1332
1333         return $assoc->checkMessageSignature($message);
1334     }
1335
1336     /**
1337      * Given a response, sign the fields in the response's 'signed'
1338      * list, and insert the signature into the response.
1339      */
1340     function sign($response)
1341     {
1342         $signed_response = $response;
1343         $assoc_handle = $response->request->assoc_handle;
1344
1345         if ($assoc_handle) {
1346             // normal mode
1347             $assoc = $this->getAssociation($assoc_handle, false, false);
1348             if (!$assoc || ($assoc->getExpiresIn() <= 0)) {
1349                 // fall back to dumb mode
1350                 $signed_response->fields->setArg(Auth_OpenID_OPENID_NS,
1351                              'invalidate_handle', $assoc_handle);
1352                 $assoc_type = ($assoc ? $assoc->assoc_type : 'HMAC-SHA1');
1353
1354                 if ($assoc && ($assoc->getExpiresIn() <= 0)) {
1355                     $this->invalidate($assoc_handle, false);
1356                 }
1357
1358                 $assoc = $this->createAssociation(true, $assoc_type);
1359             }
1360         } else {
1361             // dumb mode.
1362             $assoc = $this->createAssociation(true);
1363         }
1364
1365         $signed_response->fields = $assoc->signMessage(
1366                                       $signed_response->fields);
1367         return $signed_response;
1368     }
1369
1370     /**
1371      * Make a new association.
1372      */
1373     function createAssociation($dumb = true, $assoc_type = 'HMAC-SHA1')
1374     {
1375         $secret = Auth_OpenID_CryptUtil::getBytes(
1376                     Auth_OpenID_getSecretSize($assoc_type));
1377
1378         $uniq = base64_encode(Auth_OpenID_CryptUtil::getBytes(4));
1379         $handle = sprintf('{%s}{%x}{%s}', $assoc_type, intval(time()), $uniq);
1380
1381         $assoc = Auth_OpenID_Association::fromExpiresIn(
1382                       $this->SECRET_LIFETIME, $handle, $secret, $assoc_type);
1383
1384         if ($dumb) {
1385             $key = $this->dumb_key;
1386         } else {
1387             $key = $this->normal_key;
1388         }
1389
1390         $this->store->storeAssociation($key, $assoc);
1391         return $assoc;
1392     }
1393
1394     /**
1395      * Given an association handle, get the association from the
1396      * store, or return a ServerError or null if something goes wrong.
1397      */
1398     function getAssociation($assoc_handle, $dumb, $check_expiration=true)
1399     {
1400         if ($assoc_handle === null) {
1401             return new Auth_OpenID_ServerError(null,
1402                                      "assoc_handle must not be null");
1403         }
1404
1405         if ($dumb) {
1406             $key = $this->dumb_key;
1407         } else {
1408             $key = $this->normal_key;
1409         }
1410
1411         $assoc = $this->store->getAssociation($key, $assoc_handle);
1412
1413         if (($assoc !== null) && ($assoc->getExpiresIn() <= 0)) {
1414             if ($check_expiration) {
1415                 $this->store->removeAssociation($key, $assoc_handle);
1416                 $assoc = null;
1417             }
1418         }
1419
1420         return $assoc;
1421     }
1422
1423     /**
1424      * Invalidate a given association handle.
1425      */
1426     function invalidate($assoc_handle, $dumb)
1427     {
1428         if ($dumb) {
1429             $key = $this->dumb_key;
1430         } else {
1431             $key = $this->normal_key;
1432         }
1433         $this->store->removeAssociation($key, $assoc_handle);
1434     }
1435 }
1436
1437 /**
1438  * Encode an {@link Auth_OpenID_ServerResponse} to an
1439  * {@link Auth_OpenID_WebResponse}.
1440  *
1441  * @package OpenID
1442  */
1443 class Auth_OpenID_Encoder {
1444
1445     var $responseFactory = 'Auth_OpenID_WebResponse';
1446
1447     /**
1448      * Encode an {@link Auth_OpenID_ServerResponse} and return an
1449      * {@link Auth_OpenID_WebResponse}.
1450      */
1451     function encode($response)
1452     {
1453         $cls = $this->responseFactory;
1454
1455         $encode_as = $response->whichEncoding();
1456         if ($encode_as == Auth_OpenID_ENCODE_KVFORM) {
1457             $wr = new $cls(null, null, $response->encodeToKVForm());
1458             if (is_a($response, 'Auth_OpenID_ServerError')) {
1459                 $wr->code = AUTH_OPENID_HTTP_ERROR;
1460             }
1461         } else if ($encode_as == Auth_OpenID_ENCODE_URL) {
1462             $location = $response->encodeToURL();
1463             $wr = new $cls(AUTH_OPENID_HTTP_REDIRECT,
1464                            array('location' => $location));
1465         } else if ($encode_as == Auth_OpenID_ENCODE_HTML_FORM) {
1466           $wr = new $cls(AUTH_OPENID_HTTP_OK, array(),
1467                          $response->toHTML());
1468         } else {
1469             return new Auth_OpenID_EncodingError($response);
1470         }
1471         /* Allow the response to carry a custom error code (ex: for Association errors) */
1472         if(isset($response->code)) {
1473             $wr->code = $response->code;
1474         }
1475         return $wr;
1476     }
1477 }
1478
1479 /**
1480  * An encoder which also takes care of signing fields when required.
1481  *
1482  * @package OpenID
1483  */
1484 class Auth_OpenID_SigningEncoder extends Auth_OpenID_Encoder {
1485
1486     function Auth_OpenID_SigningEncoder($signatory)
1487     {
1488         $this->signatory = $signatory;
1489     }
1490
1491     /**
1492      * Sign an {@link Auth_OpenID_ServerResponse} and return an
1493      * {@link Auth_OpenID_WebResponse}.
1494      */
1495     function encode($response)
1496     {
1497         // the isinstance is a bit of a kludge... it means there isn't
1498         // really an adapter to make the interfaces quite match.
1499         if (!is_a($response, 'Auth_OpenID_ServerError') &&
1500             $response->needsSigning()) {
1501
1502             if (!$this->signatory) {
1503                 return new Auth_OpenID_ServerError(null,
1504                                        "Must have a store to sign request");
1505             }
1506
1507             if ($response->fields->hasKey(Auth_OpenID_OPENID_NS, 'sig')) {
1508                 return new Auth_OpenID_AlreadySigned($response);
1509             }
1510             $response = $this->signatory->sign($response);
1511         }
1512
1513         return parent::encode($response);
1514     }
1515 }
1516
1517 /**
1518  * Decode an incoming query into an Auth_OpenID_Request.
1519  *
1520  * @package OpenID
1521  */
1522 class Auth_OpenID_Decoder {
1523
1524     function Auth_OpenID_Decoder($server)
1525     {
1526         $this->server = $server;
1527
1528         $this->handlers = array(
1529             'checkid_setup' => 'Auth_OpenID_CheckIDRequest',
1530             'checkid_immediate' => 'Auth_OpenID_CheckIDRequest',
1531             'check_authentication' => 'Auth_OpenID_CheckAuthRequest',
1532             'associate' => 'Auth_OpenID_AssociateRequest'
1533             );
1534     }
1535
1536     /**
1537      * Given an HTTP query in an array (key-value pairs), decode it
1538      * into an Auth_OpenID_Request object.
1539      */
1540     function decode($query)
1541     {
1542         if (!$query) {
1543             return null;
1544         }
1545
1546         $message = Auth_OpenID_Message::fromPostArgs($query);
1547
1548         if ($message === null) {
1549             /*
1550              * It's useful to have a Message attached to a
1551              * ProtocolError, so we override the bad ns value to build
1552              * a Message out of it.  Kinda kludgy, since it's made of
1553              * lies, but the parts that aren't lies are more useful
1554              * than a 'None'.
1555              */
1556             $old_ns = $query['openid.ns'];
1557
1558             $query['openid.ns'] = Auth_OpenID_OPENID2_NS;
1559             $message = Auth_OpenID_Message::fromPostArgs($query);
1560             return new Auth_OpenID_ServerError(
1561                   $message,
1562                   sprintf("Invalid OpenID namespace URI: %s", $old_ns));
1563         }
1564
1565         $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode');
1566         if (!$mode) {
1567             return new Auth_OpenID_ServerError($message,
1568                                                "No mode value in message");
1569         }
1570
1571         if (Auth_OpenID::isFailure($mode)) {
1572             return new Auth_OpenID_ServerError($message,
1573                                                $mode->message);
1574         }
1575
1576         $handlerCls = Auth_OpenID::arrayGet($this->handlers, $mode,
1577                                             $this->defaultDecoder($message));
1578
1579         if (!is_a($handlerCls, 'Auth_OpenID_ServerError')) {
1580             return call_user_func_array(array($handlerCls, 'fromMessage'),
1581                                         array($message, $this->server));
1582         } else {
1583             return $handlerCls;
1584         }
1585     }
1586
1587     function defaultDecoder($message)
1588     {
1589         $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode');
1590
1591         if (Auth_OpenID::isFailure($mode)) {
1592             return new Auth_OpenID_ServerError($message,
1593                                                $mode->message);
1594         }
1595
1596         return new Auth_OpenID_ServerError($message,
1597                        sprintf("Unrecognized OpenID mode %s", $mode));
1598     }
1599 }
1600
1601 /**
1602  * An error that indicates an encoding problem occurred.
1603  *
1604  * @package OpenID
1605  */
1606 class Auth_OpenID_EncodingError {
1607     function Auth_OpenID_EncodingError($response)
1608     {
1609         $this->response = $response;
1610     }
1611 }
1612
1613 /**
1614  * An error that indicates that a response was already signed.
1615  *
1616  * @package OpenID
1617  */
1618 class Auth_OpenID_AlreadySigned extends Auth_OpenID_EncodingError {
1619     // This response is already signed.
1620 }
1621
1622 /**
1623  * An error that indicates that the given return_to is not under the
1624  * given trust_root.
1625  *
1626  * @package OpenID
1627  */
1628 class Auth_OpenID_UntrustedReturnURL extends Auth_OpenID_ServerError {
1629     function Auth_OpenID_UntrustedReturnURL($message, $return_to,
1630                                             $trust_root)
1631     {
1632         parent::Auth_OpenID_ServerError($message, "Untrusted return_to URL");
1633         $this->return_to = $return_to;
1634         $this->trust_root = $trust_root;
1635     }
1636
1637     function toString()
1638     {
1639         return sprintf("return_to %s not under trust_root %s",
1640                        $this->return_to, $this->trust_root);
1641     }
1642 }
1643
1644 /**
1645  * I handle requests for an OpenID server.
1646  *
1647  * Some types of requests (those which are not checkid requests) may
1648  * be handed to my {@link handleRequest} method, and I will take care
1649  * of it and return a response.
1650  *
1651  * For your convenience, I also provide an interface to {@link
1652  * Auth_OpenID_Decoder::decode()} and {@link
1653  * Auth_OpenID_SigningEncoder::encode()} through my methods {@link
1654  * decodeRequest} and {@link encodeResponse}.
1655  *
1656  * All my state is encapsulated in an {@link Auth_OpenID_OpenIDStore}.
1657  *
1658  * Example:
1659  *
1660  * <pre> $oserver = new Auth_OpenID_Server(Auth_OpenID_FileStore($data_path),
1661  *                                   "http://example.com/op");
1662  * $request = $oserver->decodeRequest();
1663  * if (in_array($request->mode, array('checkid_immediate',
1664  *                                    'checkid_setup'))) {
1665  *     if ($app->isAuthorized($request->identity, $request->trust_root)) {
1666  *         $response = $request->answer(true);
1667  *     } else if ($request->immediate) {
1668  *         $response = $request->answer(false);
1669  *     } else {
1670  *         $app->showDecidePage($request);
1671  *         return;
1672  *     }
1673  * } else {
1674  *     $response = $oserver->handleRequest($request);
1675  * }
1676  *
1677  * $webresponse = $oserver->encode($response);</pre>
1678  *
1679  * @package OpenID
1680  */
1681 class Auth_OpenID_Server {
1682     function Auth_OpenID_Server($store, $op_endpoint=null)
1683     {
1684         $this->store = $store;
1685         $this->signatory = new Auth_OpenID_Signatory($this->store);
1686         $this->encoder = new Auth_OpenID_SigningEncoder($this->signatory);
1687         $this->decoder = new Auth_OpenID_Decoder($this);
1688         $this->op_endpoint = $op_endpoint;
1689         $this->negotiator = Auth_OpenID_getDefaultNegotiator();
1690     }
1691
1692     /**
1693      * Handle a request.  Given an {@link Auth_OpenID_Request} object,
1694      * call the appropriate {@link Auth_OpenID_Server} method to
1695      * process the request and generate a response.
1696      *
1697      * @param Auth_OpenID_Request $request An {@link Auth_OpenID_Request}
1698      * returned by {@link Auth_OpenID_Server::decodeRequest()}.
1699      *
1700      * @return Auth_OpenID_ServerResponse $response A response object
1701      * capable of generating a user-agent reply.
1702      */
1703     function handleRequest($request)
1704     {
1705         if (method_exists($this, "openid_" . $request->mode)) {
1706             $handler = array($this, "openid_" . $request->mode);
1707             return call_user_func_array($handler, array($request));
1708         }
1709         return null;
1710     }
1711
1712     /**
1713      * The callback for 'check_authentication' messages.
1714      */
1715     function openid_check_authentication($request)
1716     {
1717         return $request->answer($this->signatory);
1718     }
1719
1720     /**
1721      * The callback for 'associate' messages.
1722      */
1723     function openid_associate($request)
1724     {
1725         $assoc_type = $request->assoc_type;
1726         $session_type = $request->session->session_type;
1727         if ($this->negotiator->isAllowed($assoc_type, $session_type)) {
1728             $assoc = $this->signatory->createAssociation(false,
1729                                                          $assoc_type);
1730             return $request->answer($assoc);
1731         } else {
1732             $message = sprintf('Association type %s is not supported with '.
1733                                'session type %s', $assoc_type, $session_type);
1734             list($preferred_assoc_type, $preferred_session_type) =
1735                 $this->negotiator->getAllowedType();
1736             return $request->answerUnsupported($message,
1737                                                $preferred_assoc_type,
1738                                                $preferred_session_type);
1739         }
1740     }
1741
1742     /**
1743      * Encodes as response in the appropriate format suitable for
1744      * sending to the user agent.
1745      */
1746     function encodeResponse($response)
1747     {
1748         return $this->encoder->encode($response);
1749     }
1750
1751     /**
1752      * Decodes a query args array into the appropriate
1753      * {@link Auth_OpenID_Request} object.
1754      */
1755     function decodeRequest($query=null)
1756     {
1757         if ($query === null) {
1758             $query = Auth_OpenID::getQuery();
1759         }
1760
1761         return $this->decoder->decode($query);
1762     }
1763 }
1764
1765