1<?php
2
3/**
4 * OpenID server protocol and logic.
5 *
6 * Overview
7 *
8 * An OpenID server must perform three tasks:
9 *
10 *  1. Examine the incoming request to determine its nature and validity.
11 *  2. Make a decision about how to respond to this request.
12 *  3. Format the response according to the protocol.
13 *
14 * The first and last of these tasks may performed by the {@link
15 * Auth_OpenID_Server::decodeRequest()} and {@link
16 * Auth_OpenID_Server::encodeResponse} methods.  Who gets to do the
17 * intermediate task -- deciding how to respond to the request -- will
18 * depend on what type of request it is.
19 *
20 * If it's a request to authenticate a user (a 'checkid_setup' or
21 * 'checkid_immediate' request), you need to decide if you will assert
22 * that this user may claim the identity in question.  Exactly how you
23 * do that is a matter of application policy, but it generally
24 * involves making sure the user has an account with your system and
25 * is logged in, checking to see if that identity is hers to claim,
26 * and verifying with the user that she does consent to releasing that
27 * information to the party making the request.
28 *
29 * Examine the properties of the {@link Auth_OpenID_CheckIDRequest}
30 * object, and if and when you've come to a decision, form a response
31 * by calling {@link Auth_OpenID_CheckIDRequest::answer()}.
32 *
33 * Other types of requests relate to establishing associations between
34 * client and server and verifing the authenticity of previous
35 * communications.  {@link Auth_OpenID_Server} contains all the logic
36 * and data necessary to respond to such requests; just pass it to
37 * {@link Auth_OpenID_Server::handleRequest()}.
38 *
39 * OpenID Extensions
40 *
41 * Do you want to provide other information for your users in addition
42 * to authentication?  Version 1.2 of the OpenID protocol allows
43 * consumers to add extensions to their requests.  For example, with
44 * sites using the Simple Registration
45 * Extension
46 * (http://openid.net/specs/openid-simple-registration-extension-1_0.html),
47 * a user can agree to have their nickname and e-mail address sent to
48 * a site when they sign up.
49 *
50 * Since extensions do not change the way OpenID authentication works,
51 * code to handle extension requests may be completely separate from
52 * the {@link Auth_OpenID_Request} class here.  But you'll likely want
53 * data sent back by your extension to be signed.  {@link
54 * Auth_OpenID_ServerResponse} provides methods with which you can add
55 * data to it which can be signed with the other data in the OpenID
56 * signature.
57 *
58 * For example:
59 *
60 * <pre>  // when request is a checkid_* request
61 *  $response = $request->answer(true);
62 *  // this will a signed 'openid.sreg.timezone' parameter to the response
63 *  response.addField('sreg', 'timezone', 'America/Los_Angeles')</pre>
64 *
65 * Stores
66 *
67 * The OpenID server needs to maintain state between requests in order
68 * to function.  Its mechanism for doing this is called a store.  The
69 * store interface is defined in Interface.php.  Additionally, several
70 * concrete store implementations are provided, so that most sites
71 * won't need to implement a custom store.  For a store backed by flat
72 * files on disk, see {@link Auth_OpenID_FileStore}.  For stores based
73 * on MySQL, SQLite, or PostgreSQL, see the {@link
74 * Auth_OpenID_SQLStore} subclasses.
75 *
76 * Upgrading
77 *
78 * The keys by which a server looks up associations in its store have
79 * changed in version 1.2 of this library.  If your store has entries
80 * created from version 1.0 code, you should empty it.
81 *
82 * PHP versions 4 and 5
83 *
84 * LICENSE: See the COPYING file included in this distribution.
85 *
86 * @package OpenID
87 * @author JanRain, Inc. <openid@janrain.com>
88 * @copyright 2005-2008 Janrain, Inc.
89 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
90 */
91
92/**
93 * Required imports
94 */
95require_once "Auth/OpenID.php";
96require_once "Auth/OpenID/Association.php";
97require_once "Auth/OpenID/CryptUtil.php";
98require_once "Auth/OpenID/BigMath.php";
99require_once "Auth/OpenID/DiffieHellman.php";
100require_once "Auth/OpenID/KVForm.php";
101require_once "Auth/OpenID/TrustRoot.php";
102require_once "Auth/OpenID/ServerRequest.php";
103require_once "Auth/OpenID/Message.php";
104require_once "Auth/OpenID/Nonce.php";
105
106define('AUTH_OPENID_HTTP_OK', 200);
107define('AUTH_OPENID_HTTP_REDIRECT', 302);
108define('AUTH_OPENID_HTTP_ERROR', 400);
109
110/**
111 * @access private
112 */
113global $_Auth_OpenID_Request_Modes;
114$_Auth_OpenID_Request_Modes = array('checkid_setup',
115                                    'checkid_immediate');
116
117/**
118 * @access private
119 */
120define('Auth_OpenID_ENCODE_KVFORM', 'kfvorm');
121
122/**
123 * @access private
124 */
125define('Auth_OpenID_ENCODE_URL', 'URL/redirect');
126
127/**
128 * @access private
129 */
130define('Auth_OpenID_ENCODE_HTML_FORM', 'HTML form');
131
132/**
133 * @access private
134 */
135function 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 */
147class 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 */
294class 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 */
312class 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 */
325class 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 */
343class 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 */
352class 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 */
430class 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 */
455class 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 */
556class 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 */
582class 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 */
709class 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 */
1185class 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 */
1270class 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 */
1299class 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 */
1443class 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 */
1484class 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 */
1522class 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 */
1606class 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 */
1618class 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 */
1628class 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 */
1681class 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($handler, &$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
1766