2d920e05d5
Upstream release was tagged at: a287b2d85e
607 lines
18 KiB
PHP
607 lines
18 KiB
PHP
<?php
|
|
|
|
/**
|
|
* The OpenID and Yadis discovery implementation for OpenID 1.2.
|
|
*/
|
|
|
|
require_once "Auth/OpenID.php";
|
|
require_once "Auth/OpenID/Parse.php";
|
|
require_once "Auth/OpenID/Message.php";
|
|
require_once "Auth/Yadis/XRIRes.php";
|
|
require_once "Auth/Yadis/Yadis.php";
|
|
|
|
// XML namespace value
|
|
define('Auth_OpenID_XMLNS_1_0', 'http://openid.net/xmlns/1.0');
|
|
|
|
// Yadis service types
|
|
define('Auth_OpenID_TYPE_1_2', 'http://openid.net/signon/1.2');
|
|
define('Auth_OpenID_TYPE_1_1', 'http://openid.net/signon/1.1');
|
|
define('Auth_OpenID_TYPE_1_0', 'http://openid.net/signon/1.0');
|
|
define('Auth_OpenID_TYPE_2_0_IDP', 'http://specs.openid.net/auth/2.0/server');
|
|
define('Auth_OpenID_TYPE_2_0', 'http://specs.openid.net/auth/2.0/signon');
|
|
define('Auth_OpenID_RP_RETURN_TO_URL_TYPE',
|
|
'http://specs.openid.net/auth/2.0/return_to');
|
|
|
|
function Auth_OpenID_getOpenIDTypeURIs()
|
|
{
|
|
return array(Auth_OpenID_TYPE_2_0_IDP,
|
|
Auth_OpenID_TYPE_2_0,
|
|
Auth_OpenID_TYPE_1_2,
|
|
Auth_OpenID_TYPE_1_1,
|
|
Auth_OpenID_TYPE_1_0);
|
|
}
|
|
|
|
function Auth_OpenID_getOpenIDConsumerTypeURIs()
|
|
{
|
|
return array(Auth_OpenID_RP_RETURN_TO_URL_TYPE);
|
|
}
|
|
|
|
|
|
/*
|
|
* Provides a user-readable interpretation of a type uri.
|
|
* Useful for error messages.
|
|
*/
|
|
function Auth_OpenID_getOpenIDTypeName($type_uri) {
|
|
switch ($type_uri) {
|
|
case Auth_OpenID_TYPE_2_0_IDP:
|
|
return 'OpenID 2.0 IDP';
|
|
case Auth_OpenID_TYPE_2_0:
|
|
return 'OpenID 2.0';
|
|
case Auth_OpenID_TYPE_1_2:
|
|
return 'OpenID 1.2';
|
|
case Auth_OpenID_TYPE_1_1:
|
|
return 'OpenID 1.1';
|
|
case Auth_OpenID_TYPE_1_0:
|
|
return 'OpenID 1.0';
|
|
case Auth_OpenID_RP_RETURN_TO_URL_TYPE:
|
|
return 'OpenID relying party';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Object representing an OpenID service endpoint.
|
|
*/
|
|
class Auth_OpenID_ServiceEndpoint {
|
|
function Auth_OpenID_ServiceEndpoint()
|
|
{
|
|
$this->claimed_id = null;
|
|
$this->server_url = null;
|
|
$this->type_uris = array();
|
|
$this->local_id = null;
|
|
$this->canonicalID = null;
|
|
$this->used_yadis = false; // whether this came from an XRDS
|
|
$this->display_identifier = null;
|
|
}
|
|
|
|
function getDisplayIdentifier()
|
|
{
|
|
if ($this->display_identifier) {
|
|
return $this->display_identifier;
|
|
}
|
|
if (! $this->claimed_id) {
|
|
return $this->claimed_id;
|
|
}
|
|
$parsed = parse_url($this->claimed_id);
|
|
$scheme = $parsed['scheme'];
|
|
$host = $parsed['host'];
|
|
$path = $parsed['path'];
|
|
if (array_key_exists('query', $parsed)) {
|
|
$query = $parsed['query'];
|
|
$no_frag = "$scheme://$host$path?$query";
|
|
} else {
|
|
$no_frag = "$scheme://$host$path";
|
|
}
|
|
return $no_frag;
|
|
}
|
|
|
|
function usesExtension($extension_uri)
|
|
{
|
|
return in_array($extension_uri, $this->type_uris);
|
|
}
|
|
|
|
function preferredNamespace()
|
|
{
|
|
if (in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris) ||
|
|
in_array(Auth_OpenID_TYPE_2_0, $this->type_uris)) {
|
|
return Auth_OpenID_OPENID2_NS;
|
|
} else {
|
|
return Auth_OpenID_OPENID1_NS;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Query this endpoint to see if it has any of the given type
|
|
* URIs. This is useful for implementing other endpoint classes
|
|
* that e.g. need to check for the presence of multiple versions
|
|
* of a single protocol.
|
|
*
|
|
* @param $type_uris The URIs that you wish to check
|
|
*
|
|
* @return all types that are in both in type_uris and
|
|
* $this->type_uris
|
|
*/
|
|
function matchTypes($type_uris)
|
|
{
|
|
$result = array();
|
|
foreach ($type_uris as $test_uri) {
|
|
if ($this->supportsType($test_uri)) {
|
|
$result[] = $test_uri;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
function supportsType($type_uri)
|
|
{
|
|
// Does this endpoint support this type?
|
|
return ((in_array($type_uri, $this->type_uris)) ||
|
|
(($type_uri == Auth_OpenID_TYPE_2_0) &&
|
|
$this->isOPIdentifier()));
|
|
}
|
|
|
|
function compatibilityMode()
|
|
{
|
|
return $this->preferredNamespace() != Auth_OpenID_OPENID2_NS;
|
|
}
|
|
|
|
function isOPIdentifier()
|
|
{
|
|
return in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris);
|
|
}
|
|
|
|
static function fromOPEndpointURL($op_endpoint_url)
|
|
{
|
|
// Construct an OP-Identifier OpenIDServiceEndpoint object for
|
|
// a given OP Endpoint URL
|
|
$obj = new Auth_OpenID_ServiceEndpoint();
|
|
$obj->server_url = $op_endpoint_url;
|
|
$obj->type_uris = array(Auth_OpenID_TYPE_2_0_IDP);
|
|
return $obj;
|
|
}
|
|
|
|
function parseService($yadis_url, $uri, $type_uris, $service_element)
|
|
{
|
|
// Set the state of this object based on the contents of the
|
|
// service element. Return true if successful, false if not
|
|
// (if findOPLocalIdentifier returns false).
|
|
$this->type_uris = $type_uris;
|
|
$this->server_url = $uri;
|
|
$this->used_yadis = true;
|
|
|
|
if (!$this->isOPIdentifier()) {
|
|
$this->claimed_id = $yadis_url;
|
|
$this->local_id = Auth_OpenID_findOPLocalIdentifier(
|
|
$service_element,
|
|
$this->type_uris);
|
|
if ($this->local_id === false) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function getLocalID()
|
|
{
|
|
// Return the identifier that should be sent as the
|
|
// openid.identity_url parameter to the server.
|
|
if ($this->local_id === null && $this->canonicalID === null) {
|
|
return $this->claimed_id;
|
|
} else {
|
|
if ($this->local_id) {
|
|
return $this->local_id;
|
|
} else {
|
|
return $this->canonicalID;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Parse the given document as XRDS looking for OpenID consumer services.
|
|
*
|
|
* @return array of Auth_OpenID_ServiceEndpoint or null if the
|
|
* document cannot be parsed.
|
|
*/
|
|
function consumerFromXRDS($uri, $xrds_text)
|
|
{
|
|
$xrds =& Auth_Yadis_XRDS::parseXRDS($xrds_text);
|
|
|
|
if ($xrds) {
|
|
$yadis_services =
|
|
$xrds->services(array('filter_MatchesAnyOpenIDConsumerType'));
|
|
return Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/*
|
|
* Parse the given document as XRDS looking for OpenID services.
|
|
*
|
|
* @return array of Auth_OpenID_ServiceEndpoint or null if the
|
|
* document cannot be parsed.
|
|
*/
|
|
static function fromXRDS($uri, $xrds_text)
|
|
{
|
|
$xrds = Auth_Yadis_XRDS::parseXRDS($xrds_text);
|
|
|
|
if ($xrds) {
|
|
$yadis_services =
|
|
$xrds->services(array('filter_MatchesAnyOpenIDType'));
|
|
return Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/*
|
|
* Create endpoints from a DiscoveryResult.
|
|
*
|
|
* @param discoveryResult Auth_Yadis_DiscoveryResult
|
|
* @return array of Auth_OpenID_ServiceEndpoint or null if
|
|
* endpoints cannot be created.
|
|
*/
|
|
static function fromDiscoveryResult($discoveryResult)
|
|
{
|
|
if ($discoveryResult->isXRDS()) {
|
|
return Auth_OpenID_ServiceEndpoint::fromXRDS(
|
|
$discoveryResult->normalized_uri,
|
|
$discoveryResult->response_text);
|
|
} else {
|
|
return Auth_OpenID_ServiceEndpoint::fromHTML(
|
|
$discoveryResult->normalized_uri,
|
|
$discoveryResult->response_text);
|
|
}
|
|
}
|
|
|
|
static function fromHTML($uri, $html)
|
|
{
|
|
$discovery_types = array(
|
|
array(Auth_OpenID_TYPE_2_0,
|
|
'openid2.provider', 'openid2.local_id'),
|
|
array(Auth_OpenID_TYPE_1_1,
|
|
'openid.server', 'openid.delegate')
|
|
);
|
|
|
|
$services = array();
|
|
|
|
foreach ($discovery_types as $triple) {
|
|
list($type_uri, $server_rel, $delegate_rel) = $triple;
|
|
|
|
$urls = Auth_OpenID_legacy_discover($html, $server_rel,
|
|
$delegate_rel);
|
|
|
|
if ($urls === false) {
|
|
continue;
|
|
}
|
|
|
|
list($delegate_url, $server_url) = $urls;
|
|
|
|
$service = new Auth_OpenID_ServiceEndpoint();
|
|
$service->claimed_id = $uri;
|
|
$service->local_id = $delegate_url;
|
|
$service->server_url = $server_url;
|
|
$service->type_uris = array($type_uri);
|
|
|
|
$services[] = $service;
|
|
}
|
|
|
|
return $services;
|
|
}
|
|
|
|
function copy()
|
|
{
|
|
$x = new Auth_OpenID_ServiceEndpoint();
|
|
|
|
$x->claimed_id = $this->claimed_id;
|
|
$x->server_url = $this->server_url;
|
|
$x->type_uris = $this->type_uris;
|
|
$x->local_id = $this->local_id;
|
|
$x->canonicalID = $this->canonicalID;
|
|
$x->used_yadis = $this->used_yadis;
|
|
|
|
return $x;
|
|
}
|
|
}
|
|
|
|
function Auth_OpenID_findOPLocalIdentifier($service, $type_uris)
|
|
{
|
|
// Extract a openid:Delegate value from a Yadis Service element.
|
|
// If no delegate is found, returns null. Returns false on
|
|
// discovery failure (when multiple delegate/localID tags have
|
|
// different values).
|
|
|
|
$service->parser->registerNamespace('openid',
|
|
Auth_OpenID_XMLNS_1_0);
|
|
|
|
$service->parser->registerNamespace('xrd',
|
|
Auth_Yadis_XMLNS_XRD_2_0);
|
|
|
|
$parser = $service->parser;
|
|
|
|
$permitted_tags = array();
|
|
|
|
if (in_array(Auth_OpenID_TYPE_1_1, $type_uris) ||
|
|
in_array(Auth_OpenID_TYPE_1_0, $type_uris)) {
|
|
$permitted_tags[] = 'openid:Delegate';
|
|
}
|
|
|
|
if (in_array(Auth_OpenID_TYPE_2_0, $type_uris)) {
|
|
$permitted_tags[] = 'xrd:LocalID';
|
|
}
|
|
|
|
$local_id = null;
|
|
|
|
foreach ($permitted_tags as $tag_name) {
|
|
$tags = $service->getElements($tag_name);
|
|
|
|
foreach ($tags as $tag) {
|
|
$content = $parser->content($tag);
|
|
|
|
if ($local_id === null) {
|
|
$local_id = $content;
|
|
} else if ($local_id != $content) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $local_id;
|
|
}
|
|
|
|
function filter_MatchesAnyOpenIDType($service)
|
|
{
|
|
$uris = $service->getTypes();
|
|
|
|
foreach ($uris as $uri) {
|
|
if (in_array($uri, Auth_OpenID_getOpenIDTypeURIs())) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function filter_MatchesAnyOpenIDConsumerType(&$service)
|
|
{
|
|
$uris = $service->getTypes();
|
|
|
|
foreach ($uris as $uri) {
|
|
if (in_array($uri, Auth_OpenID_getOpenIDConsumerTypeURIs())) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function Auth_OpenID_bestMatchingService($service, $preferred_types)
|
|
{
|
|
// Return the index of the first matching type, or something
|
|
// higher if no type matches.
|
|
//
|
|
// This provides an ordering in which service elements that
|
|
// contain a type that comes earlier in the preferred types list
|
|
// come before service elements that come later. If a service
|
|
// element has more than one type, the most preferred one wins.
|
|
|
|
foreach ($preferred_types as $index => $typ) {
|
|
if (in_array($typ, $service->type_uris)) {
|
|
return $index;
|
|
}
|
|
}
|
|
|
|
return count($preferred_types);
|
|
}
|
|
|
|
function Auth_OpenID_arrangeByType($service_list, $preferred_types)
|
|
{
|
|
// Rearrange service_list in a new list so services are ordered by
|
|
// types listed in preferred_types. Return the new list.
|
|
|
|
// Build a list with the service elements in tuples whose
|
|
// comparison will prefer the one with the best matching service
|
|
$prio_services = array();
|
|
foreach ($service_list as $index => $service) {
|
|
$prio_services[] = array(Auth_OpenID_bestMatchingService($service,
|
|
$preferred_types),
|
|
$index, $service);
|
|
}
|
|
|
|
sort($prio_services);
|
|
|
|
// Now that the services are sorted by priority, remove the sort
|
|
// keys from the list.
|
|
foreach ($prio_services as $index => $s) {
|
|
$prio_services[$index] = $prio_services[$index][2];
|
|
}
|
|
|
|
return $prio_services;
|
|
}
|
|
|
|
// Extract OP Identifier services. If none found, return the rest,
|
|
// sorted with most preferred first according to
|
|
// OpenIDServiceEndpoint.openid_type_uris.
|
|
//
|
|
// openid_services is a list of OpenIDServiceEndpoint objects.
|
|
//
|
|
// Returns a list of OpenIDServiceEndpoint objects."""
|
|
function Auth_OpenID_getOPOrUserServices($openid_services)
|
|
{
|
|
$op_services = Auth_OpenID_arrangeByType($openid_services,
|
|
array(Auth_OpenID_TYPE_2_0_IDP));
|
|
|
|
$openid_services = Auth_OpenID_arrangeByType($openid_services,
|
|
Auth_OpenID_getOpenIDTypeURIs());
|
|
|
|
if ($op_services) {
|
|
return $op_services;
|
|
} else {
|
|
return $openid_services;
|
|
}
|
|
}
|
|
|
|
function Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services)
|
|
{
|
|
$s = array();
|
|
|
|
if (!$yadis_services) {
|
|
return $s;
|
|
}
|
|
|
|
foreach ($yadis_services as $service) {
|
|
$type_uris = $service->getTypes();
|
|
$uris = $service->getURIs();
|
|
|
|
// If any Type URIs match and there is an endpoint URI
|
|
// specified, then this is an OpenID endpoint
|
|
if ($type_uris &&
|
|
$uris) {
|
|
foreach ($uris as $service_uri) {
|
|
$openid_endpoint = new Auth_OpenID_ServiceEndpoint();
|
|
if ($openid_endpoint->parseService($uri,
|
|
$service_uri,
|
|
$type_uris,
|
|
$service)) {
|
|
$s[] = $openid_endpoint;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $s;
|
|
}
|
|
|
|
function Auth_OpenID_discoverWithYadis($uri, $fetcher,
|
|
$endpoint_filter='Auth_OpenID_getOPOrUserServices',
|
|
$discover_function=null)
|
|
{
|
|
// Discover OpenID services for a URI. Tries Yadis and falls back
|
|
// on old-style <link rel='...'> discovery if Yadis fails.
|
|
|
|
// Might raise a yadis.discover.DiscoveryFailure if no document
|
|
// came back for that URI at all. I don't think falling back to
|
|
// OpenID 1.0 discovery on the same URL will help, so don't bother
|
|
// to catch it.
|
|
if ($discover_function === null) {
|
|
$discover_function = array('Auth_Yadis_Yadis', 'discover');
|
|
}
|
|
|
|
$openid_services = array();
|
|
|
|
$response = call_user_func_array($discover_function,
|
|
array($uri, $fetcher));
|
|
|
|
$yadis_url = $response->normalized_uri;
|
|
$yadis_services = array();
|
|
|
|
if ($response->isFailure() && !$response->isXRDS()) {
|
|
return array($uri, array());
|
|
}
|
|
|
|
$openid_services = Auth_OpenID_ServiceEndpoint::fromXRDS(
|
|
$yadis_url,
|
|
$response->response_text);
|
|
|
|
if (!$openid_services) {
|
|
if ($response->isXRDS()) {
|
|
return Auth_OpenID_discoverWithoutYadis($uri,
|
|
$fetcher);
|
|
}
|
|
|
|
// Try to parse the response as HTML to get OpenID 1.0/1.1
|
|
// <link rel="...">
|
|
$openid_services = Auth_OpenID_ServiceEndpoint::fromHTML(
|
|
$yadis_url,
|
|
$response->response_text);
|
|
}
|
|
|
|
$openid_services = call_user_func_array($endpoint_filter,
|
|
array($openid_services));
|
|
|
|
return array($yadis_url, $openid_services);
|
|
}
|
|
|
|
function Auth_OpenID_discoverURI($uri, $fetcher)
|
|
{
|
|
$uri = Auth_OpenID::normalizeUrl($uri);
|
|
return Auth_OpenID_discoverWithYadis($uri, $fetcher);
|
|
}
|
|
|
|
function Auth_OpenID_discoverWithoutYadis($uri, $fetcher)
|
|
{
|
|
$http_resp = @$fetcher->get($uri);
|
|
|
|
if ($http_resp->status != 200 and $http_resp->status != 206) {
|
|
return array($uri, array());
|
|
}
|
|
|
|
$identity_url = $http_resp->final_url;
|
|
|
|
// Try to parse the response as HTML to get OpenID 1.0/1.1 <link
|
|
// rel="...">
|
|
$openid_services = Auth_OpenID_ServiceEndpoint::fromHTML(
|
|
$identity_url,
|
|
$http_resp->body);
|
|
|
|
return array($identity_url, $openid_services);
|
|
}
|
|
|
|
function Auth_OpenID_discoverXRI($iname, $fetcher)
|
|
{
|
|
$resolver = new Auth_Yadis_ProxyResolver($fetcher);
|
|
list($canonicalID, $yadis_services) =
|
|
$resolver->query($iname,
|
|
Auth_OpenID_getOpenIDTypeURIs(),
|
|
array('filter_MatchesAnyOpenIDType'));
|
|
|
|
$openid_services = Auth_OpenID_makeOpenIDEndpoints($iname,
|
|
$yadis_services);
|
|
|
|
$openid_services = Auth_OpenID_getOPOrUserServices($openid_services);
|
|
|
|
for ($i = 0; $i < count($openid_services); $i++) {
|
|
$openid_services[$i]->canonicalID = $canonicalID;
|
|
$openid_services[$i]->claimed_id = $canonicalID;
|
|
$openid_services[$i]->display_identifier = $iname;
|
|
}
|
|
|
|
// FIXME: returned xri should probably be in some normal form
|
|
return array($iname, $openid_services);
|
|
}
|
|
|
|
function Auth_OpenID_discover($uri, $fetcher)
|
|
{
|
|
// If the fetcher (i.e., PHP) doesn't support SSL, we can't do
|
|
// discovery on an HTTPS URL.
|
|
if ($fetcher->isHTTPS($uri) && !$fetcher->supportsSSL()) {
|
|
return array($uri, array());
|
|
}
|
|
|
|
if (Auth_Yadis_identifierScheme($uri) == 'XRI') {
|
|
$result = Auth_OpenID_discoverXRI($uri, $fetcher);
|
|
} else {
|
|
$result = Auth_OpenID_discoverURI($uri, $fetcher);
|
|
}
|
|
|
|
// If the fetcher doesn't support SSL, we can't interact with
|
|
// HTTPS server URLs; remove those endpoints from the list.
|
|
if (!$fetcher->supportsSSL()) {
|
|
$http_endpoints = array();
|
|
list($new_uri, $endpoints) = $result;
|
|
|
|
foreach ($endpoints as $e) {
|
|
if (!$fetcher->isHTTPS($e->server_url)) {
|
|
$http_endpoints[] = $e;
|
|
}
|
|
}
|
|
|
|
$result = array($new_uri, $http_endpoints);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|