1
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
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
47
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
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
63 self.display_identifier = None
64
67
74
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
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
97
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
110
111
112
113 self.local_id = findOPLocalIdentifier(service_element,
114 self.type_uris)
115 self.claimed_id = yadis_url
116
118 """Return the identifier that should be sent as the
119 openid.identity parameter to the server."""
120
121
122
123
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
149
150 fromBasicServiceEndpoint = classmethod(fromBasicServiceEndpoint)
151
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
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
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
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
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
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
278
279
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
289
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
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
313 """Normalize an XRI, stripping its scheme if present"""
314 if xri.startswith("xri://"):
315 xri = xri[6:]
316 return xri
317
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
345
346 prio_services = [(bestMatchingService(s), orig_index, s)
347 for (orig_index, s) in enumerate(service_list)]
348 prio_services.sort()
349
350
351
352 for i in range(len(prio_services)):
353 prio_services[i] = prio_services[i][2]
354
355 return prio_services
356
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
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
386
387
388
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
397 openid_services = []
398
399 if not openid_services:
400
401
402 if response.isXRDS():
403
404
405
406 return discoverNoYadis(uri)
407
408
409
410 openid_services = OpenIDServiceEndpoint.fromHTML(yadis_url, body)
411
412 return (yadis_url, getOPOrUserServices(openid_services))
413
439
440
452
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
471