1<?php
2
3/**
4 * This module documents the main interface with the OpenID consumer
5 * library.  The only part of the library which has to be used and
6 * isn't documented in full here is the store required to create an
7 * Auth_OpenID_Consumer instance.  More on the abstract store type and
8 * concrete implementations of it that are provided in the
9 * documentation for the Auth_OpenID_Consumer constructor.
10 *
11 * OVERVIEW
12 *
13 * The OpenID identity verification process most commonly uses the
14 * following steps, as visible to the user of this library:
15 *
16 *   1. The user enters their OpenID into a field on the consumer's
17 *      site, and hits a login button.
18 *   2. The consumer site discovers the user's OpenID server using the
19 *      YADIS protocol.
20 *   3. The consumer site sends the browser a redirect to the identity
21 *      server.  This is the authentication request as described in
22 *      the OpenID specification.
23 *   4. The identity server's site sends the browser a redirect back
24 *      to the consumer site.  This redirect contains the server's
25 *      response to the authentication request.
26 *
27 * The most important part of the flow to note is the consumer's site
28 * must handle two separate HTTP requests in order to perform the full
29 * identity check.
30 *
31 * LIBRARY DESIGN
32 *
33 * This consumer library is designed with that flow in mind.  The goal
34 * is to make it as easy as possible to perform the above steps
35 * securely.
36 *
37 * At a high level, there are two important parts in the consumer
38 * library.  The first important part is this module, which contains
39 * the interface to actually use this library.  The second is the
40 * Auth_OpenID_Interface class, which describes the interface to use
41 * if you need to create a custom method for storing the state this
42 * library needs to maintain between requests.
43 *
44 * In general, the second part is less important for users of the
45 * library to know about, as several implementations are provided
46 * which cover a wide variety of situations in which consumers may use
47 * the library.
48 *
49 * This module contains a class, Auth_OpenID_Consumer, with methods
50 * corresponding to the actions necessary in each of steps 2, 3, and 4
51 * described in the overview.  Use of this library should be as easy
52 * as creating an Auth_OpenID_Consumer instance and calling the
53 * methods appropriate for the action the site wants to take.
54 *
55 * STORES AND DUMB MODE
56 *
57 * OpenID is a protocol that works best when the consumer site is able
58 * to store some state.  This is the normal mode of operation for the
59 * protocol, and is sometimes referred to as smart mode.  There is
60 * also a fallback mode, known as dumb mode, which is available when
61 * the consumer site is not able to store state.  This mode should be
62 * avoided when possible, as it leaves the implementation more
63 * vulnerable to replay attacks.
64 *
65 * The mode the library works in for normal operation is determined by
66 * the store that it is given.  The store is an abstraction that
67 * handles the data that the consumer needs to manage between http
68 * requests in order to operate efficiently and securely.
69 *
70 * Several store implementation are provided, and the interface is
71 * fully documented so that custom stores can be used as well.  See
72 * the documentation for the Auth_OpenID_Consumer class for more
73 * information on the interface for stores.  The implementations that
74 * are provided allow the consumer site to store the necessary data in
75 * several different ways, including several SQL databases and normal
76 * files on disk.
77 *
78 * There is an additional concrete store provided that puts the system
79 * in dumb mode.  This is not recommended, as it removes the library's
80 * ability to stop replay attacks reliably.  It still uses time-based
81 * checking to make replay attacks only possible within a small
82 * window, but they remain possible within that window.  This store
83 * should only be used if the consumer site has no way to retain data
84 * between requests at all.
85 *
86 * IMMEDIATE MODE
87 *
88 * In the flow described above, the user may need to confirm to the
89 * lidentity server that it's ok to authorize his or her identity.
90 * The server may draw pages asking for information from the user
91 * before it redirects the browser back to the consumer's site.  This
92 * is generally transparent to the consumer site, so it is typically
93 * ignored as an implementation detail.
94 *
95 * There can be times, however, where the consumer site wants to get a
96 * response immediately.  When this is the case, the consumer can put
97 * the library in immediate mode.  In immediate mode, there is an
98 * extra response possible from the server, which is essentially the
99 * server reporting that it doesn't have enough information to answer
100 * the question yet.
101 *
102 * USING THIS LIBRARY
103 *
104 * Integrating this library into an application is usually a
105 * relatively straightforward process.  The process should basically
106 * follow this plan:
107 *
108 * Add an OpenID login field somewhere on your site.  When an OpenID
109 * is entered in that field and the form is submitted, it should make
110 * a request to the your site which includes that OpenID URL.
111 *
112 * First, the application should instantiate the Auth_OpenID_Consumer
113 * class using the store of choice (Auth_OpenID_FileStore or one of
114 * the SQL-based stores).  If the application has a custom
115 * session-management implementation, an object implementing the
116 * {@link Auth_Yadis_PHPSession} interface should be passed as the
117 * second parameter.  Otherwise, the default uses $_SESSION.
118 *
119 * Next, the application should call the Auth_OpenID_Consumer object's
120 * 'begin' method.  This method takes the OpenID URL.  The 'begin'
121 * method returns an Auth_OpenID_AuthRequest object.
122 *
123 * Next, the application should call the 'redirectURL' method of the
124 * Auth_OpenID_AuthRequest object.  The 'return_to' URL parameter is
125 * the URL that the OpenID server will send the user back to after
126 * attempting to verify his or her identity.  The 'trust_root' is the
127 * URL (or URL pattern) that identifies your web site to the user when
128 * he or she is authorizing it.  Send a redirect to the resulting URL
129 * to the user's browser.
130 *
131 * That's the first half of the authentication process.  The second
132 * half of the process is done after the user's ID server sends the
133 * user's browser a redirect back to your site to complete their
134 * login.
135 *
136 * When that happens, the user will contact your site at the URL given
137 * as the 'return_to' URL to the Auth_OpenID_AuthRequest::redirectURL
138 * call made above.  The request will have several query parameters
139 * added to the URL by the identity server as the information
140 * necessary to finish the request.
141 *
142 * Lastly, instantiate an Auth_OpenID_Consumer instance as above and
143 * call its 'complete' method, passing in all the received query
144 * arguments.
145 *
146 * There are multiple possible return types possible from that
147 * method. These indicate the whether or not the login was successful,
148 * and include any additional information appropriate for their type.
149 *
150 * PHP versions 4 and 5
151 *
152 * LICENSE: See the COPYING file included in this distribution.
153 *
154 * @package OpenID
155 * @author JanRain, Inc. <openid@janrain.com>
156 * @copyright 2005-2008 Janrain, Inc.
157 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
158 */
159
160/**
161 * Require utility classes and functions for the consumer.
162 */
163require_once "Auth/OpenID.php";
164require_once "Auth/OpenID/Message.php";
165require_once "Auth/OpenID/HMAC.php";
166require_once "Auth/OpenID/Association.php";
167require_once "Auth/OpenID/CryptUtil.php";
168require_once "Auth/OpenID/DiffieHellman.php";
169require_once "Auth/OpenID/KVForm.php";
170require_once "Auth/OpenID/Nonce.php";
171require_once "Auth/OpenID/Discover.php";
172require_once "Auth/OpenID/URINorm.php";
173require_once "Auth/Yadis/Manager.php";
174require_once "Auth/Yadis/XRI.php";
175
176/**
177 * This is the status code returned when the complete method returns
178 * successfully.
179 */
180define('Auth_OpenID_SUCCESS', 'success');
181
182/**
183 * Status to indicate cancellation of OpenID authentication.
184 */
185define('Auth_OpenID_CANCEL', 'cancel');
186
187/**
188 * This is the status code completeAuth returns when the value it
189 * received indicated an invalid login.
190 */
191define('Auth_OpenID_FAILURE', 'failure');
192
193/**
194 * This is the status code completeAuth returns when the
195 * {@link Auth_OpenID_Consumer} instance is in immediate mode, and the
196 * identity server sends back a URL to send the user to to complete his
197 * or her login.
198 */
199define('Auth_OpenID_SETUP_NEEDED', 'setup needed');
200
201/**
202 * This is the status code beginAuth returns when the page fetched
203 * from the entered OpenID URL doesn't contain the necessary link tags
204 * to function as an identity page.
205 */
206define('Auth_OpenID_PARSE_ERROR', 'parse error');
207
208/**
209 * An OpenID consumer implementation that performs discovery and does
210 * session management.  See the Consumer.php file documentation for
211 * more information.
212 *
213 * @package OpenID
214 */
215class Auth_OpenID_Consumer {
216
217    /** @var Auth_OpenID_GenericConsumer */
218    public $consumer;
219
220    /** @var Auth_Yadis_PHPSession */
221    public $session;
222
223    private $discoverMethod = 'Auth_OpenID_discover';
224
225    private $session_key_prefix = "_openid_consumer_";
226
227    private $_token_suffix = "last_token";
228
229    /** @var string */
230    private $_token_key;
231
232    /**
233     * Initialize a Consumer instance.
234     *
235     * You should create a new instance of the Consumer object with
236     * every HTTP request that handles OpenID transactions.
237     *
238     * @param Auth_OpenID_OpenIDStore $store This must be an object
239     * that implements the interface in {@link
240     * Auth_OpenID_OpenIDStore}.  Several concrete implementations are
241     * provided, to cover most common use cases.  For stores backed by
242     * MySQL, PostgreSQL, or SQLite, see the {@link
243     * Auth_OpenID_SQLStore} class and its sublcasses.  For a
244     * filesystem-backed store, see the {@link Auth_OpenID_FileStore}
245     * module.  As a last resort, if it isn't possible for the server
246     * to store state at all, an instance of {@link
247     * Auth_OpenID_DumbStore} can be used.
248     *
249     * @param mixed $session An object which implements the interface
250     * of the {@link Auth_Yadis_PHPSession} class.  Particularly, this
251     * object is expected to have these methods: get($key), set($key),
252     * $value), and del($key).  This defaults to a session object
253     * which wraps PHP's native session machinery.  You should only
254     * need to pass something here if you have your own sessioning
255     * implementation.
256     *
257     * @param string $consumer_cls The name of the class to instantiate
258     * when creating the internal consumer object.  This is used for
259     * testing.
260     */
261    function __construct($store, $session = null, $consumer_cls = null)
262    {
263        if ($session === null) {
264            $session = new Auth_Yadis_PHPSession();
265        }
266
267        $this->session = $session;
268
269        if ($consumer_cls !== null) {
270            $this->consumer = new $consumer_cls($store);
271        } else {
272            $this->consumer = new Auth_OpenID_GenericConsumer($store);
273        }
274
275        $this->_token_key = $this->session_key_prefix . $this->_token_suffix;
276    }
277
278    /**
279     * Used in testing to define the discovery mechanism.
280     *
281     * @access private
282     * @param Auth_Yadis_PHPSession $session
283     * @param string $openid_url
284     * @param string $session_key_prefix
285     * @return Auth_Yadis_Discovery
286     */
287    function getDiscoveryObject($session, $openid_url, $session_key_prefix)
288    {
289        return new Auth_Yadis_Discovery($session, $openid_url, $session_key_prefix);
290    }
291
292    /**
293     * Start the OpenID authentication process. See steps 1-2 in the
294     * overview at the top of this file.
295     *
296     * @param string $user_url Identity URL given by the user. This
297     * method performs a textual transformation of the URL to try and
298     * make sure it is normalized. For example, a user_url of
299     * example.com will be normalized to http://example.com/
300     * normalizing and resolving any redirects the server might issue.
301     *
302     * @param bool $anonymous True if the OpenID request is to be sent
303     * to the server without any identifier information.  Use this
304     * when you want to transport data but don't want to do OpenID
305     * authentication with identifiers.
306     *
307     * @return Auth_OpenID_AuthRequest $auth_request An object
308     * containing the discovered information will be returned, with a
309     * method for building a redirect URL to the server, as described
310     * in step 3 of the overview. This object may also be used to add
311     * extension arguments to the request, using its 'addExtensionArg'
312     * method.
313     */
314    function begin($user_url, $anonymous=false)
315    {
316        $openid_url = $user_url;
317
318        $disco = $this->getDiscoveryObject($this->session,
319                                           $openid_url,
320                                           $this->session_key_prefix);
321
322        // Set the 'stale' attribute of the manager.  If discovery
323        // fails in a fatal way, the stale flag will cause the manager
324        // to be cleaned up next time discovery is attempted.
325
326        $m = $disco->getManager();
327        $loader = new Auth_Yadis_ManagerLoader();
328
329        if ($m) {
330            if ($m->stale) {
331                $disco->destroyManager();
332            } else {
333                $m->stale = true;
334                $disco->session->set($disco->session_key,
335                                     serialize($loader->toSession($m)));
336            }
337        }
338
339        $endpoint = $disco->getNextService($this->discoverMethod,
340                                           $this->consumer->fetcher);
341
342        // Reset the 'stale' attribute of the manager.
343        $m = $disco->getManager();
344        if ($m) {
345            $m->stale = false;
346            $disco->session->set($disco->session_key,
347                                 serialize($loader->toSession($m)));
348        }
349
350        if ($endpoint === null) {
351            return null;
352        } else {
353            return $this->beginWithoutDiscovery($endpoint,
354                                                $anonymous);
355        }
356    }
357
358    /**
359     * Start OpenID verification without doing OpenID server
360     * discovery. This method is used internally by Consumer.begin
361     * after discovery is performed, and exists to provide an
362     * interface for library users needing to perform their own
363     * discovery.
364     *
365     * @param Auth_OpenID_ServiceEndpoint $endpoint an OpenID service
366     * endpoint descriptor.
367     *
368     * @param bool $anonymous Set to true if you want to perform OpenID
369     * without identifiers.
370     *
371     * @return Auth_OpenID_AuthRequest|Auth_OpenID_FailureResponse $auth_request An OpenID
372     * authentication request object.
373     */
374    function beginWithoutDiscovery($endpoint, $anonymous=false)
375    {
376        $loader = new Auth_OpenID_ServiceEndpointLoader();
377        $auth_req = $this->consumer->begin($endpoint);
378        $this->session->set($this->_token_key,
379              $loader->toSession($auth_req->endpoint));
380        if (!$auth_req->setAnonymous($anonymous)) {
381            return new Auth_OpenID_FailureResponse(null,
382              "OpenID 1 requests MUST include the identifier " .
383              "in the request.");
384        }
385        return $auth_req;
386    }
387
388    /**
389     * Called to interpret the server's response to an OpenID
390     * request. It is called in step 4 of the flow described in the
391     * consumer overview.
392     *
393     * @param string $current_url The URL used to invoke the application.
394     * Extract the URL from your application's web
395     * request framework and specify it here to have it checked
396     * against the openid.current_url value in the response.  If
397     * the current_url URL check fails, the status of the
398     * completion will be FAILURE.
399     *
400     * @param array $query An array of the query parameters (key =>
401     * value pairs) for this HTTP request.  Defaults to null.  If
402     * null, the GET or POST data are automatically gotten from the
403     * PHP environment.  It is only useful to override $query for
404     * testing.
405     *
406     * @return Auth_OpenID_ConsumerResponse $response A instance of an
407     * Auth_OpenID_ConsumerResponse subclass. The type of response is
408     * indicated by the status attribute, which will be one of
409     * SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED.
410     */
411    function complete($current_url, $query=null)
412    {
413        if ($current_url && !is_string($current_url)) {
414            // This is ugly, but we need to complain loudly when
415            // someone uses the API incorrectly.
416            trigger_error("current_url must be a string; see NEWS file " .
417                          "for upgrading notes.",
418                          E_USER_ERROR);
419        }
420
421        if ($query === null) {
422            $query = Auth_OpenID::getQuery();
423        }
424
425        $loader = new Auth_OpenID_ServiceEndpointLoader();
426        $endpoint_data = $this->session->get($this->_token_key);
427        $endpoint =
428            $loader->fromSession($endpoint_data);
429
430        $message = Auth_OpenID_Message::fromPostArgs($query);
431        $response = $this->consumer->complete($message, $endpoint,
432                                              $current_url);
433        $this->session->del($this->_token_key);
434
435        if (in_array($response->status, [Auth_OpenID_SUCCESS, Auth_OpenID_CANCEL])) {
436            if ($response->identity_url !== null) {
437                $disco = $this->getDiscoveryObject($this->session,
438                                                   $response->identity_url,
439                                                   $this->session_key_prefix);
440                $disco->cleanup(true);
441            }
442        }
443
444        return $response;
445    }
446}
447
448/**
449 * A class implementing HMAC/DH-SHA1 consumer sessions.
450 *
451 * @package OpenID
452 */
453class Auth_OpenID_DiffieHellmanSHA1ConsumerSession {
454    public $session_type = 'DH-SHA1';
455    public $hash_func = 'Auth_OpenID_SHA1';
456    public $secret_size = 20;
457    public $allowed_assoc_types = ['HMAC-SHA1'];
458
459    /** @var Auth_OpenID_DiffieHellman */
460    protected $dh;
461
462    function __construct($dh = null)
463    {
464        if ($dh === null) {
465            $dh = new Auth_OpenID_DiffieHellman();
466        }
467
468        $this->dh = $dh;
469    }
470
471    function getRequest()
472    {
473        $math = Auth_OpenID_getMathLib();
474
475        $cpub = $math->longToBase64($this->dh->public);
476
477        $args = ['dh_consumer_public' => $cpub];
478
479        if (!$this->dh->usingDefaultValues()) {
480            $args = array_merge($args, [
481                'dh_modulus' =>
482                     $math->longToBase64($this->dh->mod),
483                'dh_gen' =>
484                     $math->longToBase64($this->dh->gen)
485            ]);
486        }
487
488        return $args;
489    }
490
491    /**
492     * @param Auth_OpenID_Message $response
493     * @return null|string
494     */
495    function extractSecret($response)
496    {
497        if (!$response->hasKey(Auth_OpenID_OPENID_NS,
498                               'dh_server_public')) {
499            return null;
500        }
501
502        if (!$response->hasKey(Auth_OpenID_OPENID_NS,
503                               'enc_mac_key')) {
504            return null;
505        }
506
507        $math = Auth_OpenID_getMathLib();
508
509        $spub = $math->base64ToLong($response->getArg(Auth_OpenID_OPENID_NS,
510                                                      'dh_server_public'));
511        $enc_mac_key = base64_decode($response->getArg(Auth_OpenID_OPENID_NS,
512                                                       'enc_mac_key'));
513
514        return $this->dh->xorSecret($spub, $enc_mac_key, $this->hash_func);
515    }
516}
517
518/**
519 * A class implementing HMAC/DH-SHA256 consumer sessions.
520 *
521 * @package OpenID
522 */
523class Auth_OpenID_DiffieHellmanSHA256ConsumerSession extends
524      Auth_OpenID_DiffieHellmanSHA1ConsumerSession {
525    public $session_type = 'DH-SHA256';
526    public $hash_func = 'Auth_OpenID_SHA256';
527    public $secret_size = 32;
528    public $allowed_assoc_types = ['HMAC-SHA256'];
529}
530
531/**
532 * A class implementing plaintext consumer sessions.
533 *
534 * @package OpenID
535 */
536class Auth_OpenID_PlainTextConsumerSession {
537    public $session_type = 'no-encryption';
538    public $allowed_assoc_types =  ['HMAC-SHA1', 'HMAC-SHA256'];
539
540    function getRequest()
541    {
542        return [];
543    }
544
545    /**
546     * @param Auth_OpenID_Message $response
547     * @return bool|null|string
548     */
549    function extractSecret($response)
550    {
551        if (!$response->hasKey(Auth_OpenID_OPENID_NS, 'mac_key')) {
552            return null;
553        }
554
555        return base64_decode($response->getArg(Auth_OpenID_OPENID_NS,
556                                               'mac_key'));
557    }
558}
559
560/**
561 * Returns available session types.
562 */
563function Auth_OpenID_getAvailableSessionTypes()
564{
565    $types = [
566      'no-encryption' => 'Auth_OpenID_PlainTextConsumerSession',
567      'DH-SHA1' => 'Auth_OpenID_DiffieHellmanSHA1ConsumerSession',
568      'DH-SHA256' => 'Auth_OpenID_DiffieHellmanSHA256ConsumerSession'
569    ];
570
571    return $types;
572}
573
574/**
575 * This class is the interface to the OpenID consumer logic.
576 * Instances of it maintain no per-request state, so they can be
577 * reused (or even used by multiple threads concurrently) as needed.
578 *
579 * @package OpenID
580 */
581class Auth_OpenID_GenericConsumer {
582    /**
583     * @access private
584     */
585    public $discoverMethod = 'Auth_OpenID_discover';
586
587    /**
588     * This consumer's store object.
589     */
590    public $store;
591
592    /**
593     * @access private
594     */
595    public $_use_assocs;
596
597    /**
598     * @access private
599     */
600    public $openid1_nonce_query_arg_name = 'janrain_nonce';
601
602    /**
603     * Another query parameter that gets added to the return_to for
604     * OpenID 1; if the user's session state is lost, use this claimed
605     * identifier to do discovery when verifying the response.
606     */
607    public $openid1_return_to_identifier_name = 'openid1_claimed_id';
608
609    /** @var Auth_Yadis_ParanoidHTTPFetcher|Auth_Yadis_PlainHTTPFetcher  */
610    public $fetcher;
611
612    /** @var array */
613    public $session_types;
614
615    /** @var Auth_OpenID_SessionNegotiator */
616    public $negotiator;
617
618    /**
619     * This method initializes a new {@link Auth_OpenID_Consumer}
620     * instance to access the library.
621     *
622     * @param Auth_OpenID_OpenIDStore $store This must be an object
623     * that implements the interface in {@link Auth_OpenID_OpenIDStore}.
624     * Several concrete implementations are provided, to cover most common use
625     * cases.  For stores backed by MySQL, PostgreSQL, or SQLite, see
626     * the {@link Auth_OpenID_SQLStore} class and its sublcasses.  For a
627     * filesystem-backed store, see the {@link Auth_OpenID_FileStore} module.
628     * As a last resort, if it isn't possible for the server to store
629     * state at all, an instance of {@link Auth_OpenID_DumbStore} can be used.
630     */
631    function __construct($store)
632    {
633        $this->store = $store;
634        $this->negotiator = Auth_OpenID_getDefaultNegotiator();
635        $this->_use_assocs = (is_null($this->store) ? false : true);
636        if (get_class($this->store) == "Auth_OpenID_DumbStore") {
637            $this->_use_assocs = false;
638        }
639
640        $this->fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
641
642        $this->session_types = Auth_OpenID_getAvailableSessionTypes();
643    }
644
645    /**
646     * Called to begin OpenID authentication using the specified
647     * {@link Auth_OpenID_ServiceEndpoint}.
648     *
649     * @access private
650     * @param Auth_OpenID_ServiceEndpoint $service_endpoint
651     * @return Auth_OpenID_AuthRequest
652     */
653    function begin($service_endpoint)
654    {
655        $assoc = $this->_getAssociation($service_endpoint);
656        $r = new Auth_OpenID_AuthRequest($service_endpoint, $assoc);
657        $r->return_to_args[$this->openid1_nonce_query_arg_name] =
658            Auth_OpenID_mkNonce();
659
660        if ($r->message->isOpenID1()) {
661            $r->return_to_args[$this->openid1_return_to_identifier_name] =
662                $r->endpoint->claimed_id;
663        }
664
665        return $r;
666    }
667
668    /**
669     * Given an {@link Auth_OpenID_Message}, {@link
670     * Auth_OpenID_ServiceEndpoint} and optional return_to URL,
671     * complete OpenID authentication.
672     *
673     * @access private
674     * @param Auth_OpenID_Message $message
675     * @param Auth_OpenID_ServiceEndpoint $endpoint
676     * @param string $return_to
677     * @return Auth_OpenID_SuccessResponse
678     */
679    function complete($message, $endpoint, $return_to)
680    {
681        $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode',
682                                 '<no mode set>');
683
684        $mode_methods = [
685                              'cancel' => '_complete_cancel',
686                              'error' => '_complete_error',
687                              'setup_needed' => '_complete_setup_needed',
688                              'id_res' => '_complete_id_res',
689        ];
690
691        $method = Auth_OpenID::arrayGet($mode_methods, $mode,
692                                        '_completeInvalid');
693
694        return call_user_func_array([$this, $method],
695                                    [$message, $endpoint, $return_to]);
696    }
697
698    /**
699     * @access private
700     * @param Auth_OpenID_Message $message
701     * @param Auth_OpenID_ServiceEndpoint $endpoint
702     * @return Auth_OpenID_FailureResponse
703     */
704    function _completeInvalid($message, $endpoint)
705    {
706        $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode',
707                                 '<No mode set>');
708
709        return new Auth_OpenID_FailureResponse($endpoint,
710                    sprintf("Invalid openid.mode '%s'", $mode));
711    }
712
713    /**
714     * @access private
715     * @param Auth_OpenID_Message $message
716     * @param Auth_OpenID_ServiceEndpoint $endpoint
717     * @return Auth_OpenID_CancelResponse
718     */
719    function _complete_cancel($message, $endpoint)
720    {
721        return new Auth_OpenID_CancelResponse($endpoint);
722    }
723
724    /**
725     * @access private
726     * @param Auth_OpenID_Message $message
727     * @param Auth_OpenID_ServiceEndpoint $endpoint
728     * @return Auth_OpenID_FailureResponse
729     */
730    function _complete_error($message, $endpoint)
731    {
732        $error = $message->getArg(Auth_OpenID_OPENID_NS, 'error');
733        $contact = $message->getArg(Auth_OpenID_OPENID_NS, 'contact');
734        $reference = $message->getArg(Auth_OpenID_OPENID_NS, 'reference');
735
736        return new Auth_OpenID_FailureResponse($endpoint, $error,
737                                               $contact, $reference);
738    }
739
740    /**
741     * @access private
742     * @param Auth_OpenID_Message $message
743     * @param Auth_OpenID_ServiceEndpoint $endpoint
744     * @return Auth_OpenID_FailureResponse|Auth_OpenID_SetupNeededResponse
745     */
746    function _complete_setup_needed($message, $endpoint)
747    {
748        if (!$message->isOpenID2()) {
749            return $this->_completeInvalid($message, $endpoint);
750        }
751
752        $user_setup_url = $message->getArg(Auth_OpenID_OPENID2_NS,
753                                           'user_setup_url');
754        return new Auth_OpenID_SetupNeededResponse($endpoint, $user_setup_url);
755    }
756
757    /**
758     * @access private
759     * @param Auth_OpenID_Message $message
760     * @param Auth_OpenID_ServiceEndpoint $endpoint
761     * @param string $return_to
762     * @return Auth_OpenID_FailureResponse|Auth_OpenID_SetupNeededResponse|Auth_OpenID_SuccessResponse|null
763     */
764    function _complete_id_res($message, $endpoint, $return_to)
765    {
766        $user_setup_url = $message->getArg(Auth_OpenID_OPENID1_NS, 'user_setup_url');
767
768        if ($this->_checkSetupNeeded($message)) {
769            return new Auth_OpenID_SetupNeededResponse($endpoint, $user_setup_url);
770        } else {
771            return $this->_doIdRes($message, $endpoint, $return_to);
772        }
773    }
774
775    /**
776     * @access private
777     * @param Auth_OpenID_Message $message
778     * @return bool
779     */
780    function _checkSetupNeeded($message)
781    {
782        // In OpenID 1, we check to see if this is a cancel from
783        // immediate mode by the presence of the user_setup_url
784        // parameter.
785        if ($message->isOpenID1()) {
786            $user_setup_url = $message->getArg(Auth_OpenID_OPENID1_NS,
787                                               'user_setup_url');
788            if ($user_setup_url !== null) {
789                return true;
790            }
791        }
792
793        return false;
794    }
795
796    /**
797     * @access private
798     * @param Auth_OpenID_Message $message
799     * @param Auth_OpenID_ServiceEndpoint $endpoint
800     * @param string $return_to
801     * @return Auth_OpenID_FailureResponse|Auth_OpenID_SuccessResponse|mixed|null|string
802     */
803    function _doIdRes($message, $endpoint, $return_to)
804    {
805        // Checks for presence of appropriate fields (and checks
806        // signed list fields)
807        $result = $this->_idResCheckForFields($message);
808
809        if (Auth_OpenID::isFailure($result)) {
810            return $result;
811        }
812
813        if (!$this->_checkReturnTo($message, $return_to)) {
814            return new Auth_OpenID_FailureResponse(null,
815            sprintf("return_to does not match return URL. Expected %s, got %s",
816                    $return_to,
817                    $message->getArg(Auth_OpenID_OPENID_NS, 'return_to')));
818        }
819
820        // Verify discovery information:
821        $result = $this->_verifyDiscoveryResults($message, $endpoint);
822
823        if (Auth_OpenID::isFailure($result)) {
824            return $result;
825        }
826
827        $endpoint = $result;
828
829        $result = $this->_idResCheckSignature($message, $endpoint->server_url);
830
831        if (Auth_OpenID::isFailure($result)) {
832            return $result;
833        }
834
835        $result = $this->_idResCheckNonce($message, $endpoint);
836
837        if (Auth_OpenID::isFailure($result)) {
838            return $result;
839        }
840
841        $signed_list_str = $message->getArg(Auth_OpenID_OPENID_NS, 'signed',
842                                            Auth_OpenID_NO_DEFAULT);
843        if (Auth_OpenID::isFailure($signed_list_str)) {
844            return $signed_list_str;
845        }
846        $signed_list = explode(',', $signed_list_str);
847
848        $signed_fields = Auth_OpenID::addPrefix($signed_list, "openid.");
849
850        return new Auth_OpenID_SuccessResponse($endpoint, $message,
851                                               $signed_fields);
852
853    }
854
855    /**
856     * @access private
857     * @param Auth_OpenID_Message $message
858     * @param string $return_to
859     * @return bool
860     */
861    function _checkReturnTo($message, $return_to)
862    {
863        // Check an OpenID message and its openid.return_to value
864        // against a return_to URL from an application.  Return True
865        // on success, False on failure.
866
867        // Check the openid.return_to args against args in the
868        // original message.
869        $result = Auth_OpenID_GenericConsumer::_verifyReturnToArgs(
870                                           $message->toPostArgs());
871        if (Auth_OpenID::isFailure($result)) {
872            return false;
873        }
874
875        // Check the return_to base URL against the one in the
876        // message.
877        $msg_return_to = $message->getArg(Auth_OpenID_OPENID_NS,
878                                          'return_to');
879        if (Auth_OpenID::isFailure($return_to)) {
880            // XXX log me
881            return false;
882        }
883
884        $return_to_parts = parse_url(Auth_OpenID_urinorm($return_to));
885        $msg_return_to_parts = parse_url(Auth_OpenID_urinorm($msg_return_to));
886
887        // If port is absent from both, add it so it's equal in the
888        // check below.
889        if ((!array_key_exists('port', $return_to_parts)) &&
890            (!array_key_exists('port', $msg_return_to_parts))) {
891            $return_to_parts['port'] = null;
892            $msg_return_to_parts['port'] = null;
893        }
894
895        // If path is absent from both, add it so it's equal in the
896        // check below.
897        if ((!array_key_exists('path', $return_to_parts)) &&
898            (!array_key_exists('path', $msg_return_to_parts))) {
899            $return_to_parts['path'] = null;
900            $msg_return_to_parts['path'] = null;
901        }
902
903        // The URL scheme, authority, and path MUST be the same
904        // between the two URLs.
905        foreach (['scheme', 'host', 'port', 'path'] as $component) {
906            // If the url component is absent in either URL, fail.
907            // There should always be a scheme, host, port, and path.
908            if (!array_key_exists($component, $return_to_parts)) {
909                return false;
910            }
911
912            if (!array_key_exists($component, $msg_return_to_parts)) {
913                return false;
914            }
915
916            if (Auth_OpenID::arrayGet($return_to_parts, $component) !==
917                Auth_OpenID::arrayGet($msg_return_to_parts, $component)) {
918                return false;
919            }
920        }
921
922        return true;
923    }
924
925    /**
926     * @access private
927     * @param array $query
928     * @return Auth_OpenID_FailureResponse|bool
929     */
930    function _verifyReturnToArgs($query)
931    {
932        // Verify that the arguments in the return_to URL are present in this
933        // response.
934
935        $message = Auth_OpenID_Message::fromPostArgs($query);
936        $return_to = $message->getArg(Auth_OpenID_OPENID_NS, 'return_to');
937
938        if (Auth_OpenID::isFailure($return_to)) {
939            return $return_to;
940        }
941        // XXX: this should be checked by _idResCheckForFields
942        if (!$return_to) {
943            return new Auth_OpenID_FailureResponse(null,
944                           "Response has no return_to");
945        }
946
947        $parsed_url = parse_url($return_to);
948
949        $q = [];
950        if (array_key_exists('query', $parsed_url)) {
951            $rt_query = $parsed_url['query'];
952            $q = Auth_OpenID::parse_str($rt_query);
953        }
954
955        foreach ($q as $rt_key => $rt_value) {
956            if (!array_key_exists($rt_key, $query)) {
957                return new Auth_OpenID_FailureResponse(null,
958                  sprintf("return_to parameter %s absent from query", $rt_key));
959            } else {
960                $value = $query[$rt_key];
961                if ($rt_value != $value) {
962                    return new Auth_OpenID_FailureResponse(null,
963                      sprintf("parameter %s value %s does not match " .
964                              "return_to value %s", $rt_key,
965                              $value, $rt_value));
966                }
967            }
968        }
969
970        // Make sure all non-OpenID arguments in the response are also
971        // in the signed return_to.
972        $bare_args = $message->getArgs(Auth_OpenID_BARE_NS);
973        foreach ($bare_args as $key => $value) {
974            if (Auth_OpenID::arrayGet($q, $key) != $value) {
975                return new Auth_OpenID_FailureResponse(null,
976                  sprintf("Parameter %s = %s not in return_to URL",
977                          $key, $value));
978            }
979        }
980
981        return true;
982    }
983
984    /**
985     * @access private
986     * @param Auth_OpenID_Message $message
987     * @param string $server_url
988     * @return Auth_OpenID_FailureResponse|null
989     */
990    function _idResCheckSignature($message, $server_url)
991    {
992        $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS,
993                                         'assoc_handle');
994        if (Auth_OpenID::isFailure($assoc_handle)) {
995            return $assoc_handle;
996        }
997
998        $assoc = $this->store->getAssociation($server_url, $assoc_handle);
999
1000        if ($assoc) {
1001            if ($assoc->getExpiresIn() <= 0) {
1002                // XXX: It might be a good idea sometimes to re-start
1003                // the authentication with a new association. Doing it
1004                // automatically opens the possibility for
1005                // denial-of-service by a server that just returns
1006                // expired associations (or really short-lived
1007                // associations)
1008                return new Auth_OpenID_FailureResponse(null,
1009                             'Association with ' . $server_url . ' expired');
1010            }
1011
1012            if (!$assoc->checkMessageSignature($message)) {
1013                // If we get a "bad signature" here, it means that the association
1014                // is unrecoverabley corrupted in some way. Any futher attempts
1015                // to login with this association is likely to fail. Drop it.
1016                $this->store->removeAssociation($server_url, $assoc_handle);
1017                return new Auth_OpenID_FailureResponse(null,
1018                                                       "Bad signature");
1019            }
1020        } else {
1021            // It's not an association we know about.  Stateless mode
1022            // is our only possible path for recovery.  XXX - async
1023            // framework will not want to block on this call to
1024            // _checkAuth.
1025            if (!$this->_checkAuth($message, $server_url)) {
1026                return new Auth_OpenID_FailureResponse(null,
1027                             "Server denied check_authentication");
1028            }
1029        }
1030
1031        return null;
1032    }
1033
1034    /**
1035     * @access private
1036     * @param Auth_OpenID_Message $message
1037     * @param Auth_OpenID_ServiceEndpoint|null $endpoint
1038     * @return Auth_OpenID_FailureResponse|Auth_OpenID_ServiceEndpoint
1039     */
1040    function _verifyDiscoveryResults($message, $endpoint=null)
1041    {
1042        if ($message->getOpenIDNamespace() == Auth_OpenID_OPENID2_NS) {
1043            return $this->_verifyDiscoveryResultsOpenID2($message, $endpoint);
1044        } else {
1045            return $this->_verifyDiscoveryResultsOpenID1($message, $endpoint);
1046        }
1047    }
1048
1049    /**
1050     * @access private
1051     * @param Auth_OpenID_Message $message
1052     * @param Auth_OpenID_ServiceEndpoint $endpoint
1053     * @return Auth_OpenID_FailureResponse|Auth_OpenID_ServiceEndpoint
1054     */
1055    function _verifyDiscoveryResultsOpenID1($message, $endpoint)
1056    {
1057        $claimed_id = $message->getArg(Auth_OpenID_BARE_NS,
1058                                $this->openid1_return_to_identifier_name);
1059
1060        if (($endpoint === null) && ($claimed_id === null)) {
1061            return new Auth_OpenID_FailureResponse($endpoint,
1062              'When using OpenID 1, the claimed ID must be supplied, ' .
1063              'either by passing it through as a return_to parameter ' .
1064              'or by using a session, and supplied to the GenericConsumer ' .
1065              'as the argument to complete()');
1066        } else if (($endpoint !== null) && ($claimed_id === null)) {
1067            $claimed_id = $endpoint->claimed_id;
1068        }
1069
1070        $to_match = new Auth_OpenID_ServiceEndpoint();
1071        $to_match->type_uris = [Auth_OpenID_TYPE_1_1];
1072        $to_match->local_id = $message->getArg(Auth_OpenID_OPENID1_NS,
1073                                               'identity');
1074
1075        // Restore delegate information from the initiation phase
1076        $to_match->claimed_id = $claimed_id;
1077
1078        if ($to_match->local_id === null) {
1079            return new Auth_OpenID_FailureResponse($endpoint,
1080                         "Missing required field openid.identity");
1081        }
1082
1083        $to_match_1_0 = $to_match->copy();
1084        $to_match_1_0->type_uris = [Auth_OpenID_TYPE_1_0];
1085
1086        if ($endpoint !== null) {
1087            $result = $this->_verifyDiscoverySingle($endpoint, $to_match);
1088
1089            if (is_a($result, 'Auth_OpenID_TypeURIMismatch')) {
1090                $result = $this->_verifyDiscoverySingle($endpoint,
1091                                                        $to_match_1_0);
1092            }
1093
1094            if (Auth_OpenID::isFailure($result)) {
1095                // oidutil.log("Error attempting to use stored
1096                //             discovery information: " + str(e))
1097                //             oidutil.log("Attempting discovery to
1098                //             verify endpoint")
1099            } else {
1100                return $endpoint;
1101            }
1102        }
1103
1104        // Endpoint is either bad (failed verification) or None
1105        return $this->_discoverAndVerify($to_match->claimed_id,
1106                                         [$to_match, $to_match_1_0]);
1107    }
1108
1109    /**
1110     * @access private
1111     * @param Auth_OpenID_ServiceEndpoint $endpoint
1112     * @param Auth_OpenID_ServiceEndpoint $to_match
1113     * @return Auth_OpenID_FailureResponse|null
1114     */
1115    function _verifyDiscoverySingle($endpoint, $to_match)
1116    {
1117        // Every type URI that's in the to_match endpoint has to be
1118        // present in the discovered endpoint.
1119        foreach ($to_match->type_uris as $type_uri) {
1120            if (!$endpoint->usesExtension($type_uri)) {
1121                return new Auth_OpenID_TypeURIMismatch($endpoint,
1122                             "Required type ".$type_uri." not present");
1123            }
1124        }
1125
1126        // Fragments do not influence discovery, so we can't compare a
1127        // claimed identifier with a fragment to discovered
1128        // information.
1129        list($defragged_claimed_id) = Auth_OpenID::urldefrag($to_match->claimed_id);
1130
1131        if ($defragged_claimed_id != $endpoint->claimed_id) {
1132            return new Auth_OpenID_FailureResponse($endpoint,
1133              sprintf('Claimed ID does not match (different subjects!), ' .
1134                      'Expected %s, got %s', $defragged_claimed_id,
1135                      $endpoint->claimed_id));
1136        }
1137
1138        if ($to_match->getLocalID() != $endpoint->getLocalID()) {
1139            return new Auth_OpenID_FailureResponse($endpoint,
1140              sprintf('local_id mismatch. Expected %s, got %s',
1141                      $to_match->getLocalID(), $endpoint->getLocalID()));
1142        }
1143
1144        // If the server URL is None, this must be an OpenID 1
1145        // response, because op_endpoint is a required parameter in
1146        // OpenID 2. In that case, we don't actually care what the
1147        // discovered server_url is, because signature checking or
1148        // check_auth should take care of that check for us.
1149        if ($to_match->server_url === null) {
1150            if ($to_match->preferredNamespace() != Auth_OpenID_OPENID1_NS) {
1151                return new Auth_OpenID_FailureResponse($endpoint,
1152                             "Preferred namespace mismatch (bug)");
1153            }
1154        } else if ($to_match->server_url != $endpoint->server_url) {
1155            return new Auth_OpenID_FailureResponse($endpoint,
1156              sprintf('OP Endpoint mismatch. Expected %s, got %s',
1157                      $to_match->server_url, $endpoint->server_url));
1158        }
1159
1160        return null;
1161    }
1162
1163    /**
1164     * @access private
1165     * @param Auth_OpenID_Message $message
1166     * @param Auth_OpenID_ServiceEndpoint $endpoint
1167     * @return Auth_OpenID_FailureResponse|Auth_OpenID_ServiceEndpoint
1168     */
1169    function _verifyDiscoveryResultsOpenID2($message, $endpoint)
1170    {
1171        $to_match = new Auth_OpenID_ServiceEndpoint();
1172        $to_match->type_uris = [Auth_OpenID_TYPE_2_0];
1173        $to_match->claimed_id = $message->getArg(Auth_OpenID_OPENID2_NS,
1174                                                 'claimed_id');
1175
1176        $to_match->local_id = $message->getArg(Auth_OpenID_OPENID2_NS,
1177                                                'identity');
1178
1179        $to_match->server_url = $message->getArg(Auth_OpenID_OPENID2_NS,
1180                                                 'op_endpoint');
1181
1182        if ($to_match->server_url === null) {
1183            return new Auth_OpenID_FailureResponse($endpoint,
1184                         "OP Endpoint URL missing");
1185        }
1186
1187        // claimed_id and identifier must both be present or both be
1188        // absent
1189        if (($to_match->claimed_id === null) &&
1190            ($to_match->local_id !== null)) {
1191            return new Auth_OpenID_FailureResponse($endpoint,
1192              'openid.identity is present without openid.claimed_id');
1193        }
1194
1195        if (($to_match->claimed_id !== null) &&
1196            ($to_match->local_id === null)) {
1197            return new Auth_OpenID_FailureResponse($endpoint,
1198              'openid.claimed_id is present without openid.identity');
1199        }
1200
1201        if ($to_match->claimed_id === null) {
1202            // This is a response without identifiers, so there's
1203            // really no checking that we can do, so return an
1204            // endpoint that's for the specified `openid.op_endpoint'
1205            return Auth_OpenID_ServiceEndpoint::fromOPEndpointURL(
1206                                                $to_match->server_url);
1207        }
1208
1209        if (!$endpoint) {
1210            // The claimed ID doesn't match, so we have to do
1211            // discovery again. This covers not using sessions, OP
1212            // identifier endpoints and responses that didn't match
1213            // the original request.
1214            // oidutil.log('No pre-discovered information supplied.')
1215            return $this->_discoverAndVerify($to_match->claimed_id,
1216                                             [$to_match]);
1217        } else {
1218
1219            // The claimed ID matches, so we use the endpoint that we
1220            // discovered in initiation. This should be the most
1221            // common case.
1222            $result = $this->_verifyDiscoverySingle($endpoint, $to_match);
1223
1224            if (Auth_OpenID::isFailure($result)) {
1225                $endpoint = $this->_discoverAndVerify($to_match->claimed_id,
1226                                                      [$to_match]);
1227                if (Auth_OpenID::isFailure($endpoint)) {
1228                    return $endpoint;
1229                }
1230            }
1231        }
1232
1233        // The endpoint we return should have the claimed ID from the
1234        // message we just verified, fragment and all.
1235        if ($endpoint->claimed_id != $to_match->claimed_id) {
1236            $endpoint->claimed_id = $to_match->claimed_id;
1237        }
1238
1239        return $endpoint;
1240    }
1241
1242    /**
1243     * @access private
1244     * @param string $claimed_id
1245     * @param Auth_OpenID_ServiceEndpoint[] $to_match_endpoints
1246     * @return Auth_OpenID_FailureResponse
1247     */
1248    function _discoverAndVerify($claimed_id, $to_match_endpoints)
1249    {
1250        // oidutil.log('Performing discovery on %s' % (claimed_id,))
1251        list(, $services) = call_user_func_array($this->discoverMethod,
1252                                                        [
1253                                                            $claimed_id,
1254                                                            $this->fetcher,
1255                                                        ]);
1256
1257        if (!$services) {
1258            return new Auth_OpenID_FailureResponse(null,
1259              sprintf("No OpenID information found at %s",
1260                      $claimed_id));
1261        }
1262
1263        return $this->_verifyDiscoveryServices($claimed_id, $services,
1264                                               $to_match_endpoints);
1265    }
1266
1267    /**
1268     * @access private
1269     * @param string $claimed_id
1270     * @param Auth_OpenID_ServiceEndpoint[] $services
1271     * @param Auth_OpenID_ServiceEndpoint[] $to_match_endpoints
1272     * @return Auth_OpenID_FailureResponse|Auth_OpenID_ServiceEndpoint
1273     */
1274    function _verifyDiscoveryServices($claimed_id,
1275                                      $services, $to_match_endpoints)
1276    {
1277        // Search the services resulting from discovery to find one
1278        // that matches the information from the assertion
1279
1280        $result = null;
1281        foreach ($services as $endpoint) {
1282            foreach ($to_match_endpoints as $to_match_endpoint) {
1283                $result = $this->_verifyDiscoverySingle($endpoint, $to_match_endpoint);
1284
1285                if (!Auth_OpenID::isFailure($result)) {
1286                    // It matches, so discover verification has
1287                    // succeeded. Return this endpoint.
1288                    return $endpoint;
1289                }
1290            }
1291        }
1292
1293        $message = $result instanceof Auth_OpenID_FailureResponse ? $result->message : '';
1294
1295        return new Auth_OpenID_FailureResponse(null,
1296          sprintf('No matching endpoint found after discovering %s: %s', $claimed_id, $message));
1297    }
1298
1299    /**
1300     * Extract the nonce from an OpenID 1 response.  Return the nonce
1301     * from the BARE_NS since we independently check the return_to
1302     * arguments are the same as those in the response message.
1303     *
1304     * See the openid1_nonce_query_arg_name class variable
1305     *
1306     * @param Auth_OpenID_Message $message
1307     * @return string The nonce as a string or null
1308     *
1309     * @access private
1310     */
1311    function _idResGetNonceOpenID1($message)
1312    {
1313        return $message->getArg(Auth_OpenID_BARE_NS, $this->openid1_nonce_query_arg_name);
1314    }
1315
1316    /**
1317     * @access private
1318     * @param Auth_OpenID_Message $message
1319     * @param Auth_OpenID_ServiceEndpoint $endpoint
1320     * @return Auth_OpenID_FailureResponse|null
1321     */
1322    function _idResCheckNonce($message, $endpoint)
1323    {
1324        if ($message->isOpenID1()) {
1325            // This indicates that the nonce was generated by the consumer
1326            $nonce = $this->_idResGetNonceOpenID1($message);
1327            $server_url = '';
1328        } else {
1329            $nonce = $message->getArg(Auth_OpenID_OPENID2_NS,
1330                                      'response_nonce');
1331
1332            $server_url = $endpoint->server_url;
1333        }
1334
1335        if ($nonce === null) {
1336            return new Auth_OpenID_FailureResponse($endpoint,
1337                                     "Nonce missing from response");
1338        }
1339
1340        $parts = Auth_OpenID_splitNonce($nonce);
1341
1342        if ($parts === null) {
1343            return new Auth_OpenID_FailureResponse($endpoint,
1344                                     "Malformed nonce in response");
1345        }
1346
1347        list($timestamp, $salt) = $parts;
1348
1349        if (!$this->store->useNonce($server_url, $timestamp, $salt)) {
1350            return new Auth_OpenID_FailureResponse($endpoint,
1351                         "Nonce already used or out of range");
1352        }
1353
1354        return null;
1355    }
1356
1357    /**
1358     * @access private
1359     * @param Auth_OpenID_Message $message
1360     * @return Auth_OpenID_FailureResponse|mixed|null|string
1361     */
1362    function _idResCheckForFields($message)
1363    {
1364        $basic_fields = ['return_to', 'assoc_handle', 'sig', 'signed'];
1365        $basic_sig_fields = ['return_to', 'identity'];
1366
1367        $require_fields = [
1368            Auth_OpenID_OPENID2_NS => array_merge($basic_fields,
1369                                                  ['op_endpoint']),
1370
1371            Auth_OpenID_OPENID1_NS => array_merge($basic_fields,
1372                                                  ['identity'])
1373        ];
1374
1375        $require_sigs = [
1376            Auth_OpenID_OPENID2_NS => array_merge($basic_sig_fields,
1377                                                  [
1378                                                      'response_nonce',
1379                                                        'claimed_id',
1380                                                        'assoc_handle',
1381                                                        'op_endpoint'
1382                                                  ]),
1383            Auth_OpenID_OPENID1_NS => array_merge($basic_sig_fields,
1384                                                  ['nonce'])
1385        ];
1386
1387        foreach ($require_fields[$message->getOpenIDNamespace()] as $field) {
1388            if (!$message->hasKey(Auth_OpenID_OPENID_NS, $field)) {
1389                return new Auth_OpenID_FailureResponse(null,
1390                             "Missing required field '".$field."'");
1391            }
1392        }
1393
1394        $signed_list_str = $message->getArg(Auth_OpenID_OPENID_NS,
1395                                            'signed',
1396                                            Auth_OpenID_NO_DEFAULT);
1397        if (Auth_OpenID::isFailure($signed_list_str)) {
1398            return $signed_list_str;
1399        }
1400        $signed_list = explode(',', $signed_list_str);
1401
1402        foreach ($require_sigs[$message->getOpenIDNamespace()] as $field) {
1403            // Field is present and not in signed list
1404            if ($message->hasKey(Auth_OpenID_OPENID_NS, $field) &&
1405                (!in_array($field, $signed_list))) {
1406                return new Auth_OpenID_FailureResponse(null,
1407                             "'".$field."' not signed");
1408            }
1409        }
1410
1411        return null;
1412    }
1413
1414    /**
1415     * @access private
1416     * @param Auth_OpenID_Message $message
1417     * @param string $server_url
1418     * @return bool
1419     */
1420    function _checkAuth($message, $server_url)
1421    {
1422        $request = $this->_createCheckAuthRequest($message);
1423        if ($request === null) {
1424            return false;
1425        }
1426
1427        $resp_message = $this->_makeKVPost($request, $server_url);
1428        if (($resp_message === null) ||
1429            (is_a($resp_message, 'Auth_OpenID_ServerErrorContainer'))) {
1430            return false;
1431        }
1432
1433        return $this->_processCheckAuthResponse($resp_message, $server_url);
1434    }
1435
1436    /**
1437     * @access private
1438     * @param Auth_OpenID_Message $message
1439     * @return Auth_OpenID_Message|null
1440     */
1441    function _createCheckAuthRequest($message)
1442    {
1443        $signed = $message->getArg(Auth_OpenID_OPENID_NS, 'signed');
1444        if ($signed) {
1445            foreach (explode(',', $signed) as $k) {
1446                $value = $message->getAliasedArg($k);
1447                if ($value === null) {
1448                    return null;
1449                }
1450            }
1451        }
1452        $ca_message = $message->copy();
1453        $ca_message->setArg(Auth_OpenID_OPENID_NS, 'mode',
1454                            'check_authentication');
1455        return $ca_message;
1456    }
1457
1458    /**
1459     * @access private
1460     * @param Auth_OpenID_Message $response
1461     * @param string $server_url
1462     * @return bool
1463     */
1464    function _processCheckAuthResponse($response, $server_url)
1465    {
1466        $is_valid = $response->getArg(Auth_OpenID_OPENID_NS, 'is_valid',
1467                                      'false');
1468
1469        $invalidate_handle = $response->getArg(Auth_OpenID_OPENID_NS,
1470                                               'invalidate_handle');
1471
1472        if ($invalidate_handle !== null) {
1473            $this->store->removeAssociation($server_url,
1474                                            $invalidate_handle);
1475        }
1476
1477        if ($is_valid == 'true') {
1478            return true;
1479        }
1480
1481        return false;
1482    }
1483
1484    /**
1485     * Adapt a POST response to a Message.
1486     *
1487     * @param Auth_Yadis_HTTPResponse $response Result of a POST to an OpenID endpoint.
1488     * @access private
1489     * @return Auth_OpenID_Message|Auth_OpenID_ServerErrorContainer|null
1490     */
1491    static function _httpResponseToMessage($response)
1492    {
1493        // Should this function be named Message.fromHTTPResponse instead?
1494        $response_message = Auth_OpenID_Message::fromKVForm($response->body);
1495
1496        if ($response->status == 400) {
1497            return Auth_OpenID_ServerErrorContainer::fromMessage(
1498                        $response_message);
1499        } else if ($response->status != 200 and $response->status != 206) {
1500            return null;
1501        }
1502
1503        return $response_message;
1504    }
1505
1506    /**
1507     * @access private
1508     * @param Auth_OpenID_Message $message
1509     * @param string $server_url
1510     * @return Auth_OpenID_Message|Auth_OpenID_ServerErrorContainer|null
1511     */
1512    function _makeKVPost($message, $server_url)
1513    {
1514        $body = $message->toURLEncoded();
1515        $resp = $this->fetcher->post($server_url, $body);
1516
1517        if ($resp === null) {
1518            return null;
1519        }
1520
1521        return $this->_httpResponseToMessage($resp);
1522    }
1523
1524    /**
1525     * @access private
1526     * @param Auth_OpenID_ServiceEndpoint $endpoint
1527     * @return Auth_OpenID_Association|Auth_OpenID_Message|Auth_OpenID_ServerErrorContainer|null
1528     */
1529    function _getAssociation($endpoint)
1530    {
1531        if (!$this->_use_assocs) {
1532            return null;
1533        }
1534
1535        $assoc = $this->store->getAssociation($endpoint->server_url);
1536
1537        if (($assoc === null) ||
1538            ($assoc->getExpiresIn() <= 0)) {
1539
1540            $assoc = $this->_negotiateAssociation($endpoint);
1541
1542            if ($assoc !== null) {
1543                $this->store->storeAssociation($endpoint->server_url, $assoc);
1544            }
1545        }
1546
1547        return $assoc;
1548    }
1549
1550    /**
1551     * Handle ServerErrors resulting from association requests.
1552     *
1553     * @param  $server_error
1554     * @return array|null $result If server replied with an C{unsupported-type}
1555     * error, return a tuple of supported C{association_type},
1556     * C{session_type}.  Otherwise logs the error and returns null.
1557     * @access private
1558     */
1559    function _extractSupportedAssociationType($server_error)
1560    {
1561        // Any error message whose code is not 'unsupported-type'
1562        // should be considered a total failure.
1563        if (($server_error->error_code != 'unsupported-type') ||
1564            ($server_error->message->isOpenID1())) {
1565            return null;
1566        }
1567
1568        // The server didn't like the association/session type that we
1569        // sent, and it sent us back a message that might tell us how
1570        // to handle it.
1571
1572        // Extract the session_type and assoc_type from the error
1573        // message
1574        $assoc_type = $server_error->message->getArg(Auth_OpenID_OPENID_NS,
1575                                                     'assoc_type');
1576
1577        $session_type = $server_error->message->getArg(Auth_OpenID_OPENID_NS,
1578                                                       'session_type');
1579
1580        if (($assoc_type === null) || ($session_type === null)) {
1581            return null;
1582        } else if (!$this->negotiator->isAllowed($assoc_type,
1583                                                 $session_type)) {
1584            return null;
1585        } else {
1586          return [$assoc_type, $session_type];
1587        }
1588    }
1589
1590    /**
1591     * @access private
1592     * @param Auth_OpenID_ServiceEndpoint $endpoint
1593     * @return Auth_OpenID_Association|null
1594     */
1595    function _negotiateAssociation($endpoint)
1596    {
1597        // Get our preferred session/association type from the negotiatior.
1598        list($assoc_type, $session_type) = $this->negotiator->getAllowedType();
1599
1600        $assoc = $this->_requestAssociation(
1601                           $endpoint, $assoc_type, $session_type);
1602
1603        if (Auth_OpenID::isFailure($assoc)) {
1604            return null;
1605        }
1606
1607        if (is_a($assoc, 'Auth_OpenID_ServerErrorContainer')) {
1608            $supportedTypes = $this->_extractSupportedAssociationType($assoc);
1609
1610            if ($supportedTypes !== null) {
1611                list($assoc_type, $session_type) = $supportedTypes;
1612
1613                // Attempt to create an association from the assoc_type
1614                // and session_type that the server told us it
1615                // supported.
1616                $assoc = $this->_requestAssociation(
1617                                   $endpoint, $assoc_type, $session_type);
1618
1619                if (is_a($assoc, 'Auth_OpenID_ServerErrorContainer')) {
1620                    // Do not keep trying, since it rejected the
1621                    // association type that it told us to use.
1622                    // oidutil.log('Server %s refused its suggested association
1623                    //             'type: session_type=%s, assoc_type=%s'
1624                    //             % (endpoint.server_url, session_type,
1625                    //                assoc_type))
1626                    return null;
1627                } else {
1628                    return $assoc;
1629                }
1630            } else {
1631                return null;
1632            }
1633        } else {
1634            return $assoc;
1635        }
1636    }
1637
1638    /**
1639     * @access private
1640     * @param Auth_OpenID_ServiceEndpoint $endpoint
1641     * @param string $assoc_type
1642     * @param string $session_type
1643     * @return Auth_OpenID_Association|Auth_OpenID_Message|Auth_OpenID_ServerErrorContainer|null
1644     */
1645    function _requestAssociation($endpoint, $assoc_type, $session_type)
1646    {
1647        list($assoc_session, $args) = $this->_createAssociateRequest(
1648                                      $endpoint, $assoc_type, $session_type);
1649
1650        $response_message = $this->_makeKVPost($args, $endpoint->server_url);
1651
1652        if ($response_message === null) {
1653            // oidutil.log('openid.associate request failed: %s' % (why[0],))
1654            return null;
1655        } else if (is_a($response_message,
1656                        'Auth_OpenID_ServerErrorContainer')) {
1657            return $response_message;
1658        }
1659
1660        return $this->_extractAssociation($response_message, $assoc_session);
1661    }
1662
1663    /**
1664     * @access private
1665     * @param Auth_OpenID_Message $assoc_response
1666     * @param Auth_OpenID_PlainTextConsumerSession $assoc_session
1667     * @return Auth_OpenID_Association|Auth_OpenID_FailureResponse|null
1668     */
1669    function _extractAssociation($assoc_response, $assoc_session)
1670    {
1671        // Extract the common fields from the response, raising an
1672        // exception if they are not found
1673        $assoc_type = $assoc_response->getArg(
1674                         Auth_OpenID_OPENID_NS, 'assoc_type',
1675                         Auth_OpenID_NO_DEFAULT);
1676
1677        if (Auth_OpenID::isFailure($assoc_type)) {
1678            return $assoc_type;
1679        }
1680
1681        $assoc_handle = $assoc_response->getArg(
1682                           Auth_OpenID_OPENID_NS, 'assoc_handle',
1683                           Auth_OpenID_NO_DEFAULT);
1684
1685        if (Auth_OpenID::isFailure($assoc_handle)) {
1686            return $assoc_handle;
1687        }
1688
1689        // expires_in is a base-10 string. The Python parsing will
1690        // accept literals that have whitespace around them and will
1691        // accept negative values. Neither of these are really in-spec,
1692        // but we think it's OK to accept them.
1693        $expires_in_str = $assoc_response->getArg(
1694                             Auth_OpenID_OPENID_NS, 'expires_in',
1695                             Auth_OpenID_NO_DEFAULT);
1696
1697        if (Auth_OpenID::isFailure($expires_in_str)) {
1698            return $expires_in_str;
1699        }
1700
1701        $expires_in = Auth_OpenID::intval($expires_in_str);
1702        if ($expires_in === false) {
1703
1704            $err = sprintf("Could not parse expires_in from association ".
1705                           "response %s", print_r($assoc_response, true));
1706            return new Auth_OpenID_FailureResponse(null, $err);
1707        }
1708
1709        // OpenID 1 has funny association session behaviour.
1710        if ($assoc_response->isOpenID1()) {
1711            $session_type = $this->_getOpenID1SessionType($assoc_response);
1712        } else {
1713            $session_type = $assoc_response->getArg(
1714                               Auth_OpenID_OPENID2_NS, 'session_type',
1715                               Auth_OpenID_NO_DEFAULT);
1716
1717            if (Auth_OpenID::isFailure($session_type)) {
1718                return $session_type;
1719            }
1720        }
1721
1722        // Session type mismatch
1723        if ($assoc_session->session_type != $session_type) {
1724            if ($assoc_response->isOpenID1() &&
1725                ($session_type == 'no-encryption')) {
1726                // In OpenID 1, any association request can result in
1727                // a 'no-encryption' association response. Setting
1728                // assoc_session to a new no-encryption session should
1729                // make the rest of this function work properly for
1730                // that case.
1731                $assoc_session = new Auth_OpenID_PlainTextConsumerSession();
1732            } else {
1733                // Any other mismatch, regardless of protocol version
1734                // results in the failure of the association session
1735                // altogether.
1736                return null;
1737            }
1738        }
1739
1740        // Make sure assoc_type is valid for session_type
1741        if (!in_array($assoc_type, $assoc_session->allowed_assoc_types)) {
1742            return null;
1743        }
1744
1745        // Delegate to the association session to extract the secret
1746        // from the response, however is appropriate for that session
1747        // type.
1748        $secret = $assoc_session->extractSecret($assoc_response);
1749
1750        if ($secret === null) {
1751            return null;
1752        }
1753
1754        return Auth_OpenID_Association::fromExpiresIn(
1755                 $expires_in, $assoc_handle, $secret, $assoc_type);
1756    }
1757
1758    /**
1759     * @access private
1760     * @param Auth_OpenID_ServiceEndpoint $endpoint
1761     * @param string $assoc_type
1762     * @param string $session_type
1763     * @return array|null
1764     */
1765    function _createAssociateRequest($endpoint, $assoc_type, $session_type)
1766    {
1767        if (array_key_exists($session_type, $this->session_types)) {
1768            $session_type_class = $this->session_types[$session_type];
1769
1770            if (is_callable($session_type_class)) {
1771                /** @var Auth_OpenID_PlainTextConsumerSession $assoc_session */
1772                $assoc_session = $session_type_class();
1773            } else {
1774                $assoc_session = new $session_type_class();
1775            }
1776        } else {
1777            return null;
1778        }
1779
1780        $args = [
1781            'mode' => 'associate',
1782            'assoc_type' => $assoc_type
1783        ];
1784
1785        if (!$endpoint->compatibilityMode()) {
1786            $args['ns'] = Auth_OpenID_OPENID2_NS;
1787        }
1788
1789        // Leave out the session type if we're in compatibility mode
1790        // *and* it's no-encryption.
1791        if ((!$endpoint->compatibilityMode()) ||
1792            ($assoc_session->session_type != 'no-encryption')) {
1793            $args['session_type'] = $assoc_session->session_type;
1794        }
1795
1796        $args = array_merge($args, $assoc_session->getRequest());
1797        $message = Auth_OpenID_Message::fromOpenIDArgs($args);
1798        return [$assoc_session, $message];
1799    }
1800
1801    /**
1802     * Given an association response message, extract the OpenID 1.X
1803     * session type.
1804     *
1805     * This function mostly takes care of the 'no-encryption' default
1806     * behavior in OpenID 1.
1807     *
1808     * If the association type is plain-text, this function will
1809     * return 'no-encryption'
1810     *
1811     * @access private
1812     * @param Auth_OpenID_Message $assoc_response
1813     * @return string The association type for this message
1814     */
1815    function _getOpenID1SessionType($assoc_response)
1816    {
1817        // If it's an OpenID 1 message, allow session_type to default
1818        // to None (which signifies "no-encryption")
1819        $session_type = $assoc_response->getArg(Auth_OpenID_OPENID1_NS,
1820                                                'session_type');
1821
1822        // Handle the differences between no-encryption association
1823        // respones in OpenID 1 and 2:
1824
1825        // no-encryption is not really a valid session type for OpenID
1826        // 1, but we'll accept it anyway, while issuing a warning.
1827        if ($session_type == 'no-encryption') {
1828            // oidutil.log('WARNING: OpenID server sent "no-encryption"'
1829            //             'for OpenID 1.X')
1830        } else if (($session_type == '') || ($session_type === null)) {
1831            // Missing or empty session type is the way to flag a
1832            // 'no-encryption' response. Change the session type to
1833            // 'no-encryption' so that it can be handled in the same
1834            // way as OpenID 2 'no-encryption' respones.
1835            $session_type = 'no-encryption';
1836        }
1837
1838        return $session_type;
1839    }
1840}
1841
1842/**
1843 * This class represents an authentication request from a consumer to
1844 * an OpenID server.
1845 *
1846 * @package OpenID
1847 */
1848class Auth_OpenID_AuthRequest {
1849
1850    /** @var Auth_OpenID_Association */
1851    public $assoc;
1852
1853    /** @var Auth_OpenID_ServiceEndpoint */
1854    public $endpoint;
1855
1856    /** @var array */
1857    public $return_to_args;
1858
1859    /** @var Auth_OpenID_Message */
1860    public $message;
1861
1862    /** @var bool */
1863    public $_anonymous;
1864
1865    /**
1866     * Initialize an authentication request with the specified token,
1867     * association, and endpoint.
1868     *
1869     * Users of this library should not create instances of this
1870     * class.  Instances of this class are created by the library when
1871     * needed.
1872     *
1873     * @param Auth_OpenID_ServiceEndpoint $endpoint
1874     * @param Auth_OpenID_Association $assoc
1875     */
1876    function __construct($endpoint, $assoc)
1877    {
1878        $this->assoc = $assoc;
1879        $this->endpoint = $endpoint;
1880        $this->return_to_args = [];
1881        $this->message = new Auth_OpenID_Message($endpoint->preferredNamespace());
1882        $this->_anonymous = false;
1883    }
1884
1885    /**
1886     * Add an extension to this checkid request.
1887     *
1888     * @param Auth_OpenID_Extension $extension_request An object that implements the extension
1889     * request interface for adding arguments to an OpenID message.
1890     */
1891    function addExtension($extension_request)
1892    {
1893        $extension_request->toMessage($this->message);
1894    }
1895
1896    /**
1897     * Add an extension argument to this OpenID authentication
1898     * request.
1899     *
1900     * Use caution when adding arguments, because they will be
1901     * URL-escaped and appended to the redirect URL, which can easily
1902     * get quite long.
1903     *
1904     * @param string $namespace The namespace for the extension. For
1905     * example, the simple registration extension uses the namespace
1906     * 'sreg'.
1907     *
1908     * @param string $key The key within the extension namespace. For
1909     * example, the nickname field in the simple registration
1910     * extension's key is 'nickname'.
1911     *
1912     * @param string $value The value to provide to the server for
1913     * this argument.
1914     *
1915     * @return Auth_OpenID_FailureResponse|bool|null|string
1916     */
1917    function addExtensionArg($namespace, $key, $value)
1918    {
1919        return $this->message->setArg($namespace, $key, $value);
1920    }
1921
1922    /**
1923     * Set whether this request should be made anonymously. If a
1924     * request is anonymous, the identifier will not be sent in the
1925     * request. This is only useful if you are making another kind of
1926     * request with an extension in this request.
1927     *
1928     * Anonymous requests are not allowed when the request is made
1929     * with OpenID 1.
1930     *
1931     * @param bool $is_anonymous
1932     * @return bool
1933     */
1934    function setAnonymous($is_anonymous)
1935    {
1936        if ($is_anonymous && $this->message->isOpenID1()) {
1937            return false;
1938        } else {
1939            $this->_anonymous = $is_anonymous;
1940            return true;
1941        }
1942    }
1943
1944    /**
1945     * Produce a {@link Auth_OpenID_Message} representing this
1946     * request.
1947     *
1948     * @param string $realm The URL (or URL pattern) that identifies
1949     * your web site to the user when she is authorizing it.
1950     *
1951     * @param string $return_to The URL that the OpenID provider will
1952     * send the user back to after attempting to verify her identity.
1953     *
1954     * Not specifying a return_to URL means that the user will not be
1955     * returned to the site issuing the request upon its completion.
1956     *
1957     * @param bool $immediate If true, the OpenID provider is to send
1958     * back a response immediately, useful for behind-the-scenes
1959     * authentication attempts.  Otherwise the OpenID provider may
1960     * engage the user before providing a response.  This is the
1961     * default case, as the user may need to provide credentials or
1962     * approve the request before a positive response can be sent.
1963     *
1964     * @return Auth_OpenID_Message|Auth_OpenID_FailureResponse
1965     */
1966    function getMessage($realm, $return_to=null, $immediate=false)
1967    {
1968        if ($return_to) {
1969            $return_to = Auth_OpenID::appendArgs($return_to,
1970                                                 $this->return_to_args);
1971        } else if ($immediate) {
1972            // raise ValueError(
1973            //     '"return_to" is mandatory when
1974            //using "checkid_immediate"')
1975            return new Auth_OpenID_FailureResponse(null,
1976              "'return_to' is mandatory when using checkid_immediate");
1977        } else if ($this->message->isOpenID1()) {
1978            // raise ValueError('"return_to" is
1979            // mandatory for OpenID 1 requests')
1980            return new Auth_OpenID_FailureResponse(null,
1981              "'return_to' is mandatory for OpenID 1 requests");
1982        } else if ($this->return_to_args) {
1983            // raise ValueError('extra "return_to" arguments
1984            // were specified, but no return_to was specified')
1985            return new Auth_OpenID_FailureResponse(null,
1986              "extra 'return_to' arguments where specified, " .
1987              "but no return_to was specified");
1988        }
1989
1990        if ($immediate) {
1991            $mode = 'checkid_immediate';
1992        } else {
1993            $mode = 'checkid_setup';
1994        }
1995
1996        $message = $this->message->copy();
1997        if ($message->isOpenID1()) {
1998            $realm_key = 'trust_root';
1999        } else {
2000            $realm_key = 'realm';
2001        }
2002
2003        $message->updateArgs(Auth_OpenID_OPENID_NS,
2004                             [
2005                                   $realm_key => $realm,
2006                                   'mode' => $mode,
2007                                   'return_to' => $return_to
2008                             ]);
2009
2010        if (!$this->_anonymous) {
2011            if ($this->endpoint->isOPIdentifier()) {
2012                // This will never happen when we're in compatibility
2013                // mode, as long as isOPIdentifier() returns False
2014                // whenever preferredNamespace() returns OPENID1_NS.
2015                $claimed_id = $request_identity =
2016                    Auth_OpenID_IDENTIFIER_SELECT;
2017            } else {
2018                $request_identity = $this->endpoint->getLocalID();
2019                $claimed_id = $this->endpoint->claimed_id;
2020            }
2021
2022            // This is true for both OpenID 1 and 2
2023            $message->setArg(Auth_OpenID_OPENID_NS, 'identity',
2024                             $request_identity);
2025
2026            if ($message->isOpenID2()) {
2027                $message->setArg(Auth_OpenID_OPENID2_NS, 'claimed_id',
2028                                 $claimed_id);
2029            }
2030        }
2031
2032        if ($this->assoc) {
2033            $message->setArg(Auth_OpenID_OPENID_NS, 'assoc_handle',
2034                             $this->assoc->handle);
2035        }
2036
2037        return $message;
2038    }
2039
2040    function redirectURL($realm, $return_to = null,
2041                         $immediate = false)
2042    {
2043        $message = $this->getMessage($realm, $return_to, $immediate);
2044
2045        if (Auth_OpenID::isFailure($message)) {
2046            return $message;
2047        }
2048
2049        return $message->toURL($this->endpoint->server_url);
2050    }
2051
2052    /**
2053     * Get html for a form to submit this request to the IDP.
2054     *
2055     * form_tag_attrs: An array of attributes to be added to the form
2056     * tag. 'accept-charset' and 'enctype' have defaults that can be
2057     * overridden. If a value is supplied for 'action' or 'method', it
2058     * will be replaced.
2059     *
2060     * @param string $realm
2061     * @param null|string $return_to
2062     * @param bool $immediate
2063     * @param null|array $form_tag_attrs
2064     * @return Auth_OpenID_FailureResponse|Auth_OpenID_Message|string
2065     */
2066    function formMarkup($realm, $return_to=null, $immediate=false,
2067                        $form_tag_attrs=null)
2068    {
2069        $message = $this->getMessage($realm, $return_to, $immediate);
2070
2071        if (Auth_OpenID::isFailure($message)) {
2072            return $message;
2073        }
2074
2075        return $message->toFormMarkup($this->endpoint->server_url, $form_tag_attrs);
2076    }
2077
2078    /**
2079     * Get a complete html document that will autosubmit the request
2080     * to the IDP.
2081     *
2082     * Wraps formMarkup.  See the documentation for that function.
2083     *
2084     * @param string $realm
2085     * @param string $return_to
2086     * @param bool $immediate
2087     * @param array $form_tag_attrs
2088     * @return Auth_OpenID_FailureResponse|Auth_OpenID_Message|string
2089     */
2090    function htmlMarkup($realm, $return_to=null, $immediate=false,
2091                        $form_tag_attrs=null)
2092    {
2093        $form = $this->formMarkup($realm, $return_to, $immediate,
2094                                  $form_tag_attrs);
2095
2096        if (Auth_OpenID::isFailure($form)) {
2097            return $form;
2098        }
2099        return Auth_OpenID::autoSubmitHTML($form);
2100    }
2101
2102    function shouldSendRedirect()
2103    {
2104        return $this->endpoint->compatibilityMode();
2105    }
2106}
2107
2108/**
2109 * The base class for responses from the Auth_OpenID_Consumer.
2110 *
2111 * @package OpenID
2112 */
2113class Auth_OpenID_ConsumerResponse {
2114    public $status = null;
2115
2116    /** @var null|string */
2117    public $identity_url = null;
2118
2119    /** @var Auth_OpenID_ServiceEndpoint */
2120    public $endpoint;
2121
2122    /**
2123     * @param Auth_OpenID_ServiceEndpoint|null $endpoint
2124     */
2125    function setEndpoint($endpoint)
2126    {
2127        $this->endpoint = $endpoint;
2128        if ($endpoint === null) {
2129            $this->identity_url = null;
2130        } else {
2131            $this->identity_url = $endpoint->claimed_id;
2132        }
2133    }
2134
2135    /**
2136     * Return the display identifier for this response.
2137     *
2138     * The display identifier is related to the Claimed Identifier, but the
2139     * two are not always identical.  The display identifier is something the
2140     * user should recognize as what they entered, whereas the response's
2141     * claimed identifier (in the identity_url attribute) may have extra
2142     * information for better persistence.
2143     *
2144     * URLs will be stripped of their fragments for display.  XRIs will
2145     * display the human-readable identifier (i-name) instead of the
2146     * persistent identifier (i-number).
2147     *
2148     * Use the display identifier in your user interface.  Use
2149     * identity_url for querying your database or authorization server.
2150     *
2151     */
2152    function getDisplayIdentifier()
2153    {
2154        if ($this->endpoint !== null) {
2155            return $this->endpoint->getDisplayIdentifier();
2156        }
2157        return null;
2158    }
2159}
2160
2161/**
2162 * A response with a status of Auth_OpenID_SUCCESS. Indicates that
2163 * this request is a successful acknowledgement from the OpenID server
2164 * that the supplied URL is, indeed controlled by the requesting
2165 * agent.  This has three relevant attributes:
2166 *
2167 * claimed_id - The identity URL that has been authenticated
2168 *
2169 * signed_args - The arguments in the server's response that were
2170 * signed and verified.
2171 *
2172 * status - Auth_OpenID_SUCCESS.
2173 *
2174 * @package OpenID
2175 */
2176class Auth_OpenID_SuccessResponse extends Auth_OpenID_ConsumerResponse {
2177    public $status = Auth_OpenID_SUCCESS;
2178
2179    /** @var array */
2180    public $signed_args = [];
2181    /** @var Auth_OpenID_Message */
2182    public $message;
2183
2184    /**
2185     * @access private
2186     * @param Auth_OpenID_ServiceEndpoint $endpoint
2187     * @param Auth_OpenID_Message $message
2188     * @param array|null $signed_args
2189     */
2190    function __construct($endpoint, $message, $signed_args=null)
2191    {
2192        $this->endpoint = $endpoint;
2193        $this->identity_url = $endpoint->claimed_id;
2194        $this->message = $message;
2195
2196        if ($this->signed_args !== null) {
2197            $this->signed_args = $signed_args;
2198        }
2199    }
2200
2201    /**
2202     * Extract signed extension data from the server's response.
2203     *
2204     * @param $namespace_uri
2205     * @param $require_signed
2206     * @return array|Auth_OpenID_FailureResponse|null|string
2207     * @internal param string $prefix The extension namespace from which to
2208     * extract the extension data.
2209     */
2210    function extensionResponse($namespace_uri, $require_signed)
2211    {
2212        if ($require_signed) {
2213            return $this->getSignedNS($namespace_uri);
2214        } else {
2215            return $this->message->getArgs($namespace_uri);
2216        }
2217    }
2218
2219    function isOpenID1()
2220    {
2221        return $this->message->isOpenID1();
2222    }
2223
2224    function isSigned($ns_uri, $ns_key)
2225    {
2226        // Return whether a particular key is signed, regardless of
2227        // its namespace alias
2228        return in_array($this->message->getKey($ns_uri, $ns_key),
2229                        $this->signed_args);
2230    }
2231
2232    function getSigned($ns_uri, $ns_key, $default = null)
2233    {
2234        // Return the specified signed field if available, otherwise
2235        // return default
2236        if ($this->isSigned($ns_uri, $ns_key)) {
2237            return $this->message->getArg($ns_uri, $ns_key, $default);
2238        } else {
2239            return $default;
2240        }
2241    }
2242
2243    function getSignedNS($ns_uri)
2244    {
2245        $msg_args = $this->message->getArgs($ns_uri);
2246        if (Auth_OpenID::isFailure($msg_args)) {
2247            return null;
2248        }
2249
2250        foreach ($msg_args as $key => $value) {
2251            if (!$this->isSigned($ns_uri, $key)) {
2252                unset($msg_args[$key]);
2253            }
2254        }
2255
2256        return $msg_args;
2257    }
2258
2259    /**
2260     * Get the openid.return_to argument from this response.
2261     *
2262     * This is useful for verifying that this request was initiated by
2263     * this consumer.
2264     *
2265     * @return string $return_to The return_to URL supplied to the
2266     * server on the initial request, or null if the response did not
2267     * contain an 'openid.return_to' argument.
2268    */
2269    function getReturnTo()
2270    {
2271        return $this->getSigned(Auth_OpenID_OPENID_NS, 'return_to');
2272    }
2273}
2274
2275/**
2276 * A response with a status of Auth_OpenID_FAILURE. Indicates that the
2277 * OpenID protocol has failed. This could be locally or remotely
2278 * triggered.  This has three relevant attributes:
2279 *
2280 * claimed_id - The identity URL for which authentication was
2281 * attempted, if it can be determined.  Otherwise, null.
2282 *
2283 * message - A message indicating why the request failed, if one is
2284 * supplied.  Otherwise, null.
2285 *
2286 * status - Auth_OpenID_FAILURE.
2287 *
2288 * @package OpenID
2289 */
2290class Auth_OpenID_FailureResponse extends Auth_OpenID_ConsumerResponse {
2291    public $status = Auth_OpenID_FAILURE;
2292
2293    /** @var string */
2294    public $message;
2295
2296    public $contact;
2297
2298    public $reference;
2299
2300    function __construct($endpoint, $message = null, $contact = null, $reference = null)
2301    {
2302        $this->setEndpoint($endpoint);
2303        $this->message = $message;
2304        $this->contact = $contact;
2305        $this->reference = $reference;
2306    }
2307}
2308
2309/**
2310 * A specific, internal failure used to detect type URI mismatch.
2311 *
2312 * @package OpenID
2313 */
2314class Auth_OpenID_TypeURIMismatch extends Auth_OpenID_FailureResponse {
2315}
2316
2317/**
2318 * Exception that is raised when the server returns a 400 response
2319 * code to a direct request.
2320 *
2321 * @package OpenID
2322 */
2323class Auth_OpenID_ServerErrorContainer {
2324
2325    /** @var Auth_OpenID_Message */
2326    public $message;
2327
2328    /** @var string */
2329    public $error_code;
2330    /** @var string */
2331    private $error_text;
2332
2333    /**
2334     * Auth_OpenID_ServerErrorContainer constructor.
2335     *
2336     * @param string $error_text
2337     * @param string $error_code
2338     * @param Auth_OpenID_Message $message
2339     */
2340    function __construct($error_text, $error_code, $message)
2341    {
2342        $this->error_text = $error_text;
2343        $this->error_code = $error_code;
2344        $this->message = $message;
2345    }
2346
2347    /**
2348     * @access private
2349     * @param Auth_OpenID_Message $message
2350     * @return Auth_OpenID_ServerErrorContainer
2351     */
2352    static function fromMessage($message)
2353    {
2354        $error_text = $message->getArg(
2355           Auth_OpenID_OPENID_NS, 'error', '<no error message supplied>');
2356        $error_code = $message->getArg(Auth_OpenID_OPENID_NS, 'error_code');
2357        return new Auth_OpenID_ServerErrorContainer($error_text,
2358                                                    $error_code,
2359                                                    $message);
2360    }
2361}
2362
2363/**
2364 * A response with a status of Auth_OpenID_CANCEL. Indicates that the
2365 * user cancelled the OpenID authentication request.  This has two
2366 * relevant attributes:
2367 *
2368 * claimed_id - The identity URL for which authentication was
2369 * attempted, if it can be determined.  Otherwise, null.
2370 *
2371 * status - Auth_OpenID_SUCCESS.
2372 *
2373 * @package OpenID
2374 */
2375class Auth_OpenID_CancelResponse extends Auth_OpenID_ConsumerResponse {
2376    public $status = Auth_OpenID_CANCEL;
2377
2378    /**
2379     * Auth_OpenID_CancelResponse constructor.
2380     *
2381     * @param Auth_OpenID_ServiceEndpoint $endpoint
2382     */
2383    function __construct($endpoint)
2384    {
2385        $this->setEndpoint($endpoint);
2386    }
2387}
2388
2389/**
2390 * A response with a status of Auth_OpenID_SETUP_NEEDED. Indicates
2391 * that the request was in immediate mode, and the server is unable to
2392 * authenticate the user without further interaction.
2393 *
2394 * claimed_id - The identity URL for which authentication was
2395 * attempted.
2396 *
2397 * setup_url - A URL that can be used to send the user to the server
2398 * to set up for authentication. The user should be redirected in to
2399 * the setup_url, either in the current window or in a new browser
2400 * window.  Null in OpenID 2.
2401 *
2402 * status - Auth_OpenID_SETUP_NEEDED.
2403 *
2404 * @package OpenID
2405 */
2406class Auth_OpenID_SetupNeededResponse extends Auth_OpenID_ConsumerResponse {
2407    public $status = Auth_OpenID_SETUP_NEEDED;
2408
2409    /** @var string */
2410    public $setup_url = '';
2411
2412    /**
2413     * Auth_OpenID_SetupNeededResponse constructor.
2414     *
2415     * @param Auth_OpenID_ServiceEndpoint $endpoint
2416     * @param string $setup_url
2417     */
2418    function __construct($endpoint, $setup_url = null)
2419    {
2420        $this->setEndpoint($endpoint);
2421        $this->setup_url = $setup_url;
2422    }
2423}
2424