Package openid :: Package consumer :: Module discover
[frames] | no frames]

Source Code for Module openid.consumer.discover

  1  # -*- test-case-name: openid.test.test_discover -*- 
  2  """Functions to discover OpenID endpoints from identifiers. 
  3  """ 
  4   
  5  __all__ = [ 
  6      'DiscoveryFailure', 
  7      'OPENID_1_0_NS', 
  8      'OPENID_1_0_TYPE', 
  9      'OPENID_1_1_TYPE', 
 10      'OPENID_2_0_TYPE', 
 11      'OPENID_IDP_2_0_TYPE', 
 12      'OpenIDServiceEndpoint', 
 13      'discover', 
 14      ] 
 15   
 16  import urlparse 
 17   
 18  from openid import oidutil, fetchers, urinorm 
 19   
 20  from openid import yadis 
 21  from openid.yadis.etxrd import nsTag, XRDSError, XRD_NS_2_0 
 22  from openid.yadis.services import applyFilter as extractServices 
 23  from openid.yadis.discover import discover as yadisDiscover 
 24  from openid.yadis.discover import DiscoveryFailure 
 25  from openid.yadis import xrires, filters 
 26  from openid.yadis import xri 
 27   
 28  from openid.consumer import html_parse 
 29   
 30  OPENID_1_0_NS = 'http://openid.net/xmlns/1.0' 
 31  OPENID_IDP_2_0_TYPE = 'http://specs.openid.net/auth/2.0/server' 
 32  OPENID_2_0_TYPE = 'http://specs.openid.net/auth/2.0/signon' 
 33  OPENID_1_1_TYPE = 'http://openid.net/signon/1.1' 
 34  OPENID_1_0_TYPE = 'http://openid.net/signon/1.0' 
 35   
 36  from openid.message import OPENID1_NS as OPENID_1_0_MESSAGE_NS 
 37  from openid.message import OPENID2_NS as OPENID_2_0_MESSAGE_NS 
 38   
39 -class OpenIDServiceEndpoint(object):
40 """Object representing an OpenID service endpoint. 41 42 @ivar identity_url: the verified identifier. 43 @ivar canonicalID: For XRI, the persistent identifier. 44 """ 45 46 # OpenID service type URIs, listed in order of preference. The 47 # ordering of this list affects yadis and XRI service discovery. 48 openid_type_uris = [ 49 OPENID_IDP_2_0_TYPE, 50 51 OPENID_2_0_TYPE, 52 OPENID_1_1_TYPE, 53 OPENID_1_0_TYPE, 54 ] 55
56 - def __init__(self):
57 self.claimed_id = None 58 self.server_url = None 59 self.type_uris = [] 60 self.local_id = None 61 self.canonicalID = None 62 self.used_yadis = False # whether this came from an XRDS 63 self.display_identifier = None
64
65 - def usesExtension(self, extension_uri):
66 return extension_uri in self.type_uris
67
68 - def preferredNamespace(self):
69 if (OPENID_IDP_2_0_TYPE in self.type_uris or 70 OPENID_2_0_TYPE in self.type_uris): 71 return OPENID_2_0_MESSAGE_NS 72 else: 73 return OPENID_1_0_MESSAGE_NS
74
75 - def supportsType(self, type_uri):
76 """Does this endpoint support this type? 77 78 I consider C{/server} endpoints to implicitly support C{/signon}. 79 """ 80 return ( 81 (type_uri in self.type_uris) or 82 (type_uri == OPENID_2_0_TYPE and self.isOPIdentifier()) 83 )
84
85 - def getDisplayIdentifier(self):
86 """Return the display_identifier if set, else return the claimed_id. 87 """ 88 if self.display_identifier is not None: 89 return self.display_identifier 90 if self.claimed_id is None: 91 return None 92 else: 93 return urlparse.urldefrag(self.claimed_id)[0]
94
95 - def compatibilityMode(self):
96 return self.preferredNamespace() != OPENID_2_0_MESSAGE_NS
97
98 - def isOPIdentifier(self):
99 return OPENID_IDP_2_0_TYPE in self.type_uris
100
101 - def parseService(self, yadis_url, uri, type_uris, service_element):
102 """Set the state of this object based on the contents of the 103 service element.""" 104 self.type_uris = type_uris 105 self.server_url = uri 106 self.used_yadis = True 107 108 if not self.isOPIdentifier(): 109 # XXX: This has crappy implications for Service elements 110 # that contain both 'server' and 'signon' Types. But 111 # that's a pathological configuration anyway, so I don't 112 # think I care. 113 self.local_id = findOPLocalIdentifier(service_element, 114 self.type_uris) 115 self.claimed_id = yadis_url
116
117 - def getLocalID(self):
118 """Return the identifier that should be sent as the 119 openid.identity parameter to the server.""" 120 # I looked at this conditional and thought "ah-hah! there's the bug!" 121 # but Python actually makes that one big expression somehow, i.e. 122 # "x is x is x" is not the same thing as "(x is x) is x". 123 # That's pretty weird, dude. -- kmt, 1/07 124 if (self.local_id is self.canonicalID is None): 125 return self.claimed_id 126 else: 127 return self.local_id or self.canonicalID
128
129 - def fromBasicServiceEndpoint(cls, endpoint):
130 """Create a new instance of this class from the endpoint 131 object passed in. 132 133 @return: None or OpenIDServiceEndpoint for this endpoint object""" 134 type_uris = endpoint.matchTypes(cls.openid_type_uris) 135 136 # If any Type URIs match and there is an endpoint URI 137 # specified, then this is an OpenID endpoint 138 if type_uris and endpoint.uri is not None: 139 openid_endpoint = cls() 140 openid_endpoint.parseService( 141 endpoint.yadis_url, 142 endpoint.uri, 143 endpoint.type_uris, 144 endpoint.service_element) 145 else: 146 openid_endpoint = None 147 148 return openid_endpoint
149 150 fromBasicServiceEndpoint = classmethod(fromBasicServiceEndpoint) 151
152 - def fromHTML(cls, uri, html):
153 """Parse the given document as HTML looking for an OpenID <link 154 rel=...> 155 156 @rtype: [OpenIDServiceEndpoint] 157 """ 158 discovery_types = [ 159 (OPENID_2_0_TYPE, 'openid2.provider', 'openid2.local_id'), 160 (OPENID_1_1_TYPE, 'openid.server', 'openid.delegate'), 161 ] 162 163 link_attrs = html_parse.parseLinkAttrs(html) 164 services = [] 165 for type_uri, op_endpoint_rel, local_id_rel in discovery_types: 166 op_endpoint_url = html_parse.findFirstHref( 167 link_attrs, op_endpoint_rel) 168 if op_endpoint_url is None: 169 continue 170 171 service = cls() 172 service.claimed_id = uri 173 service.local_id = html_parse.findFirstHref( 174 link_attrs, local_id_rel) 175 service.server_url = op_endpoint_url 176 service.type_uris = [type_uri] 177 178 services.append(service) 179 180 return services
181 182 fromHTML = classmethod(fromHTML) 183 184
185 - def fromXRDS(cls, uri, xrds):
186 """Parse the given document as XRDS looking for OpenID services. 187 188 @rtype: [OpenIDServiceEndpoint] 189 190 @raises XRDSError: When the XRDS does not parse. 191 192 @since: 2.1.0 193 """ 194 return extractServices(uri, xrds, cls)
195 196 fromXRDS = classmethod(fromXRDS) 197 198
199 - def fromDiscoveryResult(cls, discoveryResult):
200 """Create endpoints from a DiscoveryResult. 201 202 @type discoveryResult: L{DiscoveryResult} 203 204 @rtype: list of L{OpenIDServiceEndpoint} 205 206 @raises XRDSError: When the XRDS does not parse. 207 208 @since: 2.1.0 209 """ 210 if discoveryResult.isXRDS(): 211 method = cls.fromXRDS 212 else: 213 method = cls.fromHTML 214 return method(discoveryResult.normalized_uri, 215 discoveryResult.response_text)
216 217 fromDiscoveryResult = classmethod(fromDiscoveryResult) 218 219
220 - def fromOPEndpointURL(cls, op_endpoint_url):
221 """Construct an OP-Identifier OpenIDServiceEndpoint object for 222 a given OP Endpoint URL 223 224 @param op_endpoint_url: The URL of the endpoint 225 @rtype: OpenIDServiceEndpoint 226 """ 227 service = cls() 228 service.server_url = op_endpoint_url 229 service.type_uris = [OPENID_IDP_2_0_TYPE] 230 return service
231 232 fromOPEndpointURL = classmethod(fromOPEndpointURL) 233 234
235 - def __str__(self):
236 return ("<%s.%s " 237 "server_url=%r " 238 "claimed_id=%r " 239 "local_id=%r " 240 "canonicalID=%r " 241 "used_yadis=%s " 242 ">" 243 % (self.__class__.__module__, self.__class__.__name__, 244 self.server_url, 245 self.claimed_id, 246 self.local_id, 247 self.canonicalID, 248 self.used_yadis))
249 250 251
252 -def findOPLocalIdentifier(service_element, type_uris):
253 """Find the OP-Local Identifier for this xrd:Service element. 254 255 This considers openid:Delegate to be a synonym for xrd:LocalID if 256 both OpenID 1.X and OpenID 2.0 types are present. If only OpenID 257 1.X is present, it returns the value of openid:Delegate. If only 258 OpenID 2.0 is present, it returns the value of xrd:LocalID. If 259 there is more than one LocalID tag and the values are different, 260 it raises a DiscoveryFailure. This is also triggered when the 261 xrd:LocalID and openid:Delegate tags are different. 262 263 @param service_element: The xrd:Service element 264 @type service_element: ElementTree.Node 265 266 @param type_uris: The xrd:Type values present in this service 267 element. This function could extract them, but higher level 268 code needs to do that anyway. 269 @type type_uris: [str] 270 271 @raises DiscoveryFailure: when discovery fails. 272 273 @returns: The OP-Local Identifier for this service element, if one 274 is present, or None otherwise. 275 @rtype: str or unicode or NoneType 276 """ 277 # XXX: Test this function on its own! 278 279 # Build the list of tags that could contain the OP-Local Identifier 280 local_id_tags = [] 281 if (OPENID_1_1_TYPE in type_uris or 282 OPENID_1_0_TYPE in type_uris): 283 local_id_tags.append(nsTag(OPENID_1_0_NS, 'Delegate')) 284 285 if OPENID_2_0_TYPE in type_uris: 286 local_id_tags.append(nsTag(XRD_NS_2_0, 'LocalID')) 287 288 # Walk through all the matching tags and make sure that they all 289 # have the same value 290 local_id = None 291 for local_id_tag in local_id_tags: 292 for local_id_element in service_element.findall(local_id_tag): 293 if local_id is None: 294 local_id = local_id_element.text 295 elif local_id != local_id_element.text: 296 format = 'More than one %r tag found in one service element' 297 message = format % (local_id_tag,) 298 raise DiscoveryFailure(message, None) 299 300 return local_id
301
302 -def normalizeURL(url):
303 """Normalize a URL, converting normalization failures to 304 DiscoveryFailure""" 305 try: 306 normalized = urinorm.urinorm(url) 307 except ValueError, why: 308 raise DiscoveryFailure('Normalizing identifier: %s' % (why[0],), None) 309 else: 310 return urlparse.urldefrag(normalized)[0]
311
312 -def normalizeXRI(xri):
313 """Normalize an XRI, stripping its scheme if present""" 314 if xri.startswith("xri://"): 315 xri = xri[6:] 316 return xri
317
318 -def arrangeByType(service_list, preferred_types):
319 """Rearrange service_list in a new list so services are ordered by 320 types listed in preferred_types. Return the new list.""" 321 322 def enumerate(elts): 323 """Return an iterable that pairs the index of an element with 324 that element. 325 326 For Python 2.2 compatibility""" 327 return zip(range(len(elts)), elts)
328 329 def bestMatchingService(service): 330 """Return the index of the first matching type, or something 331 higher if no type matches. 332 333 This provides an ordering in which service elements that 334 contain a type that comes earlier in the preferred types list 335 come before service elements that come later. If a service 336 element has more than one type, the most preferred one wins. 337 """ 338 for i, t in enumerate(preferred_types): 339 if preferred_types[i] in service.type_uris: 340 return i 341 342 return len(preferred_types) 343 344 # Build a list with the service elements in tuples whose 345 # comparison will prefer the one with the best matching service 346 prio_services = [(bestMatchingService(s), orig_index, s) 347 for (orig_index, s) in enumerate(service_list)] 348 prio_services.sort() 349 350 # Now that the services are sorted by priority, remove the sort 351 # keys from the list. 352 for i in range(len(prio_services)): 353 prio_services[i] = prio_services[i][2] 354 355 return prio_services 356
357 -def getOPOrUserServices(openid_services):
358 """Extract OP Identifier services. If none found, return the 359 rest, sorted with most preferred first according to 360 OpenIDServiceEndpoint.openid_type_uris. 361 362 openid_services is a list of OpenIDServiceEndpoint objects. 363 364 Returns a list of OpenIDServiceEndpoint objects.""" 365 366 op_services = arrangeByType(openid_services, [OPENID_IDP_2_0_TYPE]) 367 368 openid_services = arrangeByType(openid_services, 369 OpenIDServiceEndpoint.openid_type_uris) 370 371 return op_services or openid_services
372
373 -def discoverYadis(uri):
374 """Discover OpenID services for a URI. Tries Yadis and falls back 375 on old-style <link rel='...'> discovery if Yadis fails. 376 377 @param uri: normalized identity URL 378 @type uri: str 379 380 @return: (claimed_id, services) 381 @rtype: (str, list(OpenIDServiceEndpoint)) 382 383 @raises DiscoveryFailure: when discovery fails. 384 """ 385 # Might raise a yadis.discover.DiscoveryFailure if no document 386 # came back for that URI at all. I don't think falling back 387 # to OpenID 1.0 discovery on the same URL will help, so don't 388 # bother to catch it. 389 response = yadisDiscover(uri) 390 391 yadis_url = response.normalized_uri 392 body = response.response_text 393 try: 394 openid_services = OpenIDServiceEndpoint.fromXRDS(yadis_url, body) 395 except XRDSError: 396 # Does not parse as a Yadis XRDS file 397 openid_services = [] 398 399 if not openid_services: 400 # Either not an XRDS or there are no OpenID services. 401 402 if response.isXRDS(): 403 # if we got the Yadis content-type or followed the Yadis 404 # header, re-fetch the document without following the Yadis 405 # header, with no Accept header. 406 return discoverNoYadis(uri) 407 408 # Try to parse the response as HTML. 409 # <link rel="..."> 410 openid_services = OpenIDServiceEndpoint.fromHTML(yadis_url, body) 411 412 return (yadis_url, getOPOrUserServices(openid_services))
413
414 -def discoverXRI(iname):
415 endpoints = [] 416 iname = normalizeXRI(iname) 417 try: 418 canonicalID, services = xrires.ProxyResolver().query( 419 iname, OpenIDServiceEndpoint.openid_type_uris) 420 421 if canonicalID is None: 422 raise XRDSError('No CanonicalID found for XRI %r' % (iname,)) 423 424 flt = filters.mkFilter(OpenIDServiceEndpoint) 425 for service_element in services: 426 endpoints.extend(flt.getServiceEndpoints(iname, service_element)) 427 except XRDSError: 428 oidutil.log('xrds error on ' + iname) 429 430 for endpoint in endpoints: 431 # Is there a way to pass this through the filter to the endpoint 432 # constructor instead of tacking it on after? 433 endpoint.canonicalID = canonicalID 434 endpoint.claimed_id = canonicalID 435 endpoint.display_identifier = iname 436 437 # FIXME: returned xri should probably be in some normal form 438 return iname, getOPOrUserServices(endpoints)
439 440
441 -def discoverNoYadis(uri):
442 http_resp = fetchers.fetch(uri) 443 if http_resp.status not in (200, 206): 444 raise DiscoveryFailure( 445 'HTTP Response status from identity URL host is not 200. ' 446 'Got status %r' % (http_resp.status,), http_resp) 447 448 claimed_id = http_resp.final_url 449 openid_services = OpenIDServiceEndpoint.fromHTML( 450 claimed_id, http_resp.body) 451 return claimed_id, openid_services
452
453 -def discoverURI(uri):
454 parsed = urlparse.urlparse(uri) 455 if parsed[0] and parsed[1]: 456 if parsed[0] not in ['http', 'https']: 457 raise DiscoveryFailure('URI scheme is not HTTP or HTTPS', None) 458 else: 459 uri = 'http://' + uri 460 461 uri = normalizeURL(uri) 462 claimed_id, openid_services = discoverYadis(uri) 463 claimed_id = normalizeURL(claimed_id) 464 return claimed_id, openid_services
465
466 -def discover(identifier):
467 if xri.identifierScheme(identifier) == "XRI": 468 return discoverXRI(identifier) 469 else: 470 return discoverURI(identifier)
471