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