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