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
65 return extension_uri in self.type_uris
66
73
75 """Does this endpoint support this type?
76
77 I consider C{/server} endpoints to implicitly support C{/signon}.
78 """
79 return (
80 (type_uri in self.type_uris) or
81 (type_uri == OPENID_2_0_TYPE and self.isOPIdentifier())
82 )
83
86
89
90 - def parseService(self, yadis_url, uri, type_uris, service_element):
91 """Set the state of this object based on the contents of the
92 service element."""
93 self.type_uris = type_uris
94 self.server_url = uri
95 self.used_yadis = True
96
97 if not self.isOPIdentifier():
98
99
100
101
102 self.local_id = findOPLocalIdentifier(service_element,
103 self.type_uris)
104 self.claimed_id = yadis_url
105
107 """Return the identifier that should be sent as the
108 openid.identity parameter to the server."""
109
110
111
112
113 if (self.local_id is self.canonicalID is None):
114 return self.claimed_id
115 else:
116 return self.local_id or self.canonicalID
117
119 """Create a new instance of this class from the endpoint
120 object passed in.
121
122 @return: None or OpenIDServiceEndpoint for this endpoint object"""
123 type_uris = endpoint.matchTypes(cls.openid_type_uris)
124
125
126
127 if type_uris and endpoint.uri is not None:
128 openid_endpoint = cls()
129 openid_endpoint.parseService(
130 endpoint.yadis_url,
131 endpoint.uri,
132 endpoint.type_uris,
133 endpoint.service_element)
134 else:
135 openid_endpoint = None
136
137 return openid_endpoint
138
139 fromBasicServiceEndpoint = classmethod(fromBasicServiceEndpoint)
140
142 """Parse the given document as HTML looking for an OpenID <link
143 rel=...>
144
145 @rtype: [OpenIDServiceEndpoint]
146 """
147 discovery_types = [
148 (OPENID_2_0_TYPE, 'openid2.provider', 'openid2.local_id'),
149 (OPENID_1_1_TYPE, 'openid.server', 'openid.delegate'),
150 ]
151
152 link_attrs = html_parse.parseLinkAttrs(html)
153 services = []
154 for type_uri, op_endpoint_rel, local_id_rel in discovery_types:
155 op_endpoint_url = html_parse.findFirstHref(
156 link_attrs, op_endpoint_rel)
157 if op_endpoint_url is None:
158 continue
159
160 service = cls()
161 service.claimed_id = uri
162 service.local_id = html_parse.findFirstHref(
163 link_attrs, local_id_rel)
164 service.server_url = op_endpoint_url
165 service.type_uris = [type_uri]
166
167 services.append(service)
168
169 return services
170
171 fromHTML = classmethod(fromHTML)
172
174 """Construct an OP-Identifier OpenIDServiceEndpoint object for
175 a given OP Endpoint URL
176
177 @param op_endpoint_url: The URL of the endpoint
178 @rtype: OpenIDServiceEndpoint
179 """
180 service = cls()
181 service.server_url = op_endpoint_url
182 service.type_uris = [OPENID_IDP_2_0_TYPE]
183 return service
184
185 fromOPEndpointURL = classmethod(fromOPEndpointURL)
186
187
189 return ("<%s.%s "
190 "server_url=%r "
191 "claimed_id=%r "
192 "local_id=%r "
193 "canonicalID=%r "
194 "used_yadis=%s "
195 ">"
196 % (self.__class__.__module__, self.__class__.__name__,
197 self.server_url,
198 self.claimed_id,
199 self.local_id,
200 self.canonicalID,
201 self.used_yadis))
202
203
204
206 """Find the OP-Local Identifier for this xrd:Service element.
207
208 This considers openid:Delegate to be a synonym for xrd:LocalID if
209 both OpenID 1.X and OpenID 2.0 types are present. If only OpenID
210 1.X is present, it returns the value of openid:Delegate. If only
211 OpenID 2.0 is present, it returns the value of xrd:LocalID. If
212 there is more than one LocalID tag and the values are different,
213 it raises a DiscoveryFailure. This is also triggered when the
214 xrd:LocalID and openid:Delegate tags are different.
215
216 @param service_element: The xrd:Service element
217 @type service_element: ElementTree.Node
218
219 @param type_uris: The xrd:Type values present in this service
220 element. This function could extract them, but higher level
221 code needs to do that anyway.
222 @type type_uris: [str]
223
224 @raises DiscoveryFailure: when discovery fails.
225
226 @returns: The OP-Local Identifier for this service element, if one
227 is present, or None otherwise.
228 @rtype: str or unicode or NoneType
229 """
230
231
232
233 local_id_tags = []
234 if (OPENID_1_1_TYPE in type_uris or
235 OPENID_1_0_TYPE in type_uris):
236 local_id_tags.append(nsTag(OPENID_1_0_NS, 'Delegate'))
237
238 if OPENID_2_0_TYPE in type_uris:
239 local_id_tags.append(nsTag(XRD_NS_2_0, 'LocalID'))
240
241
242
243 local_id = None
244 for local_id_tag in local_id_tags:
245 for local_id_element in service_element.findall(local_id_tag):
246 if local_id is None:
247 local_id = local_id_element.text
248 elif local_id != local_id_element.text:
249 format = 'More than one %r tag found in one service element'
250 message = format % (local_id_tag,)
251 raise DiscoveryFailure(message, None)
252
253 return local_id
254
256 """Normalize a URL, converting normalization failures to
257 DiscoveryFailure"""
258 try:
259 return urinorm.urinorm(url)
260 except ValueError, why:
261 raise DiscoveryFailure('Normalizing identifier: %s' % (why[0],), None)
262
264 """Rearrange service_list in a new list so services are ordered by
265 types listed in preferred_types. Return the new list."""
266
267 def enumerate(elts):
268 """Return an iterable that pairs the index of an element with
269 that element.
270
271 For Python 2.2 compatibility"""
272 return zip(range(len(elts)), elts)
273
274 def bestMatchingService(service):
275 """Return the index of the first matching type, or something
276 higher if no type matches.
277
278 This provides an ordering in which service elements that
279 contain a type that comes earlier in the preferred types list
280 come before service elements that come later. If a service
281 element has more than one type, the most preferred one wins.
282 """
283 for i, t in enumerate(preferred_types):
284 if preferred_types[i] in service.type_uris:
285 return i
286
287 return len(preferred_types)
288
289
290
291 prio_services = [(bestMatchingService(s), orig_index, s)
292 for (orig_index, s) in enumerate(service_list)]
293 prio_services.sort()
294
295
296
297 for i in range(len(prio_services)):
298 prio_services[i] = prio_services[i][2]
299
300 return prio_services
301
303 """Extract OP Identifier services. If none found, return the
304 rest, sorted with most preferred first according to
305 OpenIDServiceEndpoint.openid_type_uris.
306
307 openid_services is a list of OpenIDServiceEndpoint objects.
308
309 Returns a list of OpenIDServiceEndpoint objects."""
310
311 op_services = arrangeByType(openid_services, [OPENID_IDP_2_0_TYPE])
312
313 openid_services = arrangeByType(openid_services,
314 OpenIDServiceEndpoint.openid_type_uris)
315
316 return op_services or openid_services
317
319 """Discover OpenID services for a URI. Tries Yadis and falls back
320 on old-style <link rel='...'> discovery if Yadis fails.
321
322 @param uri: normalized identity URL
323 @type uri: str
324
325 @return: (claimed_id, services)
326 @rtype: (str, list(OpenIDServiceEndpoint))
327
328 @raises DiscoveryFailure: when discovery fails.
329 """
330
331
332
333
334 response = yadisDiscover(uri)
335
336 yadis_url = response.normalized_uri
337 try:
338 openid_services = extractServices(
339 response.normalized_uri, response.response_text,
340 OpenIDServiceEndpoint)
341 except XRDSError:
342
343 openid_services = []
344
345 if not openid_services:
346
347
348 if response.isXRDS():
349
350
351
352 return discoverNoYadis(uri)
353 else:
354 body = response.response_text
355
356
357
358 openid_services = OpenIDServiceEndpoint.fromHTML(yadis_url, body)
359
360 return (yadis_url, getOPOrUserServices(openid_services))
361
385
386
398
400 parsed = urlparse.urlparse(uri)
401 if parsed[0] and parsed[1]:
402 if parsed[0] not in ['http', 'https']:
403 raise DiscoveryFailure('URI scheme is not HTTP or HTTPS', None)
404 else:
405 uri = 'http://' + uri
406
407 uri = normalizeURL(uri)
408 claimed_id, openid_services = discoverYadis(uri)
409 claimed_id = normalizeURL(claimed_id)
410 return claimed_id, openid_services
411
413 if xri.identifierScheme(identifier) == "XRI":
414 return discoverXRI(identifier)
415 else:
416 return discoverURI(identifier)
417