getting file size for all dict files to be downloaded. coming to be 400mb or so.
[worddb.git] / libs / openid / consumer / consumer.py
blob72c6096a025f03d577c79de13455b29f94610eae
1 # -*- test-case-name: openid.test.test_consumer -*-
2 """OpenID support for Relying Parties (aka Consumers).
4 This module documents the main interface with the OpenID consumer
5 library. The only part of the library which has to be used and isn't
6 documented in full here is the store required to create an
7 C{L{Consumer}} instance. More on the abstract store type and
8 concrete implementations of it that are provided in the documentation
9 for the C{L{__init__<Consumer.__init__>}} method of the
10 C{L{Consumer}} class.
13 OVERVIEW
14 ========
16 The OpenID identity verification process most commonly uses the
17 following steps, as visible to the user of this library:
19 1. The user enters their OpenID into a field on the consumer's
20 site, and hits a login button.
22 2. The consumer site discovers the user's OpenID provider using
23 the Yadis protocol.
25 3. The consumer site sends the browser a redirect to the
26 OpenID provider. This is the authentication request as
27 described in the OpenID specification.
29 4. The OpenID provider's site sends the browser a redirect
30 back to the consumer site. This redirect contains the
31 provider's response to the authentication request.
33 The most important part of the flow to note is the consumer's site
34 must handle two separate HTTP requests in order to perform the
35 full identity check.
38 LIBRARY DESIGN
39 ==============
41 This consumer library is designed with that flow in mind. The
42 goal is to make it as easy as possible to perform the above steps
43 securely.
45 At a high level, there are two important parts in the consumer
46 library. The first important part is this module, which contains
47 the interface to actually use this library. The second is the
48 C{L{openid.store.interface}} module, which describes the
49 interface to use if you need to create a custom method for storing
50 the state this library needs to maintain between requests.
52 In general, the second part is less important for users of the
53 library to know about, as several implementations are provided
54 which cover a wide variety of situations in which consumers may
55 use the library.
57 This module contains a class, C{L{Consumer}}, with methods
58 corresponding to the actions necessary in each of steps 2, 3, and
59 4 described in the overview. Use of this library should be as easy
60 as creating an C{L{Consumer}} instance and calling the methods
61 appropriate for the action the site wants to take.
64 SESSIONS, STORES, AND STATELESS MODE
65 ====================================
67 The C{L{Consumer}} object keeps track of two types of state:
69 1. State of the user's current authentication attempt. Things like
70 the identity URL, the list of endpoints discovered for that
71 URL, and in case where some endpoints are unreachable, the list
72 of endpoints already tried. This state needs to be held from
73 Consumer.begin() to Consumer.complete(), but it is only applicable
74 to a single session with a single user agent, and at the end of
75 the authentication process (i.e. when an OP replies with either
76 C{id_res} or C{cancel}) it may be discarded.
78 2. State of relationships with servers, i.e. shared secrets
79 (associations) with servers and nonces seen on signed messages.
80 This information should persist from one session to the next and
81 should not be bound to a particular user-agent.
84 These two types of storage are reflected in the first two arguments of
85 Consumer's constructor, C{session} and C{store}. C{session} is a
86 dict-like object and we hope your web framework provides you with one
87 of these bound to the user agent. C{store} is an instance of
88 L{openid.store.interface.OpenIDStore}.
90 Since the store does hold secrets shared between your application and the
91 OpenID provider, you should be careful about how you use it in a shared
92 hosting environment. If the filesystem or database permissions of your
93 web host allow strangers to read from them, do not store your data there!
94 If you have no safe place to store your data, construct your consumer
95 with C{None} for the store, and it will operate only in stateless mode.
96 Stateless mode may be slower, put more load on the OpenID provider, and
97 trusts the provider to keep you safe from replay attacks.
100 Several store implementation are provided, and the interface is
101 fully documented so that custom stores can be used as well. See
102 the documentation for the C{L{Consumer}} class for more
103 information on the interface for stores. The implementations that
104 are provided allow the consumer site to store the necessary data
105 in several different ways, including several SQL databases and
106 normal files on disk.
109 IMMEDIATE MODE
110 ==============
112 In the flow described above, the user may need to confirm to the
113 OpenID provider that it's ok to disclose his or her identity.
114 The provider may draw pages asking for information from the user
115 before it redirects the browser back to the consumer's site. This
116 is generally transparent to the consumer site, so it is typically
117 ignored as an implementation detail.
119 There can be times, however, where the consumer site wants to get
120 a response immediately. When this is the case, the consumer can
121 put the library in immediate mode. In immediate mode, there is an
122 extra response possible from the server, which is essentially the
123 server reporting that it doesn't have enough information to answer
124 the question yet.
127 USING THIS LIBRARY
128 ==================
130 Integrating this library into an application is usually a
131 relatively straightforward process. The process should basically
132 follow this plan:
134 Add an OpenID login field somewhere on your site. When an OpenID
135 is entered in that field and the form is submitted, it should make
136 a request to the your site which includes that OpenID URL.
138 First, the application should L{instantiate a Consumer<Consumer.__init__>}
139 with a session for per-user state and store for shared state.
140 using the store of choice.
142 Next, the application should call the 'C{L{begin<Consumer.begin>}}' method on the
143 C{L{Consumer}} instance. This method takes the OpenID URL. The
144 C{L{begin<Consumer.begin>}} method returns an C{L{AuthRequest}}
145 object.
147 Next, the application should call the
148 C{L{redirectURL<AuthRequest.redirectURL>}} method on the
149 C{L{AuthRequest}} object. The parameter C{return_to} is the URL
150 that the OpenID server will send the user back to after attempting
151 to verify his or her identity. The C{realm} parameter is the
152 URL (or URL pattern) that identifies your web site to the user
153 when he or she is authorizing it. Send a redirect to the
154 resulting URL to the user's browser.
156 That's the first half of the authentication process. The second
157 half of the process is done after the user's OpenID Provider sends the
158 user's browser a redirect back to your site to complete their
159 login.
161 When that happens, the user will contact your site at the URL
162 given as the C{return_to} URL to the
163 C{L{redirectURL<AuthRequest.redirectURL>}} call made
164 above. The request will have several query parameters added to
165 the URL by the OpenID provider as the information necessary to
166 finish the request.
168 Get an C{L{Consumer}} instance with the same session and store as
169 before and call its C{L{complete<Consumer.complete>}} method,
170 passing in all the received query arguments.
172 There are multiple possible return types possible from that
173 method. These indicate the whether or not the login was
174 successful, and include any additional information appropriate for
175 their type.
177 @var SUCCESS: constant used as the status for
178 L{SuccessResponse<openid.consumer.consumer.SuccessResponse>} objects.
180 @var FAILURE: constant used as the status for
181 L{FailureResponse<openid.consumer.consumer.FailureResponse>} objects.
183 @var CANCEL: constant used as the status for
184 L{CancelResponse<openid.consumer.consumer.CancelResponse>} objects.
186 @var SETUP_NEEDED: constant used as the status for
187 L{SetupNeededResponse<openid.consumer.consumer.SetupNeededResponse>}
188 objects.
191 import cgi
192 import copy
193 from urlparse import urlparse, urldefrag
195 from openid import fetchers
197 from openid.consumer.discover import discover, OpenIDServiceEndpoint, \
198 DiscoveryFailure, OPENID_1_0_TYPE, OPENID_1_1_TYPE, OPENID_2_0_TYPE
199 from openid.message import Message, OPENID_NS, OPENID2_NS, OPENID1_NS, \
200 IDENTIFIER_SELECT, no_default, BARE_NS
201 from openid import cryptutil
202 from openid import oidutil
203 from openid.association import Association, default_negotiator, \
204 SessionNegotiator
205 from openid.dh import DiffieHellman
206 from openid.store.nonce import mkNonce, split as splitNonce
207 from openid.yadis.manager import Discovery
208 from openid import urinorm
211 __all__ = ['AuthRequest', 'Consumer', 'SuccessResponse',
212 'SetupNeededResponse', 'CancelResponse', 'FailureResponse',
213 'SUCCESS', 'FAILURE', 'CANCEL', 'SETUP_NEEDED',
217 def makeKVPost(request_message, server_url):
218 """Make a Direct Request to an OpenID Provider and return the
219 result as a Message object.
221 @raises openid.fetchers.HTTPFetchingError: if an error is
222 encountered in making the HTTP post.
224 @rtype: L{openid.message.Message}
226 # XXX: TESTME
227 resp = fetchers.fetch(server_url, body=request_message.toURLEncoded())
229 # Process response in separate function that can be shared by async code.
230 return _httpResponseToMessage(resp, server_url)
233 def _httpResponseToMessage(response, server_url):
234 """Adapt a POST response to a Message.
236 @type response: L{openid.fetchers.HTTPResponse}
237 @param response: Result of a POST to an OpenID endpoint.
239 @rtype: L{openid.message.Message}
241 @raises openid.fetchers.HTTPFetchingError: if the server returned a
242 status of other than 200 or 400.
244 @raises ServerError: if the server returned an OpenID error.
246 # Should this function be named Message.fromHTTPResponse instead?
247 response_message = Message.fromKVForm(response.body)
248 if response.status == 400:
249 raise ServerError.fromMessage(response_message)
251 elif response.status not in (200, 206):
252 fmt = 'bad status code from server %s: %s'
253 error_message = fmt % (server_url, response.status)
254 raise fetchers.HTTPFetchingError(error_message)
256 return response_message
260 class Consumer(object):
261 """An OpenID consumer implementation that performs discovery and
262 does session management.
264 @ivar consumer: an instance of an object implementing the OpenID
265 protocol, but doing no discovery or session management.
267 @type consumer: GenericConsumer
269 @ivar session: A dictionary-like object representing the user's
270 session data. This is used for keeping state of the OpenID
271 transaction when the user is redirected to the server.
273 @cvar session_key_prefix: A string that is prepended to session
274 keys to ensure that they are unique. This variable may be
275 changed to suit your application.
277 session_key_prefix = "_openid_consumer_"
279 _token = 'last_token'
281 _discover = staticmethod(discover)
283 def __init__(self, session, store, consumer_class=None):
284 """Initialize a Consumer instance.
286 You should create a new instance of the Consumer object with
287 every HTTP request that handles OpenID transactions.
289 @param session: See L{the session instance variable<openid.consumer.consumer.Consumer.session>}
291 @param store: an object that implements the interface in
292 C{L{openid.store.interface.OpenIDStore}}. Several
293 implementations are provided, to cover common database
294 environments.
296 @type store: C{L{openid.store.interface.OpenIDStore}}
298 @see: L{openid.store.interface}
299 @see: L{openid.store}
301 self.session = session
302 if consumer_class is None:
303 consumer_class = GenericConsumer
304 self.consumer = consumer_class(store)
305 self._token_key = self.session_key_prefix + self._token
307 def begin(self, user_url, anonymous=False):
308 """Start the OpenID authentication process. See steps 1-2 in
309 the overview at the top of this file.
311 @param user_url: Identity URL given by the user. This method
312 performs a textual transformation of the URL to try and
313 make sure it is normalized. For example, a user_url of
314 example.com will be normalized to http://example.com/
315 normalizing and resolving any redirects the server might
316 issue.
318 @type user_url: unicode
320 @param anonymous: Whether to make an anonymous request of the OpenID
321 provider. Such a request does not ask for an authorization
322 assertion for an OpenID identifier, but may be used with
323 extensions to pass other data. e.g. "I don't care who you are,
324 but I'd like to know your time zone."
326 @type anonymous: bool
328 @returns: An object containing the discovered information will
329 be returned, with a method for building a redirect URL to
330 the server, as described in step 3 of the overview. This
331 object may also be used to add extension arguments to the
332 request, using its
333 L{addExtensionArg<openid.consumer.consumer.AuthRequest.addExtensionArg>}
334 method.
336 @returntype: L{AuthRequest<openid.consumer.consumer.AuthRequest>}
338 @raises openid.consumer.discover.DiscoveryFailure: when I fail to
339 find an OpenID server for this URL. If the C{yadis} package
340 is available, L{openid.consumer.discover.DiscoveryFailure} is
341 an alias for C{yadis.discover.DiscoveryFailure}.
343 disco = Discovery(self.session, user_url, self.session_key_prefix)
344 try:
345 service = disco.getNextService(self._discover)
346 except fetchers.HTTPFetchingError, why:
347 raise DiscoveryFailure(
348 'Error fetching XRDS document: %s' % (why[0],), None)
350 if service is None:
351 raise DiscoveryFailure(
352 'No usable OpenID services found for %s' % (user_url,), None)
353 else:
354 return self.beginWithoutDiscovery(service, anonymous)
356 def beginWithoutDiscovery(self, service, anonymous=False):
357 """Start OpenID verification without doing OpenID server
358 discovery. This method is used internally by Consumer.begin
359 after discovery is performed, and exists to provide an
360 interface for library users needing to perform their own
361 discovery.
363 @param service: an OpenID service endpoint descriptor. This
364 object and factories for it are found in the
365 L{openid.consumer.discover} module.
367 @type service:
368 L{OpenIDServiceEndpoint<openid.consumer.discover.OpenIDServiceEndpoint>}
370 @returns: an OpenID authentication request object.
372 @rtype: L{AuthRequest<openid.consumer.consumer.AuthRequest>}
374 @See: Openid.consumer.consumer.Consumer.begin
375 @see: openid.consumer.discover
377 auth_req = self.consumer.begin(service)
378 self.session[self._token_key] = auth_req.endpoint
380 try:
381 auth_req.setAnonymous(anonymous)
382 except ValueError, why:
383 raise ProtocolError(str(why))
385 return auth_req
387 def complete(self, query, current_url):
388 """Called to interpret the server's response to an OpenID
389 request. It is called in step 4 of the flow described in the
390 consumer overview.
392 @param query: A dictionary of the query parameters for this
393 HTTP request.
395 @param current_url: The URL used to invoke the application.
396 Extract the URL from your application's web
397 request framework and specify it here to have it checked
398 against the openid.return_to value in the response. If
399 the return_to URL check fails, the status of the
400 completion will be FAILURE.
402 @returns: a subclass of Response. The type of response is
403 indicated by the status attribute, which will be one of
404 SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED.
406 @see: L{SuccessResponse<openid.consumer.consumer.SuccessResponse>}
407 @see: L{CancelResponse<openid.consumer.consumer.CancelResponse>}
408 @see: L{SetupNeededResponse<openid.consumer.consumer.SetupNeededResponse>}
409 @see: L{FailureResponse<openid.consumer.consumer.FailureResponse>}
412 endpoint = self.session.get(self._token_key)
414 message = Message.fromPostArgs(query)
415 response = self.consumer.complete(message, endpoint, current_url)
417 try:
418 del self.session[self._token_key]
419 except KeyError:
420 pass
422 if (response.status in ['success', 'cancel'] and
423 response.identity_url is not None):
425 disco = Discovery(self.session,
426 response.identity_url,
427 self.session_key_prefix)
428 # This is OK to do even if we did not do discovery in
429 # the first place.
430 disco.cleanup(force=True)
432 return response
434 def setAssociationPreference(self, association_preferences):
435 """Set the order in which association types/sessions should be
436 attempted. For instance, to only allow HMAC-SHA256
437 associations created with a DH-SHA256 association session:
439 >>> consumer.setAssociationPreference([('HMAC-SHA256', 'DH-SHA256')])
441 Any association type/association type pair that is not in this
442 list will not be attempted at all.
444 @param association_preferences: The list of allowed
445 (association type, association session type) pairs that
446 should be allowed for this consumer to use, in order from
447 most preferred to least preferred.
448 @type association_preferences: [(str, str)]
450 @returns: None
452 @see: C{L{openid.association.SessionNegotiator}}
454 self.consumer.negotiator = SessionNegotiator(association_preferences)
456 class DiffieHellmanSHA1ConsumerSession(object):
457 session_type = 'DH-SHA1'
458 hash_func = staticmethod(cryptutil.sha1)
459 secret_size = 20
460 allowed_assoc_types = ['HMAC-SHA1']
462 def __init__(self, dh=None):
463 if dh is None:
464 dh = DiffieHellman.fromDefaults()
466 self.dh = dh
468 def getRequest(self):
469 cpub = cryptutil.longToBase64(self.dh.public)
471 args = {'dh_consumer_public': cpub}
473 if not self.dh.usingDefaultValues():
474 args.update({
475 'dh_modulus': cryptutil.longToBase64(self.dh.modulus),
476 'dh_gen': cryptutil.longToBase64(self.dh.generator),
479 return args
481 def extractSecret(self, response):
482 dh_server_public64 = response.getArg(
483 OPENID_NS, 'dh_server_public', no_default)
484 enc_mac_key64 = response.getArg(OPENID_NS, 'enc_mac_key', no_default)
485 dh_server_public = cryptutil.base64ToLong(dh_server_public64)
486 enc_mac_key = oidutil.fromBase64(enc_mac_key64)
487 return self.dh.xorSecret(dh_server_public, enc_mac_key, self.hash_func)
489 class DiffieHellmanSHA256ConsumerSession(DiffieHellmanSHA1ConsumerSession):
490 session_type = 'DH-SHA256'
491 hash_func = staticmethod(cryptutil.sha256)
492 secret_size = 32
493 allowed_assoc_types = ['HMAC-SHA256']
495 class PlainTextConsumerSession(object):
496 session_type = 'no-encryption'
497 allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']
499 def getRequest(self):
500 return {}
502 def extractSecret(self, response):
503 mac_key64 = response.getArg(OPENID_NS, 'mac_key', no_default)
504 return oidutil.fromBase64(mac_key64)
506 class SetupNeededError(Exception):
507 """Internally-used exception that indicates that an immediate-mode
508 request cancelled."""
509 def __init__(self, user_setup_url=None):
510 Exception.__init__(self, user_setup_url)
511 self.user_setup_url = user_setup_url
513 class ProtocolError(ValueError):
514 """Exception that indicates that a message violated the
515 protocol. It is raised and caught internally to this file."""
517 class TypeURIMismatch(ProtocolError):
518 """A protocol error arising from type URIs mismatching
521 def __init__(self, expected, endpoint):
522 ProtocolError.__init__(self, expected, endpoint)
523 self.expected = expected
524 self.endpoint = endpoint
526 def __str__(self):
527 s = '<%s.%s: Required type %s not found in %s for endpoint %s>' % (
528 self.__class__.__module__, self.__class__.__name__,
529 self.expected, self.endpoint.type_uris, self.endpoint)
530 return s
534 class ServerError(Exception):
535 """Exception that is raised when the server returns a 400 response
536 code to a direct request."""
538 def __init__(self, error_text, error_code, message):
539 Exception.__init__(self, error_text)
540 self.error_text = error_text
541 self.error_code = error_code
542 self.message = message
544 def fromMessage(cls, message):
545 """Generate a ServerError instance, extracting the error text
546 and the error code from the message."""
547 error_text = message.getArg(
548 OPENID_NS, 'error', '<no error message supplied>')
549 error_code = message.getArg(OPENID_NS, 'error_code')
550 return cls(error_text, error_code, message)
552 fromMessage = classmethod(fromMessage)
554 class GenericConsumer(object):
555 """This is the implementation of the common logic for OpenID
556 consumers. It is unaware of the application in which it is
557 running.
559 @ivar negotiator: An object that controls the kind of associations
560 that the consumer makes. It defaults to
561 C{L{openid.association.default_negotiator}}. Assign a
562 different negotiator to it if you have specific requirements
563 for how associations are made.
564 @type negotiator: C{L{openid.association.SessionNegotiator}}
567 # The name of the query parameter that gets added to the return_to
568 # URL when using OpenID1. You can change this value if you want or
569 # need a different name, but don't make it start with openid,
570 # because it's not a standard protocol thing for OpenID1. For
571 # OpenID2, the library will take care of the nonce using standard
572 # OpenID query parameter names.
573 openid1_nonce_query_arg_name = 'janrain_nonce'
575 # Another query parameter that gets added to the return_to for
576 # OpenID 1; if the user's session state is lost, use this claimed
577 # identifier to do discovery when verifying the response.
578 openid1_return_to_identifier_name = 'openid1_claimed_id'
580 session_types = {
581 'DH-SHA1':DiffieHellmanSHA1ConsumerSession,
582 'DH-SHA256':DiffieHellmanSHA256ConsumerSession,
583 'no-encryption':PlainTextConsumerSession,
586 _discover = staticmethod(discover)
588 def __init__(self, store):
589 self.store = store
590 self.negotiator = default_negotiator.copy()
592 def begin(self, service_endpoint):
593 """Create an AuthRequest object for the specified
594 service_endpoint. This method will create an association if
595 necessary."""
596 if self.store is None:
597 assoc = None
598 else:
599 assoc = self._getAssociation(service_endpoint)
601 request = AuthRequest(service_endpoint, assoc)
602 request.return_to_args[self.openid1_nonce_query_arg_name] = mkNonce()
604 if request.message.isOpenID1():
605 request.return_to_args[self.openid1_return_to_identifier_name] = \
606 request.endpoint.claimed_id
608 return request
610 def complete(self, message, endpoint, return_to):
611 """Process the OpenID message, using the specified endpoint
612 and return_to URL as context. This method will handle any
613 OpenID message that is sent to the return_to URL.
615 mode = message.getArg(OPENID_NS, 'mode', '<No mode set>')
617 modeMethod = getattr(self, '_complete_' + mode,
618 self._completeInvalid)
620 return modeMethod(message, endpoint, return_to)
622 def _complete_cancel(self, message, endpoint, _):
623 return CancelResponse(endpoint)
625 def _complete_error(self, message, endpoint, _):
626 error = message.getArg(OPENID_NS, 'error')
627 contact = message.getArg(OPENID_NS, 'contact')
628 reference = message.getArg(OPENID_NS, 'reference')
630 return FailureResponse(endpoint, error, contact=contact,
631 reference=reference)
633 def _complete_setup_needed(self, message, endpoint, _):
634 if not message.isOpenID2():
635 return self._completeInvalid(message, endpoint, _)
637 return SetupNeededResponse(endpoint)
639 def _complete_id_res(self, message, endpoint, return_to):
640 try:
641 self._checkSetupNeeded(message)
642 except SetupNeededError, why:
643 return SetupNeededResponse(endpoint, why.user_setup_url)
644 else:
645 try:
646 return self._doIdRes(message, endpoint, return_to)
647 except (ProtocolError, DiscoveryFailure), why:
648 return FailureResponse(endpoint, why[0])
650 def _completeInvalid(self, message, endpoint, _):
651 mode = message.getArg(OPENID_NS, 'mode', '<No mode set>')
652 return FailureResponse(endpoint,
653 'Invalid openid.mode: %r' % (mode,))
655 def _checkReturnTo(self, message, return_to):
656 """Check an OpenID message and its openid.return_to value
657 against a return_to URL from an application. Return True on
658 success, False on failure.
660 # Check the openid.return_to args against args in the original
661 # message.
662 try:
663 self._verifyReturnToArgs(message.toPostArgs())
664 except ProtocolError, why:
665 oidutil.log("Verifying return_to arguments: %s" % (why[0],))
666 return False
668 # Check the return_to base URL against the one in the message.
669 msg_return_to = message.getArg(OPENID_NS, 'return_to')
671 # The URL scheme, authority, and path MUST be the same between
672 # the two URLs.
673 app_parts = urlparse(urinorm.urinorm(return_to))
674 msg_parts = urlparse(urinorm.urinorm(msg_return_to))
676 # (addressing scheme, network location, path) must be equal in
677 # both URLs.
678 for part in range(0, 3):
679 if app_parts[part] != msg_parts[part]:
680 return False
682 return True
684 _makeKVPost = staticmethod(makeKVPost)
686 def _checkSetupNeeded(self, message):
687 """Check an id_res message to see if it is a
688 checkid_immediate cancel response.
690 @raises SetupNeededError: if it is a checkid_immediate cancellation
692 # In OpenID 1, we check to see if this is a cancel from
693 # immediate mode by the presence of the user_setup_url
694 # parameter.
695 if message.isOpenID1():
696 user_setup_url = message.getArg(OPENID1_NS, 'user_setup_url')
697 if user_setup_url is not None:
698 raise SetupNeededError(user_setup_url)
700 def _doIdRes(self, message, endpoint, return_to):
701 """Handle id_res responses that are not cancellations of
702 immediate mode requests.
704 @param message: the response paramaters.
705 @param endpoint: the discovered endpoint object. May be None.
707 @raises ProtocolError: If the message contents are not
708 well-formed according to the OpenID specification. This
709 includes missing fields or not signing fields that should
710 be signed.
712 @raises DiscoveryFailure: If the subject of the id_res message
713 does not match the supplied endpoint, and discovery on the
714 identifier in the message fails (this should only happen
715 when using OpenID 2)
717 @returntype: L{Response}
719 # Checks for presence of appropriate fields (and checks
720 # signed list fields)
721 self._idResCheckForFields(message)
723 if not self._checkReturnTo(message, return_to):
724 raise ProtocolError(
725 "return_to does not match return URL. Expected %r, got %r"
726 % (return_to, message.getArg(OPENID_NS, 'return_to')))
729 # Verify discovery information:
730 endpoint = self._verifyDiscoveryResults(message, endpoint)
731 oidutil.log("Received id_res response from %s using association %s" %
732 (endpoint.server_url,
733 message.getArg(OPENID_NS, 'assoc_handle')))
735 self._idResCheckSignature(message, endpoint.server_url)
737 # Will raise a ProtocolError if the nonce is bad
738 self._idResCheckNonce(message, endpoint)
740 signed_list_str = message.getArg(OPENID_NS, 'signed', no_default)
741 signed_list = signed_list_str.split(',')
742 signed_fields = ["openid." + s for s in signed_list]
743 return SuccessResponse(endpoint, message, signed_fields)
745 def _idResGetNonceOpenID1(self, message, endpoint):
746 """Extract the nonce from an OpenID 1 response. Return the
747 nonce from the BARE_NS since we independently check the
748 return_to arguments are the same as those in the response
749 message.
751 See the openid1_nonce_query_arg_name class variable
753 @returns: The nonce as a string or None
755 return message.getArg(BARE_NS, self.openid1_nonce_query_arg_name)
757 def _idResCheckNonce(self, message, endpoint):
758 if message.isOpenID1():
759 # This indicates that the nonce was generated by the consumer
760 nonce = self._idResGetNonceOpenID1(message, endpoint)
761 server_url = ''
762 else:
763 nonce = message.getArg(OPENID2_NS, 'response_nonce')
764 server_url = endpoint.server_url
766 if nonce is None:
767 raise ProtocolError('Nonce missing from response')
769 try:
770 timestamp, salt = splitNonce(nonce)
771 except ValueError, why:
772 raise ProtocolError('Malformed nonce: %s' % (why[0],))
774 if (self.store is not None and
775 not self.store.useNonce(server_url, timestamp, salt)):
776 raise ProtocolError('Nonce already used or out of range')
778 def _idResCheckSignature(self, message, server_url):
779 assoc_handle = message.getArg(OPENID_NS, 'assoc_handle')
780 if self.store is None:
781 assoc = None
782 else:
783 assoc = self.store.getAssociation(server_url, assoc_handle)
785 if assoc:
786 if assoc.getExpiresIn() <= 0:
787 # XXX: It might be a good idea sometimes to re-start the
788 # authentication with a new association. Doing it
789 # automatically opens the possibility for
790 # denial-of-service by a server that just returns expired
791 # associations (or really short-lived associations)
792 raise ProtocolError(
793 'Association with %s expired' % (server_url,))
795 if not assoc.checkMessageSignature(message):
796 raise ProtocolError('Bad signature')
798 else:
799 # It's not an association we know about. Stateless mode is our
800 # only possible path for recovery.
801 # XXX - async framework will not want to block on this call to
802 # _checkAuth.
803 if not self._checkAuth(message, server_url):
804 raise ProtocolError('Server denied check_authentication')
806 def _idResCheckForFields(self, message):
807 # XXX: this should be handled by the code that processes the
808 # response (that is, if a field is missing, we should not have
809 # to explicitly check that it's present, just make sure that
810 # the fields are actually being used by the rest of the code
811 # in tests). Although, which fields are signed does need to be
812 # checked somewhere.
813 basic_fields = ['return_to', 'assoc_handle', 'sig', 'signed']
814 basic_sig_fields = ['return_to', 'identity']
816 require_fields = {
817 OPENID2_NS: basic_fields + ['op_endpoint'],
818 OPENID1_NS: basic_fields + ['identity'],
821 require_sigs = {
822 OPENID2_NS: basic_sig_fields + ['response_nonce',
823 'claimed_id',
824 'assoc_handle',],
825 OPENID1_NS: basic_sig_fields,
828 for field in require_fields[message.getOpenIDNamespace()]:
829 if not message.hasKey(OPENID_NS, field):
830 raise ProtocolError('Missing required field %r' % (field,))
832 signed_list_str = message.getArg(OPENID_NS, 'signed', no_default)
833 signed_list = signed_list_str.split(',')
835 for field in require_sigs[message.getOpenIDNamespace()]:
836 # Field is present and not in signed list
837 if message.hasKey(OPENID_NS, field) and field not in signed_list:
838 raise ProtocolError('"%s" not signed' % (field,))
841 def _verifyReturnToArgs(query):
842 """Verify that the arguments in the return_to URL are present in this
843 response.
845 message = Message.fromPostArgs(query)
846 return_to = message.getArg(OPENID_NS, 'return_to')
848 if return_to is None:
849 raise ProtocolError('Response has no return_to')
851 parsed_url = urlparse(return_to)
852 rt_query = parsed_url[4]
853 parsed_args = cgi.parse_qsl(rt_query)
855 for rt_key, rt_value in parsed_args:
856 try:
857 value = query[rt_key]
858 if rt_value != value:
859 format = ("parameter %s value %r does not match "
860 "return_to's value %r")
861 raise ProtocolError(format % (rt_key, value, rt_value))
862 except KeyError:
863 format = "return_to parameter %s absent from query %r"
864 raise ProtocolError(format % (rt_key, query))
866 # Make sure all non-OpenID arguments in the response are also
867 # in the signed return_to.
868 bare_args = message.getArgs(BARE_NS)
869 for pair in bare_args.iteritems():
870 if pair not in parsed_args:
871 raise ProtocolError("Parameter %s not in return_to URL" % (pair[0],))
873 _verifyReturnToArgs = staticmethod(_verifyReturnToArgs)
875 def _verifyDiscoveryResults(self, resp_msg, endpoint=None):
877 Extract the information from an OpenID assertion message and
878 verify it against the original
880 @param endpoint: The endpoint that resulted from doing discovery
881 @param resp_msg: The id_res message object
883 @returns: the verified endpoint
885 if resp_msg.getOpenIDNamespace() == OPENID2_NS:
886 return self._verifyDiscoveryResultsOpenID2(resp_msg, endpoint)
887 else:
888 return self._verifyDiscoveryResultsOpenID1(resp_msg, endpoint)
891 def _verifyDiscoveryResultsOpenID2(self, resp_msg, endpoint):
892 to_match = OpenIDServiceEndpoint()
893 to_match.type_uris = [OPENID_2_0_TYPE]
894 to_match.claimed_id = resp_msg.getArg(OPENID2_NS, 'claimed_id')
895 to_match.local_id = resp_msg.getArg(OPENID2_NS, 'identity')
897 # Raises a KeyError when the op_endpoint is not present
898 to_match.server_url = resp_msg.getArg(
899 OPENID2_NS, 'op_endpoint', no_default)
901 # claimed_id and identifier must both be present or both
902 # be absent
903 if (to_match.claimed_id is None and
904 to_match.local_id is not None):
905 raise ProtocolError(
906 'openid.identity is present without openid.claimed_id')
908 elif (to_match.claimed_id is not None and
909 to_match.local_id is None):
910 raise ProtocolError(
911 'openid.claimed_id is present without openid.identity')
913 # This is a response without identifiers, so there's really no
914 # checking that we can do, so return an endpoint that's for
915 # the specified `openid.op_endpoint'
916 elif to_match.claimed_id is None:
917 return OpenIDServiceEndpoint.fromOPEndpointURL(to_match.server_url)
919 # The claimed ID doesn't match, so we have to do discovery
920 # again. This covers not using sessions, OP identifier
921 # endpoints and responses that didn't match the original
922 # request.
923 if not endpoint:
924 oidutil.log('No pre-discovered information supplied.')
925 endpoint = self._discoverAndVerify(to_match.claimed_id, [to_match])
926 else:
927 # The claimed ID matches, so we use the endpoint that we
928 # discovered in initiation. This should be the most common
929 # case.
930 try:
931 self._verifyDiscoverySingle(endpoint, to_match)
932 except ProtocolError, e:
933 oidutil.log(
934 "Error attempting to use stored discovery information: " +
935 str(e))
936 oidutil.log("Attempting discovery to verify endpoint")
937 endpoint = self._discoverAndVerify(
938 to_match.claimed_id, [to_match])
940 # The endpoint we return should have the claimed ID from the
941 # message we just verified, fragment and all.
942 if endpoint.claimed_id != to_match.claimed_id:
943 endpoint = copy.copy(endpoint)
944 endpoint.claimed_id = to_match.claimed_id
945 return endpoint
947 def _verifyDiscoveryResultsOpenID1(self, resp_msg, endpoint):
948 claimed_id = resp_msg.getArg(BARE_NS, self.openid1_return_to_identifier_name)
950 if endpoint is None and claimed_id is None:
951 raise RuntimeError(
952 'When using OpenID 1, the claimed ID must be supplied, '
953 'either by passing it through as a return_to parameter '
954 'or by using a session, and supplied to the GenericConsumer '
955 'as the argument to complete()')
956 elif endpoint is not None and claimed_id is None:
957 claimed_id = endpoint.claimed_id
959 to_match = OpenIDServiceEndpoint()
960 to_match.type_uris = [OPENID_1_1_TYPE]
961 to_match.local_id = resp_msg.getArg(OPENID1_NS, 'identity')
962 # Restore delegate information from the initiation phase
963 to_match.claimed_id = claimed_id
965 if to_match.local_id is None:
966 raise ProtocolError('Missing required field openid.identity')
968 to_match_1_0 = copy.copy(to_match)
969 to_match_1_0.type_uris = [OPENID_1_0_TYPE]
971 if endpoint is not None:
972 try:
973 try:
974 self._verifyDiscoverySingle(endpoint, to_match)
975 except TypeURIMismatch:
976 self._verifyDiscoverySingle(endpoint, to_match_1_0)
977 except ProtocolError, e:
978 oidutil.log("Error attempting to use stored discovery information: " +
979 str(e))
980 oidutil.log("Attempting discovery to verify endpoint")
981 else:
982 return endpoint
984 # Endpoint is either bad (failed verification) or None
985 return self._discoverAndVerify(claimed_id, [to_match, to_match_1_0])
987 def _verifyDiscoverySingle(self, endpoint, to_match):
988 """Verify that the given endpoint matches the information
989 extracted from the OpenID assertion, and raise an exception if
990 there is a mismatch.
992 @type endpoint: openid.consumer.discover.OpenIDServiceEndpoint
993 @type to_match: openid.consumer.discover.OpenIDServiceEndpoint
995 @rtype: NoneType
997 @raises ProtocolError: when the endpoint does not match the
998 discovered information.
1000 # Every type URI that's in the to_match endpoint has to be
1001 # present in the discovered endpoint.
1002 for type_uri in to_match.type_uris:
1003 if not endpoint.usesExtension(type_uri):
1004 raise TypeURIMismatch(type_uri, endpoint)
1006 # Fragments do not influence discovery, so we can't compare a
1007 # claimed identifier with a fragment to discovered information.
1008 defragged_claimed_id, _ = urldefrag(to_match.claimed_id)
1009 if defragged_claimed_id != endpoint.claimed_id:
1010 raise ProtocolError(
1011 'Claimed ID does not match (different subjects!), '
1012 'Expected %s, got %s' %
1013 (defragged_claimed_id, endpoint.claimed_id))
1015 if to_match.getLocalID() != endpoint.getLocalID():
1016 raise ProtocolError('local_id mismatch. Expected %s, got %s' %
1017 (to_match.getLocalID(), endpoint.getLocalID()))
1019 # If the server URL is None, this must be an OpenID 1
1020 # response, because op_endpoint is a required parameter in
1021 # OpenID 2. In that case, we don't actually care what the
1022 # discovered server_url is, because signature checking or
1023 # check_auth should take care of that check for us.
1024 if to_match.server_url is None:
1025 assert to_match.preferredNamespace() == OPENID1_NS, (
1026 """The code calling this must ensure that OpenID 2
1027 responses have a non-none `openid.op_endpoint' and
1028 that it is set as the `server_url' attribute of the
1029 `to_match' endpoint.""")
1031 elif to_match.server_url != endpoint.server_url:
1032 raise ProtocolError('OP Endpoint mismatch. Expected %s, got %s' %
1033 (to_match.server_url, endpoint.server_url))
1035 def _discoverAndVerify(self, claimed_id, to_match_endpoints):
1036 """Given an endpoint object created from the information in an
1037 OpenID response, perform discovery and verify the discovery
1038 results, returning the matching endpoint that is the result of
1039 doing that discovery.
1041 @type to_match: openid.consumer.discover.OpenIDServiceEndpoint
1042 @param to_match: The endpoint whose information we're confirming
1044 @rtype: openid.consumer.discover.OpenIDServiceEndpoint
1045 @returns: The result of performing discovery on the claimed
1046 identifier in `to_match'
1048 @raises DiscoveryFailure: when discovery fails.
1050 oidutil.log('Performing discovery on %s' % (claimed_id,))
1051 _, services = self._discover(claimed_id)
1052 if not services:
1053 raise DiscoveryFailure('No OpenID information found at %s' %
1054 (claimed_id,), None)
1055 return self._verifyDiscoveredServices(claimed_id, services,
1056 to_match_endpoints)
1059 def _verifyDiscoveredServices(self, claimed_id, services, to_match_endpoints):
1060 """See @L{_discoverAndVerify}"""
1062 # Search the services resulting from discovery to find one
1063 # that matches the information from the assertion
1064 failure_messages = []
1065 for endpoint in services:
1066 for to_match_endpoint in to_match_endpoints:
1067 try:
1068 self._verifyDiscoverySingle(
1069 endpoint, to_match_endpoint)
1070 except ProtocolError, why:
1071 failure_messages.append(str(why))
1072 else:
1073 # It matches, so discover verification has
1074 # succeeded. Return this endpoint.
1075 return endpoint
1076 else:
1077 oidutil.log('Discovery verification failure for %s' %
1078 (claimed_id,))
1079 for failure_message in failure_messages:
1080 oidutil.log(' * Endpoint mismatch: ' + failure_message)
1082 raise DiscoveryFailure(
1083 'No matching endpoint found after discovering %s'
1084 % (claimed_id,), None)
1086 def _checkAuth(self, message, server_url):
1087 """Make a check_authentication request to verify this message.
1089 @returns: True if the request is valid.
1090 @rtype: bool
1092 oidutil.log('Using OpenID check_authentication')
1093 request = self._createCheckAuthRequest(message)
1094 if request is None:
1095 return False
1096 try:
1097 response = self._makeKVPost(request, server_url)
1098 except (fetchers.HTTPFetchingError, ServerError), e:
1099 oidutil.log('check_authentication failed: %s' % (e[0],))
1100 return False
1101 else:
1102 return self._processCheckAuthResponse(response, server_url)
1104 def _createCheckAuthRequest(self, message):
1105 """Generate a check_authentication request message given an
1106 id_res message.
1108 signed = message.getArg(OPENID_NS, 'signed')
1109 if signed:
1110 for k in signed.split(','):
1111 oidutil.log(k)
1112 val = message.getAliasedArg(k)
1114 # Signed value is missing
1115 if val is None:
1116 oidutil.log('Missing signed field %r' % (k,))
1117 return None
1119 check_auth_message = message.copy()
1120 check_auth_message.setArg(OPENID_NS, 'mode', 'check_authentication')
1121 return check_auth_message
1123 def _processCheckAuthResponse(self, response, server_url):
1124 """Process the response message from a check_authentication
1125 request, invalidating associations if requested.
1127 is_valid = response.getArg(OPENID_NS, 'is_valid', 'false')
1129 invalidate_handle = response.getArg(OPENID_NS, 'invalidate_handle')
1130 if invalidate_handle is not None:
1131 oidutil.log(
1132 'Received "invalidate_handle" from server %s' % (server_url,))
1133 if self.store is None:
1134 oidutil.log('Unexpectedly got invalidate_handle without '
1135 'a store!')
1136 else:
1137 self.store.removeAssociation(server_url, invalidate_handle)
1139 if is_valid == 'true':
1140 return True
1141 else:
1142 oidutil.log('Server responds that checkAuth call is not valid')
1143 return False
1145 def _getAssociation(self, endpoint):
1146 """Get an association for the endpoint's server_url.
1148 First try seeing if we have a good association in the
1149 store. If we do not, then attempt to negotiate an association
1150 with the server.
1152 If we negotiate a good association, it will get stored.
1154 @returns: A valid association for the endpoint's server_url or None
1155 @rtype: openid.association.Association or NoneType
1157 assoc = self.store.getAssociation(endpoint.server_url)
1159 if assoc is None or assoc.expiresIn <= 0:
1160 assoc = self._negotiateAssociation(endpoint)
1161 if assoc is not None:
1162 self.store.storeAssociation(endpoint.server_url, assoc)
1164 return assoc
1166 def _negotiateAssociation(self, endpoint):
1167 """Make association requests to the server, attempting to
1168 create a new association.
1170 @returns: a new association object
1172 @rtype: L{openid.association.Association}
1174 # Get our preferred session/association type from the negotiatior.
1175 assoc_type, session_type = self.negotiator.getAllowedType()
1177 try:
1178 assoc = self._requestAssociation(
1179 endpoint, assoc_type, session_type)
1180 except ServerError, why:
1181 supportedTypes = self._extractSupportedAssociationType(why,
1182 endpoint,
1183 assoc_type)
1184 if supportedTypes is not None:
1185 assoc_type, session_type = supportedTypes
1186 # Attempt to create an association from the assoc_type
1187 # and session_type that the server told us it
1188 # supported.
1189 try:
1190 assoc = self._requestAssociation(
1191 endpoint, assoc_type, session_type)
1192 except ServerError, why:
1193 # Do not keep trying, since it rejected the
1194 # association type that it told us to use.
1195 oidutil.log('Server %s refused its suggested association '
1196 'type: session_type=%s, assoc_type=%s'
1197 % (endpoint.server_url, session_type,
1198 assoc_type))
1199 return None
1200 else:
1201 return assoc
1202 else:
1203 return assoc
1205 def _extractSupportedAssociationType(self, server_error, endpoint,
1206 assoc_type):
1207 """Handle ServerErrors resulting from association requests.
1209 @returns: If server replied with an C{unsupported-type} error,
1210 return a tuple of supported C{association_type}, C{session_type}.
1211 Otherwise logs the error and returns None.
1212 @rtype: tuple or None
1214 # Any error message whose code is not 'unsupported-type'
1215 # should be considered a total failure.
1216 if server_error.error_code != 'unsupported-type' or \
1217 server_error.message.isOpenID1():
1218 oidutil.log(
1219 'Server error when requesting an association from %r: %s'
1220 % (endpoint.server_url, server_error.error_text))
1221 return None
1223 # The server didn't like the association/session type
1224 # that we sent, and it sent us back a message that
1225 # might tell us how to handle it.
1226 oidutil.log(
1227 'Unsupported association type %s: %s' % (assoc_type,
1228 server_error.error_text,))
1230 # Extract the session_type and assoc_type from the
1231 # error message
1232 assoc_type = server_error.message.getArg(OPENID_NS, 'assoc_type')
1233 session_type = server_error.message.getArg(OPENID_NS, 'session_type')
1235 if assoc_type is None or session_type is None:
1236 oidutil.log('Server responded with unsupported association '
1237 'session but did not supply a fallback.')
1238 return None
1239 elif not self.negotiator.isAllowed(assoc_type, session_type):
1240 fmt = ('Server sent unsupported session/association type: '
1241 'session_type=%s, assoc_type=%s')
1242 oidutil.log(fmt % (session_type, assoc_type))
1243 return None
1244 else:
1245 return assoc_type, session_type
1248 def _requestAssociation(self, endpoint, assoc_type, session_type):
1249 """Make and process one association request to this endpoint's
1250 OP endpoint URL.
1252 @returns: An association object or None if the association
1253 processing failed.
1255 @raises ServerError: when the remote OpenID server returns an error.
1257 assoc_session, args = self._createAssociateRequest(
1258 endpoint, assoc_type, session_type)
1260 try:
1261 response = self._makeKVPost(args, endpoint.server_url)
1262 except fetchers.HTTPFetchingError, why:
1263 oidutil.log('openid.associate request failed: %s' % (why[0],))
1264 return None
1266 try:
1267 assoc = self._extractAssociation(response, assoc_session)
1268 except KeyError, why:
1269 oidutil.log('Missing required parameter in response from %s: %s'
1270 % (endpoint.server_url, why[0]))
1271 return None
1272 except ProtocolError, why:
1273 oidutil.log('Protocol error parsing response from %s: %s' % (
1274 endpoint.server_url, why[0]))
1275 return None
1276 else:
1277 return assoc
1279 def _createAssociateRequest(self, endpoint, assoc_type, session_type):
1280 """Create an association request for the given assoc_type and
1281 session_type.
1283 @param endpoint: The endpoint whose server_url will be
1284 queried. The important bit about the endpoint is whether
1285 it's in compatiblity mode (OpenID 1.1)
1287 @param assoc_type: The association type that the request
1288 should ask for.
1289 @type assoc_type: str
1291 @param session_type: The session type that should be used in
1292 the association request. The session_type is used to
1293 create an association session object, and that session
1294 object is asked for any additional fields that it needs to
1295 add to the request.
1296 @type session_type: str
1298 @returns: a pair of the association session object and the
1299 request message that will be sent to the server.
1300 @rtype: (association session type (depends on session_type),
1301 openid.message.Message)
1303 session_type_class = self.session_types[session_type]
1304 assoc_session = session_type_class()
1306 args = {
1307 'mode': 'associate',
1308 'assoc_type': assoc_type,
1311 if not endpoint.compatibilityMode():
1312 args['ns'] = OPENID2_NS
1314 # Leave out the session type if we're in compatibility mode
1315 # *and* it's no-encryption.
1316 if (not endpoint.compatibilityMode() or
1317 assoc_session.session_type != 'no-encryption'):
1318 args['session_type'] = assoc_session.session_type
1320 args.update(assoc_session.getRequest())
1321 message = Message.fromOpenIDArgs(args)
1322 return assoc_session, message
1324 def _getOpenID1SessionType(self, assoc_response):
1325 """Given an association response message, extract the OpenID
1326 1.X session type.
1328 This function mostly takes care of the 'no-encryption' default
1329 behavior in OpenID 1.
1331 If the association type is plain-text, this function will
1332 return 'no-encryption'
1334 @returns: The association type for this message
1335 @rtype: str
1337 @raises KeyError: when the session_type field is absent.
1339 # If it's an OpenID 1 message, allow session_type to default
1340 # to None (which signifies "no-encryption")
1341 session_type = assoc_response.getArg(OPENID1_NS, 'session_type')
1343 # Handle the differences between no-encryption association
1344 # respones in OpenID 1 and 2:
1346 # no-encryption is not really a valid session type for
1347 # OpenID 1, but we'll accept it anyway, while issuing a
1348 # warning.
1349 if session_type == 'no-encryption':
1350 oidutil.log('WARNING: OpenID server sent "no-encryption"'
1351 'for OpenID 1.X')
1353 # Missing or empty session type is the way to flag a
1354 # 'no-encryption' response. Change the session type to
1355 # 'no-encryption' so that it can be handled in the same
1356 # way as OpenID 2 'no-encryption' respones.
1357 elif session_type == '' or session_type is None:
1358 session_type = 'no-encryption'
1360 return session_type
1362 def _extractAssociation(self, assoc_response, assoc_session):
1363 """Attempt to extract an association from the response, given
1364 the association response message and the established
1365 association session.
1367 @param assoc_response: The association response message from
1368 the server
1369 @type assoc_response: openid.message.Message
1371 @param assoc_session: The association session object that was
1372 used when making the request
1373 @type assoc_session: depends on the session type of the request
1375 @raises ProtocolError: when data is malformed
1376 @raises KeyError: when a field is missing
1378 @rtype: openid.association.Association
1380 # Extract the common fields from the response, raising an
1381 # exception if they are not found
1382 assoc_type = assoc_response.getArg(
1383 OPENID_NS, 'assoc_type', no_default)
1384 assoc_handle = assoc_response.getArg(
1385 OPENID_NS, 'assoc_handle', no_default)
1387 # expires_in is a base-10 string. The Python parsing will
1388 # accept literals that have whitespace around them and will
1389 # accept negative values. Neither of these are really in-spec,
1390 # but we think it's OK to accept them.
1391 expires_in_str = assoc_response.getArg(
1392 OPENID_NS, 'expires_in', no_default)
1393 try:
1394 expires_in = int(expires_in_str)
1395 except ValueError, why:
1396 raise ProtocolError('Invalid expires_in field: %s' % (why[0],))
1398 # OpenID 1 has funny association session behaviour.
1399 if assoc_response.isOpenID1():
1400 session_type = self._getOpenID1SessionType(assoc_response)
1401 else:
1402 session_type = assoc_response.getArg(
1403 OPENID2_NS, 'session_type', no_default)
1405 # Session type mismatch
1406 if assoc_session.session_type != session_type:
1407 if (assoc_response.isOpenID1() and
1408 session_type == 'no-encryption'):
1409 # In OpenID 1, any association request can result in a
1410 # 'no-encryption' association response. Setting
1411 # assoc_session to a new no-encryption session should
1412 # make the rest of this function work properly for
1413 # that case.
1414 assoc_session = PlainTextConsumerSession()
1415 else:
1416 # Any other mismatch, regardless of protocol version
1417 # results in the failure of the association session
1418 # altogether.
1419 fmt = 'Session type mismatch. Expected %r, got %r'
1420 message = fmt % (assoc_session.session_type, session_type)
1421 raise ProtocolError(message)
1423 # Make sure assoc_type is valid for session_type
1424 if assoc_type not in assoc_session.allowed_assoc_types:
1425 fmt = 'Unsupported assoc_type for session %s returned: %s'
1426 raise ProtocolError(fmt % (assoc_session.session_type, assoc_type))
1428 # Delegate to the association session to extract the secret
1429 # from the response, however is appropriate for that session
1430 # type.
1431 try:
1432 secret = assoc_session.extractSecret(assoc_response)
1433 except ValueError, why:
1434 fmt = 'Malformed response for %s session: %s'
1435 raise ProtocolError(fmt % (assoc_session.session_type, why[0]))
1437 return Association.fromExpiresIn(
1438 expires_in, assoc_handle, secret, assoc_type)
1440 class AuthRequest(object):
1441 """An object that holds the state necessary for generating an
1442 OpenID authentication request. This object holds the association
1443 with the server and the discovered information with which the
1444 request will be made.
1446 It is separate from the consumer because you may wish to add
1447 things to the request before sending it on its way to the
1448 server. It also has serialization options that let you encode the
1449 authentication request as a URL or as a form POST.
1452 def __init__(self, endpoint, assoc):
1454 Creates a new AuthRequest object. This just stores each
1455 argument in an appropriately named field.
1457 Users of this library should not create instances of this
1458 class. Instances of this class are created by the library
1459 when needed.
1461 self.assoc = assoc
1462 self.endpoint = endpoint
1463 self.return_to_args = {}
1464 self.message = Message(endpoint.preferredNamespace())
1465 self._anonymous = False
1467 def setAnonymous(self, is_anonymous):
1468 """Set whether this request should be made anonymously. If a
1469 request is anonymous, the identifier will not be sent in the
1470 request. This is only useful if you are making another kind of
1471 request with an extension in this request.
1473 Anonymous requests are not allowed when the request is made
1474 with OpenID 1.
1476 @raises ValueError: when attempting to set an OpenID1 request
1477 as anonymous
1479 if is_anonymous and self.message.isOpenID1():
1480 raise ValueError('OpenID 1 requests MUST include the '
1481 'identifier in the request')
1482 else:
1483 self._anonymous = is_anonymous
1485 def addExtension(self, extension_request):
1486 """Add an extension to this checkid request.
1488 @param extension_request: An object that implements the
1489 extension interface for adding arguments to an OpenID
1490 message.
1492 extension_request.toMessage(self.message)
1494 def addExtensionArg(self, namespace, key, value):
1495 """Add an extension argument to this OpenID authentication
1496 request.
1498 Use caution when adding arguments, because they will be
1499 URL-escaped and appended to the redirect URL, which can easily
1500 get quite long.
1502 @param namespace: The namespace for the extension. For
1503 example, the simple registration extension uses the
1504 namespace C{sreg}.
1506 @type namespace: str
1508 @param key: The key within the extension namespace. For
1509 example, the nickname field in the simple registration
1510 extension's key is C{nickname}.
1512 @type key: str
1514 @param value: The value to provide to the server for this
1515 argument.
1517 @type value: str
1519 self.message.setArg(namespace, key, value)
1521 def getMessage(self, realm, return_to=None, immediate=False):
1522 """Produce a L{openid.message.Message} representing this request.
1524 @param realm: The URL (or URL pattern) that identifies your
1525 web site to the user when she is authorizing it.
1527 @type realm: str
1529 @param return_to: The URL that the OpenID provider will send the
1530 user back to after attempting to verify her identity.
1532 Not specifying a return_to URL means that the user will not
1533 be returned to the site issuing the request upon its
1534 completion.
1536 @type return_to: str
1538 @param immediate: If True, the OpenID provider is to send back
1539 a response immediately, useful for behind-the-scenes
1540 authentication attempts. Otherwise the OpenID provider
1541 may engage the user before providing a response. This is
1542 the default case, as the user may need to provide
1543 credentials or approve the request before a positive
1544 response can be sent.
1546 @type immediate: bool
1548 @returntype: L{openid.message.Message}
1550 if return_to:
1551 return_to = oidutil.appendArgs(return_to, self.return_to_args)
1552 elif immediate:
1553 raise ValueError(
1554 '"return_to" is mandatory when using "checkid_immediate"')
1555 elif self.message.isOpenID1():
1556 raise ValueError('"return_to" is mandatory for OpenID 1 requests')
1557 elif self.return_to_args:
1558 raise ValueError('extra "return_to" arguments were specified, '
1559 'but no return_to was specified')
1561 if immediate:
1562 mode = 'checkid_immediate'
1563 else:
1564 mode = 'checkid_setup'
1566 message = self.message.copy()
1567 if message.isOpenID1():
1568 realm_key = 'trust_root'
1569 else:
1570 realm_key = 'realm'
1572 message.updateArgs(OPENID_NS,
1574 realm_key:realm,
1575 'mode':mode,
1576 'return_to':return_to,
1579 if not self._anonymous:
1580 if self.endpoint.isOPIdentifier():
1581 # This will never happen when we're in compatibility
1582 # mode, as long as isOPIdentifier() returns False
1583 # whenever preferredNamespace() returns OPENID1_NS.
1584 claimed_id = request_identity = IDENTIFIER_SELECT
1585 else:
1586 request_identity = self.endpoint.getLocalID()
1587 claimed_id = self.endpoint.claimed_id
1589 # This is true for both OpenID 1 and 2
1590 message.setArg(OPENID_NS, 'identity', request_identity)
1592 if message.isOpenID2():
1593 message.setArg(OPENID2_NS, 'claimed_id', claimed_id)
1595 if self.assoc:
1596 message.setArg(OPENID_NS, 'assoc_handle', self.assoc.handle)
1597 assoc_log_msg = 'with assocication %s' % (self.assoc.handle,)
1598 else:
1599 assoc_log_msg = 'using stateless mode.'
1601 oidutil.log("Generated %s request to %s %s" %
1602 (mode, self.endpoint.server_url, assoc_log_msg))
1604 return message
1606 def redirectURL(self, realm, return_to=None, immediate=False):
1607 """Returns a URL with an encoded OpenID request.
1609 The resulting URL is the OpenID provider's endpoint URL with
1610 parameters appended as query arguments. You should redirect
1611 the user agent to this URL.
1613 OpenID 2.0 endpoints also accept POST requests, see
1614 C{L{shouldSendRedirect}} and C{L{formMarkup}}.
1616 @param realm: The URL (or URL pattern) that identifies your
1617 web site to the user when she is authorizing it.
1619 @type realm: str
1621 @param return_to: The URL that the OpenID provider will send the
1622 user back to after attempting to verify her identity.
1624 Not specifying a return_to URL means that the user will not
1625 be returned to the site issuing the request upon its
1626 completion.
1628 @type return_to: str
1630 @param immediate: If True, the OpenID provider is to send back
1631 a response immediately, useful for behind-the-scenes
1632 authentication attempts. Otherwise the OpenID provider
1633 may engage the user before providing a response. This is
1634 the default case, as the user may need to provide
1635 credentials or approve the request before a positive
1636 response can be sent.
1638 @type immediate: bool
1640 @returns: The URL to redirect the user agent to.
1642 @returntype: str
1644 message = self.getMessage(realm, return_to, immediate)
1645 return message.toURL(self.endpoint.server_url)
1647 def formMarkup(self, realm, return_to=None, immediate=False,
1648 form_tag_attrs=None):
1649 """Get html for a form to submit this request to the IDP.
1651 @param form_tag_attrs: Dictionary of attributes to be added to
1652 the form tag. 'accept-charset' and 'enctype' have defaults
1653 that can be overridden. If a value is supplied for
1654 'action' or 'method', it will be replaced.
1655 @type form_tag_attrs: {unicode: unicode}
1657 message = self.getMessage(realm, return_to, immediate)
1658 return message.toFormMarkup(self.endpoint.server_url,
1659 form_tag_attrs)
1661 def htmlMarkup(self, realm, return_to=None, immediate=False,
1662 form_tag_attrs=None):
1663 """Get an autosubmitting HTML page that submits this request to the
1664 IDP. This is just a wrapper for formMarkup.
1666 @see: formMarkup
1668 @returns: str
1670 return oidutil.autoSubmitHTML(self.formMarkup(realm,
1671 return_to,
1672 immediate,
1673 form_tag_attrs))
1675 def shouldSendRedirect(self):
1676 """Should this OpenID authentication request be sent as a HTTP
1677 redirect or as a POST (form submission)?
1679 @rtype: bool
1681 return self.endpoint.compatibilityMode()
1683 FAILURE = 'failure'
1684 SUCCESS = 'success'
1685 CANCEL = 'cancel'
1686 SETUP_NEEDED = 'setup_needed'
1688 class Response(object):
1689 status = None
1691 def setEndpoint(self, endpoint):
1692 self.endpoint = endpoint
1693 if endpoint is None:
1694 self.identity_url = None
1695 else:
1696 self.identity_url = endpoint.claimed_id
1698 def getDisplayIdentifier(self):
1699 """Return the display identifier for this response.
1701 The display identifier is related to the Claimed Identifier, but the
1702 two are not always identical. The display identifier is something the
1703 user should recognize as what they entered, whereas the response's
1704 claimed identifier (in the L{identity_url} attribute) may have extra
1705 information for better persistence.
1707 URLs will be stripped of their fragments for display. XRIs will
1708 display the human-readable identifier (i-name) instead of the
1709 persistent identifier (i-number).
1711 Use the display identifier in your user interface. Use
1712 L{identity_url} for querying your database or authorization server.
1714 if self.endpoint is not None:
1715 return self.endpoint.getDisplayIdentifier()
1716 return None
1718 class SuccessResponse(Response):
1719 """A response with a status of SUCCESS. Indicates that this request is a
1720 successful acknowledgement from the OpenID server that the
1721 supplied URL is, indeed controlled by the requesting agent.
1723 @ivar identity_url: The identity URL that has been authenticated; the Claimed Identifier.
1724 See also L{getDisplayIdentifier}.
1726 @ivar endpoint: The endpoint that authenticated the identifier. You
1727 may access other discovered information related to this endpoint,
1728 such as the CanonicalID of an XRI, through this object.
1729 @type endpoint: L{OpenIDServiceEndpoint<openid.consumer.discover.OpenIDServiceEndpoint>}
1731 @ivar signed_fields: The arguments in the server's response that
1732 were signed and verified.
1734 @cvar status: SUCCESS
1737 status = SUCCESS
1739 def __init__(self, endpoint, message, signed_fields=None):
1740 # Don't use setEndpoint, because endpoint should never be None
1741 # for a successfull transaction.
1742 self.endpoint = endpoint
1743 self.identity_url = endpoint.claimed_id
1745 self.message = message
1747 if signed_fields is None:
1748 signed_fields = []
1749 self.signed_fields = signed_fields
1751 def isOpenID1(self):
1752 """Was this authentication response an OpenID 1 authentication
1753 response?
1755 return self.message.isOpenID1()
1757 def isSigned(self, ns_uri, ns_key):
1758 """Return whether a particular key is signed, regardless of
1759 its namespace alias
1761 return self.message.getKey(ns_uri, ns_key) in self.signed_fields
1763 def getSigned(self, ns_uri, ns_key, default=None):
1764 """Return the specified signed field if available,
1765 otherwise return default
1767 if self.isSigned(ns_uri, ns_key):
1768 return self.message.getArg(ns_uri, ns_key, default)
1769 else:
1770 return default
1772 def getSignedNS(self, ns_uri):
1773 """Get signed arguments from the response message. Return a
1774 dict of all arguments in the specified namespace. If any of
1775 the arguments are not signed, return None.
1777 msg_args = self.message.getArgs(ns_uri)
1779 for key in msg_args.iterkeys():
1780 if not self.isSigned(ns_uri, key):
1781 oidutil.log("SuccessResponse.getSignedNS: (%s, %s) not signed."
1782 % (ns_uri, key))
1783 return None
1785 return msg_args
1787 def extensionResponse(self, namespace_uri, require_signed):
1788 """Return response arguments in the specified namespace.
1790 @param namespace_uri: The namespace URI of the arguments to be
1791 returned.
1793 @param require_signed: True if the arguments should be among
1794 those signed in the response, False if you don't care.
1796 If require_signed is True and the arguments are not signed,
1797 return None.
1799 if require_signed:
1800 return self.getSignedNS(namespace_uri)
1801 else:
1802 return self.message.getArgs(namespace_uri)
1804 def getReturnTo(self):
1805 """Get the openid.return_to argument from this response.
1807 This is useful for verifying that this request was initiated
1808 by this consumer.
1810 @returns: The return_to URL supplied to the server on the
1811 initial request, or C{None} if the response did not contain
1812 an C{openid.return_to} argument.
1814 @returntype: str
1816 return self.getSigned(OPENID_NS, 'return_to')
1818 def __eq__(self, other):
1819 return (
1820 (self.endpoint == other.endpoint) and
1821 (self.identity_url == other.identity_url) and
1822 (self.message == other.message) and
1823 (self.signed_fields == other.signed_fields) and
1824 (self.status == other.status))
1826 def __ne__(self, other):
1827 return not (self == other)
1829 def __repr__(self):
1830 return '<%s.%s id=%r signed=%r>' % (
1831 self.__class__.__module__,
1832 self.__class__.__name__,
1833 self.identity_url, self.signed_fields)
1836 class FailureResponse(Response):
1837 """A response with a status of FAILURE. Indicates that the OpenID
1838 protocol has failed. This could be locally or remotely triggered.
1840 @ivar identity_url: The identity URL for which authenitcation was
1841 attempted, if it can be determined. Otherwise, None.
1843 @ivar message: A message indicating why the request failed, if one
1844 is supplied. otherwise, None.
1846 @cvar status: FAILURE
1849 status = FAILURE
1851 def __init__(self, endpoint, message=None, contact=None,
1852 reference=None):
1853 self.setEndpoint(endpoint)
1854 self.message = message
1855 self.contact = contact
1856 self.reference = reference
1858 def __repr__(self):
1859 return "<%s.%s id=%r message=%r>" % (
1860 self.__class__.__module__, self.__class__.__name__,
1861 self.identity_url, self.message)
1864 class CancelResponse(Response):
1865 """A response with a status of CANCEL. Indicates that the user
1866 cancelled the OpenID authentication request.
1868 @ivar identity_url: The identity URL for which authenitcation was
1869 attempted, if it can be determined. Otherwise, None.
1871 @cvar status: CANCEL
1874 status = CANCEL
1876 def __init__(self, endpoint):
1877 self.setEndpoint(endpoint)
1879 class SetupNeededResponse(Response):
1880 """A response with a status of SETUP_NEEDED. Indicates that the
1881 request was in immediate mode, and the server is unable to
1882 authenticate the user without further interaction.
1884 @ivar identity_url: The identity URL for which authenitcation was
1885 attempted.
1887 @ivar setup_url: A URL that can be used to send the user to the
1888 server to set up for authentication. The user should be
1889 redirected in to the setup_url, either in the current window
1890 or in a new browser window. C{None} in OpenID 2.0.
1892 @cvar status: SETUP_NEEDED
1895 status = SETUP_NEEDED
1897 def __init__(self, endpoint, setup_url=None):
1898 self.setEndpoint(endpoint)
1899 self.setup_url = setup_url