1<?php
2
3/**
4 * The OpenID and Yadis discovery implementation for OpenID 1.2.
5 */
6
7require_once "Auth/OpenID.php";
8require_once "Auth/OpenID/Parse.php";
9require_once "Auth/OpenID/Message.php";
10require_once "Auth/Yadis/XRIRes.php";
11require_once "Auth/Yadis/Yadis.php";
12
13// XML namespace value
14define('Auth_OpenID_XMLNS_1_0', 'http://openid.net/xmlns/1.0');
15
16// Yadis service types
17define('Auth_OpenID_TYPE_1_2', 'http://openid.net/signon/1.2');
18define('Auth_OpenID_TYPE_1_1', 'http://openid.net/signon/1.1');
19define('Auth_OpenID_TYPE_1_0', 'http://openid.net/signon/1.0');
20define('Auth_OpenID_TYPE_2_0_IDP', 'http://specs.openid.net/auth/2.0/server');
21define('Auth_OpenID_TYPE_2_0', 'http://specs.openid.net/auth/2.0/signon');
22define('Auth_OpenID_RP_RETURN_TO_URL_TYPE',
23       'http://specs.openid.net/auth/2.0/return_to');
24
25function Auth_OpenID_getOpenIDTypeURIs()
26{
27    return array(Auth_OpenID_TYPE_2_0_IDP,
28                 Auth_OpenID_TYPE_2_0,
29                 Auth_OpenID_TYPE_1_2,
30                 Auth_OpenID_TYPE_1_1,
31                 Auth_OpenID_TYPE_1_0);
32}
33
34function Auth_OpenID_getOpenIDConsumerTypeURIs()
35{
36    return array(Auth_OpenID_RP_RETURN_TO_URL_TYPE);
37}
38
39
40/*
41 * Provides a user-readable interpretation of a type uri.
42 * Useful for error messages.
43 */
44function Auth_OpenID_getOpenIDTypeName($type_uri) {
45    switch ($type_uri) {
46    case Auth_OpenID_TYPE_2_0_IDP:
47      return 'OpenID 2.0 IDP';
48    case Auth_OpenID_TYPE_2_0:
49      return 'OpenID 2.0';
50    case Auth_OpenID_TYPE_1_2:
51      return 'OpenID 1.2';
52    case Auth_OpenID_TYPE_1_1:
53      return 'OpenID 1.1';
54    case Auth_OpenID_TYPE_1_0:
55      return 'OpenID 1.0';
56    case Auth_OpenID_RP_RETURN_TO_URL_TYPE:
57      return 'OpenID relying party';
58    }
59}
60
61/**
62 * Object representing an OpenID service endpoint.
63 */
64class Auth_OpenID_ServiceEndpoint {
65    function Auth_OpenID_ServiceEndpoint()
66    {
67        $this->claimed_id = null;
68        $this->server_url = null;
69        $this->type_uris = array();
70        $this->local_id = null;
71        $this->canonicalID = null;
72        $this->used_yadis = false; // whether this came from an XRDS
73        $this->display_identifier = null;
74    }
75
76    function getDisplayIdentifier()
77    {
78        if ($this->display_identifier) {
79            return $this->display_identifier;
80        }
81        if (! $this->claimed_id) {
82          return $this->claimed_id;
83        }
84        $parsed = parse_url($this->claimed_id);
85        $scheme = $parsed['scheme'];
86        $host = $parsed['host'];
87        $path = $parsed['path'];
88        if (array_key_exists('query', $parsed)) {
89            $query = $parsed['query'];
90            $no_frag = "$scheme://$host$path?$query";
91        } else {
92            $no_frag = "$scheme://$host$path";
93        }
94        return $no_frag;
95    }
96
97    function usesExtension($extension_uri)
98    {
99        return in_array($extension_uri, $this->type_uris);
100    }
101
102    function preferredNamespace()
103    {
104        if (in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris) ||
105            in_array(Auth_OpenID_TYPE_2_0, $this->type_uris)) {
106            return Auth_OpenID_OPENID2_NS;
107        } else {
108            return Auth_OpenID_OPENID1_NS;
109        }
110    }
111
112    /*
113     * Query this endpoint to see if it has any of the given type
114     * URIs. This is useful for implementing other endpoint classes
115     * that e.g. need to check for the presence of multiple versions
116     * of a single protocol.
117     *
118     * @param $type_uris The URIs that you wish to check
119     *
120     * @return all types that are in both in type_uris and
121     * $this->type_uris
122     */
123    function matchTypes($type_uris)
124    {
125        $result = array();
126        foreach ($type_uris as $test_uri) {
127            if ($this->supportsType($test_uri)) {
128                $result[] = $test_uri;
129            }
130        }
131
132        return $result;
133    }
134
135    function supportsType($type_uri)
136    {
137        // Does this endpoint support this type?
138        return ((in_array($type_uri, $this->type_uris)) ||
139                (($type_uri == Auth_OpenID_TYPE_2_0) &&
140                 $this->isOPIdentifier()));
141    }
142
143    function compatibilityMode()
144    {
145        return $this->preferredNamespace() != Auth_OpenID_OPENID2_NS;
146    }
147
148    function isOPIdentifier()
149    {
150        return in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris);
151    }
152
153    static function fromOPEndpointURL($op_endpoint_url)
154    {
155        // Construct an OP-Identifier OpenIDServiceEndpoint object for
156        // a given OP Endpoint URL
157        $obj = new Auth_OpenID_ServiceEndpoint();
158        $obj->server_url = $op_endpoint_url;
159        $obj->type_uris = array(Auth_OpenID_TYPE_2_0_IDP);
160        return $obj;
161    }
162
163    function parseService($yadis_url, $uri, $type_uris, $service_element)
164    {
165        // Set the state of this object based on the contents of the
166        // service element.  Return true if successful, false if not
167        // (if findOPLocalIdentifier returns false).
168        $this->type_uris = $type_uris;
169        $this->server_url = $uri;
170        $this->used_yadis = true;
171
172        if (!$this->isOPIdentifier()) {
173            $this->claimed_id = $yadis_url;
174            $this->local_id = Auth_OpenID_findOPLocalIdentifier(
175                                                    $service_element,
176                                                    $this->type_uris);
177            if ($this->local_id === false) {
178                return false;
179            }
180        }
181
182        return true;
183    }
184
185    function getLocalID()
186    {
187        // Return the identifier that should be sent as the
188        // openid.identity_url parameter to the server.
189        if ($this->local_id === null && $this->canonicalID === null) {
190            return $this->claimed_id;
191        } else {
192            if ($this->local_id) {
193                return $this->local_id;
194            } else {
195                return $this->canonicalID;
196            }
197        }
198    }
199
200    /*
201     * Parse the given document as XRDS looking for OpenID consumer services.
202     *
203     * @return array of Auth_OpenID_ServiceEndpoint or null if the
204     * document cannot be parsed.
205     */
206    function consumerFromXRDS($uri, $xrds_text)
207    {
208        $xrds =& Auth_Yadis_XRDS::parseXRDS($xrds_text);
209
210        if ($xrds) {
211            $yadis_services =
212              $xrds->services(array('filter_MatchesAnyOpenIDConsumerType'));
213            return Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services);
214        }
215
216        return null;
217    }
218
219    /*
220     * Parse the given document as XRDS looking for OpenID services.
221     *
222     * @return array of Auth_OpenID_ServiceEndpoint or null if the
223     * document cannot be parsed.
224     */
225    static function fromXRDS($uri, $xrds_text)
226    {
227        $xrds = Auth_Yadis_XRDS::parseXRDS($xrds_text);
228
229        if ($xrds) {
230            $yadis_services =
231              $xrds->services(array('filter_MatchesAnyOpenIDType'));
232            return Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services);
233        }
234
235        return null;
236    }
237
238    /*
239     * Create endpoints from a DiscoveryResult.
240     *
241     * @param discoveryResult Auth_Yadis_DiscoveryResult
242     * @return array of Auth_OpenID_ServiceEndpoint or null if
243     * endpoints cannot be created.
244     */
245    static function fromDiscoveryResult($discoveryResult)
246    {
247        if ($discoveryResult->isXRDS()) {
248            return Auth_OpenID_ServiceEndpoint::fromXRDS(
249                                     $discoveryResult->normalized_uri,
250                                     $discoveryResult->response_text);
251        } else {
252            return Auth_OpenID_ServiceEndpoint::fromHTML(
253                                     $discoveryResult->normalized_uri,
254                                     $discoveryResult->response_text);
255        }
256    }
257
258    static function fromHTML($uri, $html)
259    {
260        $discovery_types = array(
261                                 array(Auth_OpenID_TYPE_2_0,
262                                       'openid2.provider', 'openid2.local_id'),
263                                 array(Auth_OpenID_TYPE_1_1,
264                                       'openid.server', 'openid.delegate')
265                                 );
266
267        $services = array();
268
269        foreach ($discovery_types as $triple) {
270            list($type_uri, $server_rel, $delegate_rel) = $triple;
271
272            $urls = Auth_OpenID_legacy_discover($html, $server_rel,
273                                                $delegate_rel);
274
275            if ($urls === false) {
276                continue;
277            }
278
279            list($delegate_url, $server_url) = $urls;
280
281            $service = new Auth_OpenID_ServiceEndpoint();
282            $service->claimed_id = $uri;
283            $service->local_id = $delegate_url;
284            $service->server_url = $server_url;
285            $service->type_uris = array($type_uri);
286
287            $services[] = $service;
288        }
289
290        return $services;
291    }
292
293    function copy()
294    {
295        $x = new Auth_OpenID_ServiceEndpoint();
296
297        $x->claimed_id = $this->claimed_id;
298        $x->server_url = $this->server_url;
299        $x->type_uris = $this->type_uris;
300        $x->local_id = $this->local_id;
301        $x->canonicalID = $this->canonicalID;
302        $x->used_yadis = $this->used_yadis;
303
304        return $x;
305    }
306}
307
308function Auth_OpenID_findOPLocalIdentifier($service, $type_uris)
309{
310    // Extract a openid:Delegate value from a Yadis Service element.
311    // If no delegate is found, returns null.  Returns false on
312    // discovery failure (when multiple delegate/localID tags have
313    // different values).
314
315    $service->parser->registerNamespace('openid',
316                                        Auth_OpenID_XMLNS_1_0);
317
318    $service->parser->registerNamespace('xrd',
319                                        Auth_Yadis_XMLNS_XRD_2_0);
320
321    $parser = $service->parser;
322
323    $permitted_tags = array();
324
325    if (in_array(Auth_OpenID_TYPE_1_1, $type_uris) ||
326        in_array(Auth_OpenID_TYPE_1_0, $type_uris)) {
327        $permitted_tags[] = 'openid:Delegate';
328    }
329
330    if (in_array(Auth_OpenID_TYPE_2_0, $type_uris)) {
331        $permitted_tags[] = 'xrd:LocalID';
332    }
333
334    $local_id = null;
335
336    foreach ($permitted_tags as $tag_name) {
337        $tags = $service->getElements($tag_name);
338
339        foreach ($tags as $tag) {
340            $content = $parser->content($tag);
341
342            if ($local_id === null) {
343                $local_id = $content;
344            } else if ($local_id != $content) {
345                return false;
346            }
347        }
348    }
349
350    return $local_id;
351}
352
353function filter_MatchesAnyOpenIDType($service)
354{
355    $uris = $service->getTypes();
356
357    foreach ($uris as $uri) {
358        if (in_array($uri, Auth_OpenID_getOpenIDTypeURIs())) {
359            return true;
360        }
361    }
362
363    return false;
364}
365
366function filter_MatchesAnyOpenIDConsumerType(&$service)
367{
368    $uris = $service->getTypes();
369
370    foreach ($uris as $uri) {
371        if (in_array($uri, Auth_OpenID_getOpenIDConsumerTypeURIs())) {
372            return true;
373        }
374    }
375
376    return false;
377}
378
379function Auth_OpenID_bestMatchingService($service, $preferred_types)
380{
381    // Return the index of the first matching type, or something
382    // higher if no type matches.
383    //
384    // This provides an ordering in which service elements that
385    // contain a type that comes earlier in the preferred types list
386    // come before service elements that come later. If a service
387    // element has more than one type, the most preferred one wins.
388
389    foreach ($preferred_types as $index => $typ) {
390        if (in_array($typ, $service->type_uris)) {
391            return $index;
392        }
393    }
394
395    return count($preferred_types);
396}
397
398function Auth_OpenID_arrangeByType($service_list, $preferred_types)
399{
400    // Rearrange service_list in a new list so services are ordered by
401    // types listed in preferred_types.  Return the new list.
402
403    // Build a list with the service elements in tuples whose
404    // comparison will prefer the one with the best matching service
405    $prio_services = array();
406    foreach ($service_list as $index => $service) {
407        $prio_services[] = array(Auth_OpenID_bestMatchingService($service,
408                                                        $preferred_types),
409                                 $index, $service);
410    }
411
412    sort($prio_services);
413
414    // Now that the services are sorted by priority, remove the sort
415    // keys from the list.
416    foreach ($prio_services as $index => $s) {
417        $prio_services[$index] = $prio_services[$index][2];
418    }
419
420    return $prio_services;
421}
422
423// Extract OP Identifier services.  If none found, return the rest,
424// sorted with most preferred first according to
425// OpenIDServiceEndpoint.openid_type_uris.
426//
427// openid_services is a list of OpenIDServiceEndpoint objects.
428//
429// Returns a list of OpenIDServiceEndpoint objects."""
430function Auth_OpenID_getOPOrUserServices($openid_services)
431{
432    $op_services = Auth_OpenID_arrangeByType($openid_services,
433                                     array(Auth_OpenID_TYPE_2_0_IDP));
434
435    $openid_services = Auth_OpenID_arrangeByType($openid_services,
436                                     Auth_OpenID_getOpenIDTypeURIs());
437
438    if ($op_services) {
439        return $op_services;
440    } else {
441        return $openid_services;
442    }
443}
444
445function Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services)
446{
447    $s = array();
448
449    if (!$yadis_services) {
450        return $s;
451    }
452
453    foreach ($yadis_services as $service) {
454        $type_uris = $service->getTypes();
455        $uris = $service->getURIs();
456
457        // If any Type URIs match and there is an endpoint URI
458        // specified, then this is an OpenID endpoint
459        if ($type_uris &&
460            $uris) {
461            foreach ($uris as $service_uri) {
462                $openid_endpoint = new Auth_OpenID_ServiceEndpoint();
463                if ($openid_endpoint->parseService($uri,
464                                                   $service_uri,
465                                                   $type_uris,
466                                                   $service)) {
467                    $s[] = $openid_endpoint;
468                }
469            }
470        }
471    }
472
473    return $s;
474}
475
476function Auth_OpenID_discoverWithYadis($uri, $fetcher,
477              $endpoint_filter='Auth_OpenID_getOPOrUserServices',
478              $discover_function=null)
479{
480    // Discover OpenID services for a URI. Tries Yadis and falls back
481    // on old-style <link rel='...'> discovery if Yadis fails.
482
483    // Might raise a yadis.discover.DiscoveryFailure if no document
484    // came back for that URI at all.  I don't think falling back to
485    // OpenID 1.0 discovery on the same URL will help, so don't bother
486    // to catch it.
487    if ($discover_function === null) {
488        $discover_function = array('Auth_Yadis_Yadis', 'discover');
489    }
490
491    $openid_services = array();
492
493    $response = call_user_func_array($discover_function,
494                                     array($uri, $fetcher));
495
496    $yadis_url = $response->normalized_uri;
497    $yadis_services = array();
498
499    if ($response->isFailure() && !$response->isXRDS()) {
500        return array($uri, array());
501    }
502
503    $openid_services = Auth_OpenID_ServiceEndpoint::fromXRDS(
504                                         $yadis_url,
505                                         $response->response_text);
506
507    if (!$openid_services) {
508        if ($response->isXRDS()) {
509            return Auth_OpenID_discoverWithoutYadis($uri,
510                                                    $fetcher);
511        }
512
513        // Try to parse the response as HTML to get OpenID 1.0/1.1
514        // <link rel="...">
515        $openid_services = Auth_OpenID_ServiceEndpoint::fromHTML(
516                                        $yadis_url,
517                                        $response->response_text);
518    }
519
520    $openid_services = call_user_func_array($endpoint_filter,
521                                            array($openid_services));
522
523    return array($yadis_url, $openid_services);
524}
525
526function Auth_OpenID_discoverURI($uri, $fetcher)
527{
528    $uri = Auth_OpenID::normalizeUrl($uri);
529    return Auth_OpenID_discoverWithYadis($uri, $fetcher);
530}
531
532function Auth_OpenID_discoverWithoutYadis($uri, $fetcher)
533{
534    $http_resp = @$fetcher->get($uri);
535
536    if ($http_resp->status != 200 and $http_resp->status != 206) {
537        return array($uri, array());
538    }
539
540    $identity_url = $http_resp->final_url;
541
542    // Try to parse the response as HTML to get OpenID 1.0/1.1 <link
543    // rel="...">
544    $openid_services = Auth_OpenID_ServiceEndpoint::fromHTML(
545                                           $identity_url,
546                                           $http_resp->body);
547
548    return array($identity_url, $openid_services);
549}
550
551function Auth_OpenID_discoverXRI($iname, $fetcher)
552{
553    $resolver = new Auth_Yadis_ProxyResolver($fetcher);
554    list($canonicalID, $yadis_services) =
555        $resolver->query($iname,
556                         Auth_OpenID_getOpenIDTypeURIs(),
557                         array('filter_MatchesAnyOpenIDType'));
558
559    $openid_services = Auth_OpenID_makeOpenIDEndpoints($iname,
560                                                       $yadis_services);
561
562    $openid_services = Auth_OpenID_getOPOrUserServices($openid_services);
563
564    for ($i = 0; $i < count($openid_services); $i++) {
565        $openid_services[$i]->canonicalID = $canonicalID;
566        $openid_services[$i]->claimed_id = $canonicalID;
567        $openid_services[$i]->display_identifier = $iname;
568    }
569
570    // FIXME: returned xri should probably be in some normal form
571    return array($iname, $openid_services);
572}
573
574function Auth_OpenID_discover($uri, $fetcher)
575{
576    // If the fetcher (i.e., PHP) doesn't support SSL, we can't do
577    // discovery on an HTTPS URL.
578    if ($fetcher->isHTTPS($uri) && !$fetcher->supportsSSL()) {
579        return array($uri, array());
580    }
581
582    if (Auth_Yadis_identifierScheme($uri) == 'XRI') {
583        $result = Auth_OpenID_discoverXRI($uri, $fetcher);
584    } else {
585        $result = Auth_OpenID_discoverURI($uri, $fetcher);
586    }
587
588    // If the fetcher doesn't support SSL, we can't interact with
589    // HTTPS server URLs; remove those endpoints from the list.
590    if (!$fetcher->supportsSSL()) {
591        $http_endpoints = array();
592        list($new_uri, $endpoints) = $result;
593
594        foreach ($endpoints as $e) {
595            if (!$fetcher->isHTTPS($e->server_url)) {
596                $http_endpoints[] = $e;
597            }
598        }
599
600        $result = array($new_uri, $http_endpoints);
601    }
602
603    return $result;
604}
605
606
607