1
2 """Functions to discover OpenID endpoints from identifiers.
3 """
4
5 import urlparse
6
7 from openid import oidutil, fetchers, urinorm
8
9 from openid import yadis
10 from openid.yadis.etxrd import nsTag, XRDSError, XRD_NS_2_0
11 from openid.yadis.services import applyFilter as extractServices
12 from openid.yadis.discover import discover as yadisDiscover
13 from openid.yadis.discover import DiscoveryFailure
14 from openid.yadis import xrires, filters
15 from openid.yadis import xri
16
17 from openid.consumer import html_parse
18
19 OPENID_1_0_NS = 'http://openid.net/xmlns/1.0'
20 OPENID_IDP_2_0_TYPE = 'http://specs.openid.net/auth/2.0/server'
21 OPENID_2_0_TYPE = 'http://specs.openid.net/auth/2.0/signon'
22 OPENID_1_1_TYPE = 'http://openid.net/signon/1.1'
23 OPENID_1_0_TYPE = 'http://openid.net/signon/1.0'
24
25 from openid.message import OPENID1_NS as OPENID_1_0_MESSAGE_NS
26 from openid.message import OPENID2_NS as OPENID_2_0_MESSAGE_NS
27
29 """Object representing an OpenID service endpoint.
30
31 @ivar identity_url: the verified identifier.
32 @ivar canonicalID: For XRI, the persistent identifier.
33 """
34
35
36
37 openid_type_uris = [
38 OPENID_IDP_2_0_TYPE,
39
40 OPENID_2_0_TYPE,
41 OPENID_1_1_TYPE,
42 OPENID_1_0_TYPE,
43 ]
44
46 self.claimed_id = None
47 self.server_url = None
48 self.type_uris = []
49 self.local_id = None
50 self.canonicalID = None
51 self.used_yadis = False
52
54 return extension_uri in self.type_uris
55
62
64 """Does this endpoint support this type?
65
66 I consider C{/server} endpoints to implicitly support C{/signon}.
67 """
68 return (
69 (type_uri in self.type_uris) or
70 (type_uri == OPENID_2_0_TYPE and self.isOPIdentifier())
71 )
72
75
78
79 - def parseService(self, yadis_url, uri, type_uris, service_element):
80 """Set the state of this object based on the contents of the
81 service element."""
82 self.type_uris = type_uris
83 self.server_url = uri
84 self.used_yadis = True
85
86 if not self.isOPIdentifier():
87
88
89
90
91 self.local_id = findOPLocalIdentifier(service_element,
92 self.type_uris)
93 self.claimed_id = yadis_url
94
96 """Return the identifier that should be sent as the
97 openid.identity parameter to the server."""
98
99
100
101
102 if (self.local_id is self.canonicalID is None):
103 return self.claimed_id
104 else:
105 return self.local_id or self.canonicalID
106
108 """Create a new instance of this class from the endpoint
109 object passed in.
110
111 @return: None or OpenIDServiceEndpoint for this endpoint object"""
112 type_uris = endpoint.matchTypes(cls.openid_type_uris)
113
114
115
116 if type_uris and endpoint.uri is not None:
117 openid_endpoint = cls()
118 openid_endpoint.parseService(
119 endpoint.yadis_url,
120 endpoint.uri,
121 endpoint.type_uris,
122 endpoint.service_element)
123 else:
124 openid_endpoint = None
125
126 return openid_endpoint
127
128 fromBasicServiceEndpoint = classmethod(fromBasicServiceEndpoint)
129
131 """Parse the given document as HTML looking for an OpenID <link
132 rel=...>
133
134 @rtype: [OpenIDServiceEndpoint]
135 """
136 discovery_types = [
137 (OPENID_2_0_TYPE, 'openid2.provider', 'openid2.local_id'),
138 (OPENID_1_1_TYPE, 'openid.server', 'openid.delegate'),
139 ]
140
141 link_attrs = html_parse.parseLinkAttrs(html)
142 services = []
143 for type_uri, op_endpoint_rel, local_id_rel in discovery_types:
144 op_endpoint_url = html_parse.findFirstHref(
145 link_attrs, op_endpoint_rel)
146 if op_endpoint_url is None:
147 continue
148
149 service = cls()
150 service.claimed_id = uri
151 service.local_id = html_parse.findFirstHref(
152 link_attrs, local_id_rel)
153 service.server_url = op_endpoint_url
154 service.type_uris = [type_uri]
155
156 services.append(service)
157
158 return services
159
160 fromHTML = classmethod(fromHTML)
161
163 """Construct an OP-Identifier OpenIDServiceEndpoint object for
164 a given OP Endpoint URL
165
166 @param op_endpoint_url: The URL of the endpoint
167 @rtype: OpenIDServiceEndpoint
168 """
169 service = cls()
170 service.server_url = op_endpoint_url
171 service.type_uris = [OPENID_IDP_2_0_TYPE]
172 return service
173
174 fromOPEndpointURL = classmethod(fromOPEndpointURL)
175
176
178 return ("<%s.%s "
179 "server_url=%r "
180 "claimed_id=%r "
181 "local_id=%r "
182 "canonicalID=%r "
183 "used_yadis=%s "
184 ">"
185 % (self.__class__.__module__, self.__class__.__name__,
186 self.server_url,
187 self.claimed_id,
188 self.local_id,
189 self.canonicalID,
190 self.used_yadis))
191
192
193
195 """Find the OP-Local Identifier for this xrd:Service element.
196
197 This considers openid:Delegate to be a synonym for xrd:LocalID if
198 both OpenID 1.X and OpenID 2.0 types are present. If only OpenID
199 1.X is present, it returns the value of openid:Delegate. If only
200 OpenID 2.0 is present, it returns the value of xrd:LocalID. If
201 there is more than one LocalID tag and the values are different,
202 it raises a DiscoveryFailure. This is also triggered when the
203 xrd:LocalID and openid:Delegate tags are different.
204
205 @param service_element: The xrd:Service element
206 @type service_element: ElementTree.Node
207
208 @param type_uris: The xrd:Type values present in this service
209 element. This function could extract them, but higher level
210 code needs to do that anyway.
211 @type type_uris: [str]
212
213 @raises DiscoveryFailure: when discovery fails.
214
215 @returns: The OP-Local Identifier for this service element, if one
216 is present, or None otherwise.
217 @rtype: str or unicode or NoneType
218 """
219
220
221
222 local_id_tags = []
223 if (OPENID_1_1_TYPE in type_uris or
224 OPENID_1_0_TYPE in type_uris):
225 local_id_tags.append(nsTag(OPENID_1_0_NS, 'Delegate'))
226
227 if OPENID_2_0_TYPE in type_uris:
228 local_id_tags.append(nsTag(XRD_NS_2_0, 'LocalID'))
229
230
231
232 local_id = None
233 for local_id_tag in local_id_tags:
234 for local_id_element in service_element.findall(local_id_tag):
235 if local_id is None:
236 local_id = local_id_element.text
237 elif local_id != local_id_element.text:
238 format = 'More than one %r tag found in one service element'
239 message = format % (local_id_tag,)
240 raise DiscoveryFailure(message, None)
241
242 return local_id
243
245 """Normalize a URL, converting normalization failures to
246 DiscoveryFailure"""
247 try:
248 return urinorm.urinorm(url)
249 except ValueError, why:
250 raise DiscoveryFailure('Normalizing identifier: %s' % (why[0],), None)
251
253 """Rearrange service_list in a new list so services are ordered by
254 types listed in preferred_types. Return the new list."""
255
256 def enumerate(elts):
257 """Return an iterable that pairs the index of an element with
258 that element.
259
260 For Python 2.2 compatibility"""
261 return zip(range(len(elts)), elts)
262
263 def bestMatchingService(service):
264 """Return the index of the first matching type, or something
265 higher if no type matches.
266
267 This provides an ordering in which service elements that
268 contain a type that comes earlier in the preferred types list
269 come before service elements that come later. If a service
270 element has more than one type, the most preferred one wins.
271 """
272 for i, t in enumerate(preferred_types):
273 if preferred_types[i] in service.type_uris:
274 return i
275
276 return len(preferred_types)
277
278
279
280 prio_services = [(bestMatchingService(s), orig_index, s)
281 for (orig_index, s) in enumerate(service_list)]
282 prio_services.sort()
283
284
285
286 for i in range(len(prio_services)):
287 prio_services[i] = prio_services[i][2]
288
289 return prio_services
290
292 """Extract OP Identifier services. If none found, return the
293 rest, sorted with most preferred first according to
294 OpenIDServiceEndpoint.openid_type_uris.
295
296 openid_services is a list of OpenIDServiceEndpoint objects.
297
298 Returns a list of OpenIDServiceEndpoint objects."""
299
300 op_services = arrangeByType(openid_services, [OPENID_IDP_2_0_TYPE])
301
302 openid_services = arrangeByType(openid_services,
303 OpenIDServiceEndpoint.openid_type_uris)
304
305 return op_services or openid_services
306
308 """Discover OpenID services for a URI. Tries Yadis and falls back
309 on old-style <link rel='...'> discovery if Yadis fails.
310
311 @param uri: normalized identity URL
312 @type uri: str
313
314 @return: (claimed_id, services)
315 @rtype: (str, list(OpenIDServiceEndpoint))
316
317 @raises DiscoveryFailure: when discovery fails.
318 """
319
320
321
322
323 response = yadisDiscover(uri)
324
325 yadis_url = response.normalized_uri
326 try:
327 openid_services = extractServices(
328 response.normalized_uri, response.response_text,
329 OpenIDServiceEndpoint)
330 except XRDSError:
331
332 openid_services = []
333
334 if not openid_services:
335
336
337 if response.isXRDS():
338
339
340
341 return discoverNoYadis(uri)
342 else:
343 body = response.response_text
344
345
346
347 openid_services = OpenIDServiceEndpoint.fromHTML(yadis_url, body)
348
349 return (yadis_url, getOPOrUserServices(openid_services))
350
374
375
387
389 parsed = urlparse.urlparse(uri)
390 if parsed[0] and parsed[1]:
391 if parsed[0] not in ['http', 'https']:
392 raise DiscoveryFailure('URI scheme is not HTTP or HTTPS', None)
393 else:
394 uri = 'http://' + uri
395
396 uri = normalizeURL(uri)
397 claimed_id, openid_services = discoverYadis(uri)
398 claimed_id = normalizeURL(claimed_id)
399 return claimed_id, openid_services
400
406