Commits

Anonymous committed 247c221

add openid library

  • Participants
  • Parent commits 494e2e1

Comments (0)

Files changed (124)

openid/__init__.py

+"""
+This package is an implementation of the OpenID specification in
+Python.  It contains code for both server and consumer
+implementations.  For information on implementing an OpenID consumer,
+see the C{L{openid.consumer.consumer}} module.  For information on
+implementing an OpenID server, see the C{L{openid.server.server}}
+module.
+
+@contact: U{dev@lists.openidenabled.com
+    <http://lists.openidenabled.com/mailman/listinfo/dev>}
+
+@copyright: (C) 2005-2008 JanRain, Inc.
+
+@license: Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+    U{http://www.apache.org/licenses/LICENSE-2.0}
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions
+    and limitations under the License.
+"""
+
+__version__ = '[library version:2.2.1]'[17:-1]
+
+__all__ = [
+    'association',
+    'consumer',
+    'cryptutil',
+    'dh',
+    'extension',
+    'extensions',
+    'fetchers',
+    'kvform',
+    'message',
+    'oidutil',
+    'server',
+    'sreg',
+    'store',
+    'urinorm',
+    'yadis',
+    ]
+
+# Parse the version info
+try:
+    version_info = map(int, __version__.split('.'))
+except ValueError:
+    version_info = (None, None, None)
+else:
+    if len(version_info) != 3:
+        version_info = (None, None, None)
+    else:
+        version_info = tuple(version_info)

openid/association.py

+# -*- test-case-name: openid.test.test_association -*-
+"""
+This module contains code for dealing with associations between
+consumers and servers. Associations contain a shared secret that is
+used to sign C{openid.mode=id_res} messages.
+
+Users of the library should not usually need to interact directly with
+associations. The L{store<openid.store>},
+L{server<openid.server.server>} and
+L{consumer<openid.consumer.consumer>} objects will create and manage
+the associations. The consumer and server code will make use of a
+C{L{SessionNegotiator}} when managing associations, which enables
+users to express a preference for what kind of associations should be
+allowed, and what kind of exchange should be done to establish the
+association.
+
+@var default_negotiator: A C{L{SessionNegotiator}} that allows all
+    association types that are specified by the OpenID
+    specification. It prefers to use HMAC-SHA1/DH-SHA1, if it's
+    available. If HMAC-SHA256 is not supported by your Python runtime,
+    HMAC-SHA256 and DH-SHA256 will not be available.
+
+@var encrypted_negotiator: A C{L{SessionNegotiator}} that
+    does not support C{'no-encryption'} associations. It prefers
+    HMAC-SHA1/DH-SHA1 association types if available.
+"""
+
+__all__ = [
+    'default_negotiator',
+    'encrypted_negotiator',
+    'SessionNegotiator',
+    'Association',
+    ]
+
+import time
+
+from openid import cryptutil
+from openid import kvform
+from openid import oidutil
+from openid.message import OPENID_NS
+
+all_association_types = [
+    'HMAC-SHA1',
+    'HMAC-SHA256',
+    ]
+
+if hasattr(cryptutil, 'hmacSha256'):
+    supported_association_types = list(all_association_types)
+
+    default_association_order = [
+        ('HMAC-SHA1', 'DH-SHA1'),
+        ('HMAC-SHA1', 'no-encryption'),
+        ('HMAC-SHA256', 'DH-SHA256'),
+        ('HMAC-SHA256', 'no-encryption'),
+        ]
+
+    only_encrypted_association_order = [
+        ('HMAC-SHA1', 'DH-SHA1'),
+        ('HMAC-SHA256', 'DH-SHA256'),
+        ]
+else:
+    supported_association_types = ['HMAC-SHA1']
+
+    default_association_order = [
+        ('HMAC-SHA1', 'DH-SHA1'),
+        ('HMAC-SHA1', 'no-encryption'),
+        ]
+
+    only_encrypted_association_order = [
+        ('HMAC-SHA1', 'DH-SHA1'),
+        ]
+
+def getSessionTypes(assoc_type):
+    """Return the allowed session types for a given association type"""
+    assoc_to_session = {
+        'HMAC-SHA1': ['DH-SHA1', 'no-encryption'],
+        'HMAC-SHA256': ['DH-SHA256', 'no-encryption'],
+        }
+    return assoc_to_session.get(assoc_type, [])
+
+def checkSessionType(assoc_type, session_type):
+    """Check to make sure that this pair of assoc type and session
+    type are allowed"""
+    if session_type not in getSessionTypes(assoc_type):
+        raise ValueError(
+            'Session type %r not valid for assocation type %r'
+            % (session_type, assoc_type))
+
+class SessionNegotiator(object):
+    """A session negotiator controls the allowed and preferred
+    association types and association session types. Both the
+    C{L{Consumer<openid.consumer.consumer.Consumer>}} and
+    C{L{Server<openid.server.server.Server>}} use negotiators when
+    creating associations.
+
+    You can create and use negotiators if you:
+
+     - Do not want to do Diffie-Hellman key exchange because you use
+       transport-layer encryption (e.g. SSL)
+
+     - Want to use only SHA-256 associations
+
+     - Do not want to support plain-text associations over a non-secure
+       channel
+
+    It is up to you to set a policy for what kinds of associations to
+    accept. By default, the library will make any kind of association
+    that is allowed in the OpenID 2.0 specification.
+
+    Use of negotiators in the library
+    =================================
+
+    When a consumer makes an association request, it calls
+    C{L{getAllowedType}} to get the preferred association type and
+    association session type.
+
+    The server gets a request for a particular association/session
+    type and calls C{L{isAllowed}} to determine if it should
+    create an association. If it is supported, negotiation is
+    complete. If it is not, the server calls C{L{getAllowedType}} to
+    get an allowed association type to return to the consumer.
+
+    If the consumer gets an error response indicating that the
+    requested association/session type is not supported by the server
+    that contains an assocation/session type to try, it calls
+    C{L{isAllowed}} to determine if it should try again with the
+    given combination of association/session type.
+
+    @ivar allowed_types: A list of association/session types that are
+        allowed by the server. The order of the pairs in this list
+        determines preference. If an association/session type comes
+        earlier in the list, the library is more likely to use that
+        type.
+    @type allowed_types: [(str, str)]
+    """
+
+    def __init__(self, allowed_types):
+        self.setAllowedTypes(allowed_types)
+
+    def copy(self):
+        return self.__class__(list(self.allowed_types))
+
+    def setAllowedTypes(self, allowed_types):
+        """Set the allowed association types, checking to make sure
+        each combination is valid."""
+        for (assoc_type, session_type) in allowed_types:
+            checkSessionType(assoc_type, session_type)
+
+        self.allowed_types = allowed_types
+
+    def addAllowedType(self, assoc_type, session_type=None):
+        """Add an association type and session type to the allowed
+        types list. The assocation/session pairs are tried in the
+        order that they are added."""
+        if self.allowed_types is None:
+            self.allowed_types = []
+
+        if session_type is None:
+            available = getSessionTypes(assoc_type)
+
+            if not available:
+                raise ValueError('No session available for association type %r'
+                                 % (assoc_type,))
+
+            for session_type in getSessionTypes(assoc_type):
+                self.addAllowedType(assoc_type, session_type)
+        else:
+            checkSessionType(assoc_type, session_type)
+            self.allowed_types.append((assoc_type, session_type))
+
+
+    def isAllowed(self, assoc_type, session_type):
+        """Is this combination of association type and session type allowed?"""
+        assoc_good = (assoc_type, session_type) in self.allowed_types
+        matches = session_type in getSessionTypes(assoc_type)
+        return assoc_good and matches
+
+    def getAllowedType(self):
+        """Get a pair of assocation type and session type that are
+        supported"""
+        try:
+            return self.allowed_types[0]
+        except IndexError:
+            return (None, None)
+
+default_negotiator = SessionNegotiator(default_association_order)
+encrypted_negotiator = SessionNegotiator(only_encrypted_association_order)
+
+def getSecretSize(assoc_type):
+    if assoc_type == 'HMAC-SHA1':
+        return 20
+    elif assoc_type == 'HMAC-SHA256':
+        return 32
+    else:
+        raise ValueError('Unsupported association type: %r' % (assoc_type,))
+
+class Association(object):
+    """
+    This class represents an association between a server and a
+    consumer.  In general, users of this library will never see
+    instances of this object.  The only exception is if you implement
+    a custom C{L{OpenIDStore<openid.store.interface.OpenIDStore>}}.
+
+    If you do implement such a store, it will need to store the values
+    of the C{L{handle}}, C{L{secret}}, C{L{issued}}, C{L{lifetime}}, and
+    C{L{assoc_type}} instance variables.
+
+    @ivar handle: This is the handle the server gave this association.
+
+    @type handle: C{str}
+
+
+    @ivar secret: This is the shared secret the server generated for
+        this association.
+
+    @type secret: C{str}
+
+
+    @ivar issued: This is the time this association was issued, in
+        seconds since 00:00 GMT, January 1, 1970.  (ie, a unix
+        timestamp)
+
+    @type issued: C{int}
+
+
+    @ivar lifetime: This is the amount of time this association is
+        good for, measured in seconds since the association was
+        issued.
+
+    @type lifetime: C{int}
+
+
+    @ivar assoc_type: This is the type of association this instance
+        represents.  The only valid value of this field at this time
+        is C{'HMAC-SHA1'}, but new types may be defined in the future.
+
+    @type assoc_type: C{str}
+
+
+    @sort: __init__, fromExpiresIn, getExpiresIn, __eq__, __ne__,
+        handle, secret, issued, lifetime, assoc_type
+    """
+
+    # The ordering and name of keys as stored by serialize
+    assoc_keys = [
+        'version',
+        'handle',
+        'secret',
+        'issued',
+        'lifetime',
+        'assoc_type',
+        ]
+
+
+    _macs = {
+        'HMAC-SHA1': cryptutil.hmacSha1,
+        'HMAC-SHA256': cryptutil.hmacSha256,
+        }
+
+
+    def fromExpiresIn(cls, expires_in, handle, secret, assoc_type):
+        """
+        This is an alternate constructor used by the OpenID consumer
+        library to create associations.  C{L{OpenIDStore
+        <openid.store.interface.OpenIDStore>}} implementations
+        shouldn't use this constructor.
+
+
+        @param expires_in: This is the amount of time this association
+            is good for, measured in seconds since the association was
+            issued.
+
+        @type expires_in: C{int}
+
+
+        @param handle: This is the handle the server gave this
+            association.
+
+        @type handle: C{str}
+
+
+        @param secret: This is the shared secret the server generated
+            for this association.
+
+        @type secret: C{str}
+
+
+        @param assoc_type: This is the type of association this
+            instance represents.  The only valid value of this field
+            at this time is C{'HMAC-SHA1'}, but new types may be
+            defined in the future.
+
+        @type assoc_type: C{str}
+        """
+        issued = int(time.time())
+        lifetime = expires_in
+        return cls(handle, secret, issued, lifetime, assoc_type)
+
+    fromExpiresIn = classmethod(fromExpiresIn)
+
+    def __init__(self, handle, secret, issued, lifetime, assoc_type):
+        """
+        This is the standard constructor for creating an association.
+
+
+        @param handle: This is the handle the server gave this
+            association.
+
+        @type handle: C{str}
+
+
+        @param secret: This is the shared secret the server generated
+            for this association.
+
+        @type secret: C{str}
+
+
+        @param issued: This is the time this association was issued,
+            in seconds since 00:00 GMT, January 1, 1970.  (ie, a unix
+            timestamp)
+
+        @type issued: C{int}
+
+
+        @param lifetime: This is the amount of time this association
+            is good for, measured in seconds since the association was
+            issued.
+
+        @type lifetime: C{int}
+
+
+        @param assoc_type: This is the type of association this
+            instance represents.  The only valid value of this field
+            at this time is C{'HMAC-SHA1'}, but new types may be
+            defined in the future.
+
+        @type assoc_type: C{str}
+        """
+        if assoc_type not in all_association_types:
+            fmt = '%r is not a supported association type'
+            raise ValueError(fmt % (assoc_type,))
+
+#         secret_size = getSecretSize(assoc_type)
+#         if len(secret) != secret_size:
+#             fmt = 'Wrong size secret (%s bytes) for association type %s'
+#             raise ValueError(fmt % (len(secret), assoc_type))
+
+        self.handle = handle
+        self.secret = secret
+        self.issued = issued
+        self.lifetime = lifetime
+        self.assoc_type = assoc_type
+
+    def getExpiresIn(self, now=None):
+        """
+        This returns the number of seconds this association is still
+        valid for, or C{0} if the association is no longer valid.
+
+
+        @return: The number of seconds this association is still valid
+            for, or C{0} if the association is no longer valid.
+
+        @rtype: C{int}
+        """
+        if now is None:
+            now = int(time.time())
+
+        return max(0, self.issued + self.lifetime - now)
+
+    expiresIn = property(getExpiresIn)
+
+    def __eq__(self, other):
+        """
+        This checks to see if two C{L{Association}} instances
+        represent the same association.
+
+
+        @return: C{True} if the two instances represent the same
+            association, C{False} otherwise.
+
+        @rtype: C{bool}
+        """
+        return type(self) is type(other) and self.__dict__ == other.__dict__
+
+    def __ne__(self, other):
+        """
+        This checks to see if two C{L{Association}} instances
+        represent different associations.
+
+
+        @return: C{True} if the two instances represent different
+            associations, C{False} otherwise.
+
+        @rtype: C{bool}
+        """
+        return not (self == other)
+
+    def serialize(self):
+        """
+        Convert an association to KV form.
+
+        @return: String in KV form suitable for deserialization by
+            deserialize.
+
+        @rtype: str
+        """
+        data = {
+            'version':'2',
+            'handle':self.handle,
+            'secret':oidutil.toBase64(self.secret),
+            'issued':str(int(self.issued)),
+            'lifetime':str(int(self.lifetime)),
+            'assoc_type':self.assoc_type
+            }
+
+        assert len(data) == len(self.assoc_keys)
+        pairs = []
+        for field_name in self.assoc_keys:
+            pairs.append((field_name, data[field_name]))
+
+        return kvform.seqToKV(pairs, strict=True)
+
+    def deserialize(cls, assoc_s):
+        """
+        Parse an association as stored by serialize().
+
+        inverse of serialize
+
+
+        @param assoc_s: Association as serialized by serialize()
+
+        @type assoc_s: str
+
+
+        @return: instance of this class
+        """
+        pairs = kvform.kvToSeq(assoc_s, strict=True)
+        keys = []
+        values = []
+        for k, v in pairs:
+            keys.append(k)
+            values.append(v)
+
+        if keys != cls.assoc_keys:
+            raise ValueError('Unexpected key values: %r', keys)
+
+        version, handle, secret, issued, lifetime, assoc_type = values
+        if version != '2':
+            raise ValueError('Unknown version: %r' % version)
+        issued = int(issued)
+        lifetime = int(lifetime)
+        secret = oidutil.fromBase64(secret)
+        return cls(handle, secret, issued, lifetime, assoc_type)
+
+    deserialize = classmethod(deserialize)
+
+    def sign(self, pairs):
+        """
+        Generate a signature for a sequence of (key, value) pairs
+
+
+        @param pairs: The pairs to sign, in order
+
+        @type pairs: sequence of (str, str)
+
+
+        @return: The binary signature of this sequence of pairs
+
+        @rtype: str
+        """
+        kv = kvform.seqToKV(pairs)
+
+        try:
+            mac = self._macs[self.assoc_type]
+        except KeyError:
+            raise ValueError(
+                'Unknown association type: %r' % (self.assoc_type,))
+
+        return mac(self.secret, kv)
+
+
+    def getMessageSignature(self, message):
+        """Return the signature of a message.
+
+        If I am not a sign-all association, the message must have a
+        signed list.
+
+        @return: the signature, base64 encoded
+
+        @rtype: str
+
+        @raises ValueError: If there is no signed list and I am not a sign-all
+            type of association.
+        """
+        pairs = self._makePairs(message)
+        return oidutil.toBase64(self.sign(pairs))
+
+    def signMessage(self, message):
+        """Add a signature (and a signed list) to a message.
+
+        @return: a new Message object with a signature
+        @rtype: L{openid.message.Message}
+        """
+        if (message.hasKey(OPENID_NS, 'sig') or
+            message.hasKey(OPENID_NS, 'signed')):
+            raise ValueError('Message already has signed list or signature')
+
+        extant_handle = message.getArg(OPENID_NS, 'assoc_handle')
+        if extant_handle and extant_handle != self.handle:
+            raise ValueError("Message has a different association handle")
+
+        signed_message = message.copy()
+        signed_message.setArg(OPENID_NS, 'assoc_handle', self.handle)
+        message_keys = signed_message.toPostArgs().keys()
+        signed_list = [k[7:] for k in message_keys
+                       if k.startswith('openid.')]
+        signed_list.append('signed')
+        signed_list.sort()
+        signed_message.setArg(OPENID_NS, 'signed', ','.join(signed_list))
+        sig = self.getMessageSignature(signed_message)
+        signed_message.setArg(OPENID_NS, 'sig', sig)
+        return signed_message
+
+    def checkMessageSignature(self, message):
+        """Given a message with a signature, calculate a new signature
+        and return whether it matches the signature in the message.
+
+        @raises ValueError: if the message has no signature or no signature
+            can be calculated for it.
+        """        
+        message_sig = message.getArg(OPENID_NS, 'sig')
+        if not message_sig:
+            raise ValueError("%s has no sig." % (message,))
+        calculated_sig = self.getMessageSignature(message)
+        return calculated_sig == message_sig
+
+
+    def _makePairs(self, message):
+        signed = message.getArg(OPENID_NS, 'signed')
+        if not signed:
+            raise ValueError('Message has no signed list: %s' % (message,))
+
+        signed_list = signed.split(',')
+        pairs = []
+        data = message.toPostArgs()
+        for field in signed_list:
+            pairs.append((field, data.get('openid.' + field, '')))
+        return pairs
+
+    def __repr__(self):
+        return "<%s.%s %s %s>" % (
+            self.__class__.__module__,
+            self.__class__.__name__,
+            self.assoc_type,
+            self.handle)

openid/consumer/__init__.py

+"""
+This package contains the portions of the library used only when
+implementing an OpenID consumer.
+"""
+
+__all__ = ['consumer', 'discover']

openid/consumer/consumer.py

+# -*- test-case-name: openid.test.test_consumer -*-
+"""OpenID support for Relying Parties (aka Consumers).
+
+This module documents the main interface with the OpenID consumer
+library.  The only part of the library which has to be used and isn't
+documented in full here is the store required to create an
+C{L{Consumer}} instance.  More on the abstract store type and
+concrete implementations of it that are provided in the documentation
+for the C{L{__init__<Consumer.__init__>}} method of the
+C{L{Consumer}} class.
+
+
+OVERVIEW
+========
+
+    The OpenID identity verification process most commonly uses the
+    following steps, as visible to the user of this library:
+
+        1. The user enters their OpenID into a field on the consumer's
+           site, and hits a login button.
+
+        2. The consumer site discovers the user's OpenID provider using
+           the Yadis protocol.
+
+        3. The consumer site sends the browser a redirect to the
+           OpenID provider.  This is the authentication request as
+           described in the OpenID specification.
+
+        4. The OpenID provider's site sends the browser a redirect
+           back to the consumer site.  This redirect contains the
+           provider's response to the authentication request.
+
+    The most important part of the flow to note is the consumer's site
+    must handle two separate HTTP requests in order to perform the
+    full identity check.
+
+
+LIBRARY DESIGN
+==============
+
+    This consumer library is designed with that flow in mind.  The
+    goal is to make it as easy as possible to perform the above steps
+    securely.
+
+    At a high level, there are two important parts in the consumer
+    library.  The first important part is this module, which contains
+    the interface to actually use this library.  The second is the
+    C{L{openid.store.interface}} module, which describes the
+    interface to use if you need to create a custom method for storing
+    the state this library needs to maintain between requests.
+
+    In general, the second part is less important for users of the
+    library to know about, as several implementations are provided
+    which cover a wide variety of situations in which consumers may
+    use the library.
+
+    This module contains a class, C{L{Consumer}}, with methods
+    corresponding to the actions necessary in each of steps 2, 3, and
+    4 described in the overview.  Use of this library should be as easy
+    as creating an C{L{Consumer}} instance and calling the methods
+    appropriate for the action the site wants to take.
+
+
+SESSIONS, STORES, AND STATELESS MODE
+====================================
+
+    The C{L{Consumer}} object keeps track of two types of state:
+
+        1. State of the user's current authentication attempt.  Things like
+           the identity URL, the list of endpoints discovered for that
+           URL, and in case where some endpoints are unreachable, the list
+           of endpoints already tried.  This state needs to be held from
+           Consumer.begin() to Consumer.complete(), but it is only applicable
+           to a single session with a single user agent, and at the end of
+           the authentication process (i.e. when an OP replies with either
+           C{id_res} or C{cancel}) it may be discarded.
+
+        2. State of relationships with servers, i.e. shared secrets
+           (associations) with servers and nonces seen on signed messages.
+           This information should persist from one session to the next and
+           should not be bound to a particular user-agent.
+
+
+    These two types of storage are reflected in the first two arguments of
+    Consumer's constructor, C{session} and C{store}.  C{session} is a
+    dict-like object and we hope your web framework provides you with one
+    of these bound to the user agent.  C{store} is an instance of
+    L{openid.store.interface.OpenIDStore}.
+
+    Since the store does hold secrets shared between your application and the
+    OpenID provider, you should be careful about how you use it in a shared
+    hosting environment.  If the filesystem or database permissions of your
+    web host allow strangers to read from them, do not store your data there!
+    If you have no safe place to store your data, construct your consumer
+    with C{None} for the store, and it will operate only in stateless mode.
+    Stateless mode may be slower, put more load on the OpenID provider, and
+    trusts the provider to keep you safe from replay attacks.
+
+
+    Several store implementation are provided, and the interface is
+    fully documented so that custom stores can be used as well.  See
+    the documentation for the C{L{Consumer}} class for more
+    information on the interface for stores.  The implementations that
+    are provided allow the consumer site to store the necessary data
+    in several different ways, including several SQL databases and
+    normal files on disk.
+
+
+IMMEDIATE MODE
+==============
+
+    In the flow described above, the user may need to confirm to the
+    OpenID provider that it's ok to disclose his or her identity.
+    The provider may draw pages asking for information from the user
+    before it redirects the browser back to the consumer's site.  This
+    is generally transparent to the consumer site, so it is typically
+    ignored as an implementation detail.
+
+    There can be times, however, where the consumer site wants to get
+    a response immediately.  When this is the case, the consumer can
+    put the library in immediate mode.  In immediate mode, there is an
+    extra response possible from the server, which is essentially the
+    server reporting that it doesn't have enough information to answer
+    the question yet.
+
+
+USING THIS LIBRARY
+==================
+
+    Integrating this library into an application is usually a
+    relatively straightforward process.  The process should basically
+    follow this plan:
+
+    Add an OpenID login field somewhere on your site.  When an OpenID
+    is entered in that field and the form is submitted, it should make
+    a request to the your site which includes that OpenID URL.
+
+    First, the application should L{instantiate a Consumer<Consumer.__init__>}
+    with a session for per-user state and store for shared state.
+    using the store of choice.
+
+    Next, the application should call the 'C{L{begin<Consumer.begin>}}' method on the
+    C{L{Consumer}} instance.  This method takes the OpenID URL.  The
+    C{L{begin<Consumer.begin>}} method returns an C{L{AuthRequest}}
+    object.
+
+    Next, the application should call the
+    C{L{redirectURL<AuthRequest.redirectURL>}} method on the
+    C{L{AuthRequest}} object.  The parameter C{return_to} is the URL
+    that the OpenID server will send the user back to after attempting
+    to verify his or her identity.  The C{realm} parameter is the
+    URL (or URL pattern) that identifies your web site to the user
+    when he or she is authorizing it.  Send a redirect to the
+    resulting URL to the user's browser.
+
+    That's the first half of the authentication process.  The second
+    half of the process is done after the user's OpenID Provider sends the
+    user's browser a redirect back to your site to complete their
+    login.
+
+    When that happens, the user will contact your site at the URL
+    given as the C{return_to} URL to the
+    C{L{redirectURL<AuthRequest.redirectURL>}} call made
+    above.  The request will have several query parameters added to
+    the URL by the OpenID provider as the information necessary to
+    finish the request.
+
+    Get an C{L{Consumer}} instance with the same session and store as
+    before and call its C{L{complete<Consumer.complete>}} method,
+    passing in all the received query arguments.
+
+    There are multiple possible return types possible from that
+    method. These indicate the whether or not the login was
+    successful, and include any additional information appropriate for
+    their type.
+
+@var SUCCESS: constant used as the status for
+    L{SuccessResponse<openid.consumer.consumer.SuccessResponse>} objects.
+
+@var FAILURE: constant used as the status for
+    L{FailureResponse<openid.consumer.consumer.FailureResponse>} objects.
+
+@var CANCEL: constant used as the status for
+    L{CancelResponse<openid.consumer.consumer.CancelResponse>} objects.
+
+@var SETUP_NEEDED: constant used as the status for
+    L{SetupNeededResponse<openid.consumer.consumer.SetupNeededResponse>}
+    objects.
+"""
+
+import cgi
+import copy
+from urlparse import urlparse, urldefrag
+
+from openid import fetchers
+
+from openid.consumer.discover import discover, OpenIDServiceEndpoint, \
+     DiscoveryFailure, OPENID_1_0_TYPE, OPENID_1_1_TYPE, OPENID_2_0_TYPE
+from openid.message import Message, OPENID_NS, OPENID2_NS, OPENID1_NS, \
+     IDENTIFIER_SELECT, no_default, BARE_NS
+from openid import cryptutil
+from openid import oidutil
+from openid.association import Association, default_negotiator, \
+     SessionNegotiator
+from openid.dh import DiffieHellman
+from openid.store.nonce import mkNonce, split as splitNonce
+from openid.yadis.manager import Discovery
+from openid import urinorm
+
+
+__all__ = ['AuthRequest', 'Consumer', 'SuccessResponse',
+           'SetupNeededResponse', 'CancelResponse', 'FailureResponse',
+           'SUCCESS', 'FAILURE', 'CANCEL', 'SETUP_NEEDED',
+           ]
+
+
+def makeKVPost(request_message, server_url):
+    """Make a Direct Request to an OpenID Provider and return the
+    result as a Message object.
+
+    @raises openid.fetchers.HTTPFetchingError: if an error is
+        encountered in making the HTTP post.
+
+    @rtype: L{openid.message.Message}
+    """
+    # XXX: TESTME
+    resp = fetchers.fetch(server_url, body=request_message.toURLEncoded())
+
+    # Process response in separate function that can be shared by async code.
+    return _httpResponseToMessage(resp, server_url)
+
+
+def _httpResponseToMessage(response, server_url):
+    """Adapt a POST response to a Message.
+
+    @type response: L{openid.fetchers.HTTPResponse}
+    @param response: Result of a POST to an OpenID endpoint.
+
+    @rtype: L{openid.message.Message}
+
+    @raises openid.fetchers.HTTPFetchingError: if the server returned a
+        status of other than 200 or 400.
+
+    @raises ServerError: if the server returned an OpenID error.
+    """
+    # Should this function be named Message.fromHTTPResponse instead?
+    response_message = Message.fromKVForm(response.body)
+    if response.status == 400:
+        raise ServerError.fromMessage(response_message)
+
+    elif response.status not in (200, 206):
+        fmt = 'bad status code from server %s: %s'
+        error_message = fmt % (server_url, response.status)
+        raise fetchers.HTTPFetchingError(error_message)
+
+    return response_message
+
+
+
+class Consumer(object):
+    """An OpenID consumer implementation that performs discovery and
+    does session management.
+
+    @ivar consumer: an instance of an object implementing the OpenID
+        protocol, but doing no discovery or session management.
+
+    @type consumer: GenericConsumer
+
+    @ivar session: A dictionary-like object representing the user's
+        session data.  This is used for keeping state of the OpenID
+        transaction when the user is redirected to the server.
+
+    @cvar session_key_prefix: A string that is prepended to session
+        keys to ensure that they are unique. This variable may be
+        changed to suit your application.
+    """
+    session_key_prefix = "_openid_consumer_"
+
+    _token = 'last_token'
+
+    _discover = staticmethod(discover)
+
+    def __init__(self, session, store, consumer_class=None):
+        """Initialize a Consumer instance.
+
+        You should create a new instance of the Consumer object with
+        every HTTP request that handles OpenID transactions.
+
+        @param session: See L{the session instance variable<openid.consumer.consumer.Consumer.session>}
+
+        @param store: an object that implements the interface in
+            C{L{openid.store.interface.OpenIDStore}}.  Several
+            implementations are provided, to cover common database
+            environments.
+
+        @type store: C{L{openid.store.interface.OpenIDStore}}
+
+        @see: L{openid.store.interface}
+        @see: L{openid.store}
+        """
+        self.session = session
+        if consumer_class is None:
+            consumer_class = GenericConsumer
+        self.consumer = consumer_class(store)
+        self._token_key = self.session_key_prefix + self._token
+
+    def begin(self, user_url, anonymous=False):
+        """Start the OpenID authentication process. See steps 1-2 in
+        the overview at the top of this file.
+
+        @param user_url: Identity URL given by the user. This method
+            performs a textual transformation of the URL to try and
+            make sure it is normalized. For example, a user_url of
+            example.com will be normalized to http://example.com/
+            normalizing and resolving any redirects the server might
+            issue.
+
+        @type user_url: unicode
+
+        @param anonymous: Whether to make an anonymous request of the OpenID
+            provider.  Such a request does not ask for an authorization
+            assertion for an OpenID identifier, but may be used with
+            extensions to pass other data.  e.g. "I don't care who you are,
+            but I'd like to know your time zone."
+
+        @type anonymous: bool
+
+        @returns: An object containing the discovered information will
+            be returned, with a method for building a redirect URL to
+            the server, as described in step 3 of the overview. This
+            object may also be used to add extension arguments to the
+            request, using its
+            L{addExtensionArg<openid.consumer.consumer.AuthRequest.addExtensionArg>}
+            method.
+
+        @returntype: L{AuthRequest<openid.consumer.consumer.AuthRequest>}
+
+        @raises openid.consumer.discover.DiscoveryFailure: when I fail to
+            find an OpenID server for this URL.  If the C{yadis} package
+            is available, L{openid.consumer.discover.DiscoveryFailure} is
+            an alias for C{yadis.discover.DiscoveryFailure}.
+        """
+        disco = Discovery(self.session, user_url, self.session_key_prefix)
+        try:
+            service = disco.getNextService(self._discover)
+        except fetchers.HTTPFetchingError, why:
+            raise DiscoveryFailure(
+                'Error fetching XRDS document: %s' % (why[0],), None)
+
+        if service is None:
+            raise DiscoveryFailure(
+                'No usable OpenID services found for %s' % (user_url,), None)
+        else:
+            return self.beginWithoutDiscovery(service, anonymous)
+
+    def beginWithoutDiscovery(self, service, anonymous=False):
+        """Start OpenID verification without doing OpenID server
+        discovery. This method is used internally by Consumer.begin
+        after discovery is performed, and exists to provide an
+        interface for library users needing to perform their own
+        discovery.
+
+        @param service: an OpenID service endpoint descriptor.  This
+            object and factories for it are found in the
+            L{openid.consumer.discover} module.
+
+        @type service:
+            L{OpenIDServiceEndpoint<openid.consumer.discover.OpenIDServiceEndpoint>}
+
+        @returns: an OpenID authentication request object.
+
+        @rtype: L{AuthRequest<openid.consumer.consumer.AuthRequest>}
+
+        @See: Openid.consumer.consumer.Consumer.begin
+        @see: openid.consumer.discover
+        """
+        auth_req = self.consumer.begin(service)
+        self.session[self._token_key] = auth_req.endpoint
+
+        try:
+            auth_req.setAnonymous(anonymous)
+        except ValueError, why:
+            raise ProtocolError(str(why))
+
+        return auth_req
+
+    def complete(self, query, current_url):
+        """Called to interpret the server's response to an OpenID
+        request. It is called in step 4 of the flow described in the
+        consumer overview.
+
+        @param query: A dictionary of the query parameters for this
+            HTTP request.
+
+        @param current_url: The URL used to invoke the application.
+            Extract the URL from your application's web
+            request framework and specify it here to have it checked
+            against the openid.return_to value in the response.  If
+            the return_to URL check fails, the status of the
+            completion will be FAILURE.
+
+        @returns: a subclass of Response. The type of response is
+            indicated by the status attribute, which will be one of
+            SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED.
+
+        @see: L{SuccessResponse<openid.consumer.consumer.SuccessResponse>}
+        @see: L{CancelResponse<openid.consumer.consumer.CancelResponse>}
+        @see: L{SetupNeededResponse<openid.consumer.consumer.SetupNeededResponse>}
+        @see: L{FailureResponse<openid.consumer.consumer.FailureResponse>}
+        """
+
+        endpoint = self.session.get(self._token_key)
+
+        message = Message.fromPostArgs(query)
+        response = self.consumer.complete(message, endpoint, current_url)
+
+        try:
+            del self.session[self._token_key]
+        except KeyError:
+            pass
+
+        if (response.status in ['success', 'cancel'] and
+            response.identity_url is not None):
+
+            disco = Discovery(self.session,
+                              response.identity_url,
+                              self.session_key_prefix)
+            # This is OK to do even if we did not do discovery in
+            # the first place.
+            disco.cleanup(force=True)
+
+        return response
+
+    def setAssociationPreference(self, association_preferences):
+        """Set the order in which association types/sessions should be
+        attempted. For instance, to only allow HMAC-SHA256
+        associations created with a DH-SHA256 association session:
+
+        >>> consumer.setAssociationPreference([('HMAC-SHA256', 'DH-SHA256')])
+
+        Any association type/association type pair that is not in this
+        list will not be attempted at all.
+
+        @param association_preferences: The list of allowed
+            (association type, association session type) pairs that
+            should be allowed for this consumer to use, in order from
+            most preferred to least preferred.
+        @type association_preferences: [(str, str)]
+
+        @returns: None
+
+        @see: C{L{openid.association.SessionNegotiator}}
+        """
+        self.consumer.negotiator = SessionNegotiator(association_preferences)
+
+class DiffieHellmanSHA1ConsumerSession(object):
+    session_type = 'DH-SHA1'
+    hash_func = staticmethod(cryptutil.sha1)
+    secret_size = 20
+    allowed_assoc_types = ['HMAC-SHA1']
+
+    def __init__(self, dh=None):
+        if dh is None:
+            dh = DiffieHellman.fromDefaults()
+
+        self.dh = dh
+
+    def getRequest(self):
+        cpub = cryptutil.longToBase64(self.dh.public)
+
+        args = {'dh_consumer_public': cpub}
+
+        if not self.dh.usingDefaultValues():
+            args.update({
+                'dh_modulus': cryptutil.longToBase64(self.dh.modulus),
+                'dh_gen': cryptutil.longToBase64(self.dh.generator),
+                })
+
+        return args
+
+    def extractSecret(self, response):
+        dh_server_public64 = response.getArg(
+            OPENID_NS, 'dh_server_public', no_default)
+        enc_mac_key64 = response.getArg(OPENID_NS, 'enc_mac_key', no_default)
+        dh_server_public = cryptutil.base64ToLong(dh_server_public64)
+        enc_mac_key = oidutil.fromBase64(enc_mac_key64)
+        return self.dh.xorSecret(dh_server_public, enc_mac_key, self.hash_func)
+
+class DiffieHellmanSHA256ConsumerSession(DiffieHellmanSHA1ConsumerSession):
+    session_type = 'DH-SHA256'
+    hash_func = staticmethod(cryptutil.sha256)
+    secret_size = 32
+    allowed_assoc_types = ['HMAC-SHA256']
+
+class PlainTextConsumerSession(object):
+    session_type = 'no-encryption'
+    allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']
+
+    def getRequest(self):
+        return {}
+
+    def extractSecret(self, response):
+        mac_key64 = response.getArg(OPENID_NS, 'mac_key', no_default)
+        return oidutil.fromBase64(mac_key64)
+
+class SetupNeededError(Exception):
+    """Internally-used exception that indicates that an immediate-mode
+    request cancelled."""
+    def __init__(self, user_setup_url=None):
+        Exception.__init__(self, user_setup_url)
+        self.user_setup_url = user_setup_url
+
+class ProtocolError(ValueError):
+    """Exception that indicates that a message violated the
+    protocol. It is raised and caught internally to this file."""
+
+class TypeURIMismatch(ProtocolError):
+    """A protocol error arising from type URIs mismatching
+    """
+
+    def __init__(self, expected, endpoint):
+        ProtocolError.__init__(self, expected, endpoint)
+        self.expected = expected
+        self.endpoint = endpoint
+
+    def __str__(self):
+        s = '<%s.%s: Required type %s not found in %s for endpoint %s>' % (
+            self.__class__.__module__, self.__class__.__name__,
+            self.expected, self.endpoint.type_uris, self.endpoint)
+        return s
+
+
+
+class ServerError(Exception):
+    """Exception that is raised when the server returns a 400 response
+    code to a direct request."""
+
+    def __init__(self, error_text, error_code, message):
+        Exception.__init__(self, error_text)
+        self.error_text = error_text
+        self.error_code = error_code
+        self.message = message
+
+    def fromMessage(cls, message):
+        """Generate a ServerError instance, extracting the error text
+        and the error code from the message."""
+        error_text = message.getArg(
+            OPENID_NS, 'error', '<no error message supplied>')
+        error_code = message.getArg(OPENID_NS, 'error_code')
+        return cls(error_text, error_code, message)
+
+    fromMessage = classmethod(fromMessage)
+
+class GenericConsumer(object):
+    """This is the implementation of the common logic for OpenID
+    consumers. It is unaware of the application in which it is
+    running.
+
+    @ivar negotiator: An object that controls the kind of associations
+        that the consumer makes. It defaults to
+        C{L{openid.association.default_negotiator}}. Assign a
+        different negotiator to it if you have specific requirements
+        for how associations are made.
+    @type negotiator: C{L{openid.association.SessionNegotiator}}
+    """
+
+    # The name of the query parameter that gets added to the return_to
+    # URL when using OpenID1. You can change this value if you want or
+    # need a different name, but don't make it start with openid,
+    # because it's not a standard protocol thing for OpenID1. For
+    # OpenID2, the library will take care of the nonce using standard
+    # OpenID query parameter names.
+    openid1_nonce_query_arg_name = 'janrain_nonce'
+
+    # Another query parameter that gets added to the return_to for
+    # OpenID 1; if the user's session state is lost, use this claimed
+    # identifier to do discovery when verifying the response.
+    openid1_return_to_identifier_name = 'openid1_claimed_id'
+
+    session_types = {
+        'DH-SHA1':DiffieHellmanSHA1ConsumerSession,
+        'DH-SHA256':DiffieHellmanSHA256ConsumerSession,
+        'no-encryption':PlainTextConsumerSession,
+        }
+
+    _discover = staticmethod(discover)
+
+    def __init__(self, store):
+        self.store = store
+        self.negotiator = default_negotiator.copy()
+
+    def begin(self, service_endpoint):
+        """Create an AuthRequest object for the specified
+        service_endpoint. This method will create an association if
+        necessary."""
+        if self.store is None:
+            assoc = None
+        else:
+            assoc = self._getAssociation(service_endpoint)
+
+        request = AuthRequest(service_endpoint, assoc)
+        request.return_to_args[self.openid1_nonce_query_arg_name] = mkNonce()
+
+        if request.message.isOpenID1():
+            request.return_to_args[self.openid1_return_to_identifier_name] = \
+                request.endpoint.claimed_id
+
+        return request
+
+    def complete(self, message, endpoint, return_to):
+        """Process the OpenID message, using the specified endpoint
+        and return_to URL as context. This method will handle any
+        OpenID message that is sent to the return_to URL.
+        """
+        mode = message.getArg(OPENID_NS, 'mode', '<No mode set>')
+
+        modeMethod = getattr(self, '_complete_' + mode,
+                             self._completeInvalid)
+
+        return modeMethod(message, endpoint, return_to)
+
+    def _complete_cancel(self, message, endpoint, _):
+        return CancelResponse(endpoint)
+
+    def _complete_error(self, message, endpoint, _):
+        error = message.getArg(OPENID_NS, 'error')
+        contact = message.getArg(OPENID_NS, 'contact')
+        reference = message.getArg(OPENID_NS, 'reference')
+
+        return FailureResponse(endpoint, error, contact=contact,
+                               reference=reference)
+
+    def _complete_setup_needed(self, message, endpoint, _):
+        if not message.isOpenID2():
+            return self._completeInvalid(message, endpoint, _)
+
+        user_setup_url = message.getArg(OPENID2_NS, 'user_setup_url')
+        return SetupNeededResponse(endpoint, user_setup_url)
+
+    def _complete_id_res(self, message, endpoint, return_to):
+        try:
+            self._checkSetupNeeded(message)
+        except SetupNeededError, why:
+            return SetupNeededResponse(endpoint, why.user_setup_url)
+        else:
+            try:
+                return self._doIdRes(message, endpoint, return_to)
+            except (ProtocolError, DiscoveryFailure), why:
+                return FailureResponse(endpoint, why[0])
+
+    def _completeInvalid(self, message, endpoint, _):
+        mode = message.getArg(OPENID_NS, 'mode', '<No mode set>')
+        return FailureResponse(endpoint,
+                               'Invalid openid.mode: %r' % (mode,))
+
+    def _checkReturnTo(self, message, return_to):
+        """Check an OpenID message and its openid.return_to value
+        against a return_to URL from an application.  Return True on
+        success, False on failure.
+        """
+        # Check the openid.return_to args against args in the original
+        # message.
+        try:
+            self._verifyReturnToArgs(message.toPostArgs())
+        except ProtocolError, why:
+            oidutil.log("Verifying return_to arguments: %s" % (why[0],))
+            return False
+
+        # Check the return_to base URL against the one in the message.
+        msg_return_to = message.getArg(OPENID_NS, 'return_to')
+
+        # The URL scheme, authority, and path MUST be the same between
+        # the two URLs.
+        app_parts = urlparse(urinorm.urinorm(return_to))
+        msg_parts = urlparse(urinorm.urinorm(msg_return_to))
+
+        # (addressing scheme, network location, path) must be equal in
+        # both URLs.
+        for part in range(0, 3):
+            if app_parts[part] != msg_parts[part]:
+                return False
+
+        return True
+
+    _makeKVPost = staticmethod(makeKVPost)
+
+    def _checkSetupNeeded(self, message):
+        """Check an id_res message to see if it is a
+        checkid_immediate cancel response.
+
+        @raises SetupNeededError: if it is a checkid_immediate cancellation
+        """
+        # In OpenID 1, we check to see if this is a cancel from
+        # immediate mode by the presence of the user_setup_url
+        # parameter.
+        if message.isOpenID1():
+            user_setup_url = message.getArg(OPENID1_NS, 'user_setup_url')
+            if user_setup_url is not None:
+                raise SetupNeededError(user_setup_url)
+
+    def _doIdRes(self, message, endpoint, return_to):
+        """Handle id_res responses that are not cancellations of
+        immediate mode requests.
+
+        @param message: the response paramaters.
+        @param endpoint: the discovered endpoint object. May be None.
+
+        @raises ProtocolError: If the message contents are not
+            well-formed according to the OpenID specification. This
+            includes missing fields or not signing fields that should
+            be signed.
+
+        @raises DiscoveryFailure: If the subject of the id_res message
+            does not match the supplied endpoint, and discovery on the
+            identifier in the message fails (this should only happen
+            when using OpenID 2)
+
+        @returntype: L{Response}
+        """
+        # Checks for presence of appropriate fields (and checks
+        # signed list fields)
+        self._idResCheckForFields(message)
+
+        if not self._checkReturnTo(message, return_to):
+            raise ProtocolError(
+                "return_to does not match return URL. Expected %r, got %r"
+                % (return_to, message.getArg(OPENID_NS, 'return_to')))
+
+
+        # Verify discovery information:
+        endpoint = self._verifyDiscoveryResults(message, endpoint)
+        oidutil.log("Received id_res response from %s using association %s" %
+                    (endpoint.server_url,
+                     message.getArg(OPENID_NS, 'assoc_handle')))
+
+        self._idResCheckSignature(message, endpoint.server_url)
+
+        # Will raise a ProtocolError if the nonce is bad
+        self._idResCheckNonce(message, endpoint)
+
+        signed_list_str = message.getArg(OPENID_NS, 'signed', no_default)
+        signed_list = signed_list_str.split(',')
+        signed_fields = ["openid." + s for s in signed_list]
+        return SuccessResponse(endpoint, message, signed_fields)
+
+    def _idResGetNonceOpenID1(self, message, endpoint):
+        """Extract the nonce from an OpenID 1 response.  Return the
+        nonce from the BARE_NS since we independently check the
+        return_to arguments are the same as those in the response
+        message.
+
+        See the openid1_nonce_query_arg_name class variable
+
+        @returns: The nonce as a string or None
+        """
+        return message.getArg(BARE_NS, self.openid1_nonce_query_arg_name)
+
+    def _idResCheckNonce(self, message, endpoint):
+        if message.isOpenID1():
+            # This indicates that the nonce was generated by the consumer
+            nonce = self._idResGetNonceOpenID1(message, endpoint)
+            server_url = ''
+        else:
+            nonce = message.getArg(OPENID2_NS, 'response_nonce')
+            server_url = endpoint.server_url
+
+        if nonce is None:
+            raise ProtocolError('Nonce missing from response')
+
+        try:
+            timestamp, salt = splitNonce(nonce)
+        except ValueError, why:
+            raise ProtocolError('Malformed nonce: %s' % (why[0],))
+
+        if (self.store is not None and
+            not self.store.useNonce(server_url, timestamp, salt)):
+            raise ProtocolError('Nonce already used or out of range')
+
+    def _idResCheckSignature(self, message, server_url):
+        assoc_handle = message.getArg(OPENID_NS, 'assoc_handle')
+        if self.store is None:
+            assoc = None
+        else:
+            assoc = self.store.getAssociation(server_url, assoc_handle)
+
+        if assoc:
+            if assoc.getExpiresIn() <= 0:
+                # XXX: It might be a good idea sometimes to re-start the
+                # authentication with a new association. Doing it
+                # automatically opens the possibility for
+                # denial-of-service by a server that just returns expired
+                # associations (or really short-lived associations)
+                raise ProtocolError(
+                    'Association with %s expired' % (server_url,))
+
+            if not assoc.checkMessageSignature(message):
+                raise ProtocolError('Bad signature')
+
+        else:
+            # It's not an association we know about.  Stateless mode is our
+            # only possible path for recovery.
+            # XXX - async framework will not want to block on this call to
+            # _checkAuth.
+            if not self._checkAuth(message, server_url):
+                raise ProtocolError('Server denied check_authentication')
+
+    def _idResCheckForFields(self, message):
+        # XXX: this should be handled by the code that processes the
+        # response (that is, if a field is missing, we should not have
+        # to explicitly check that it's present, just make sure that
+        # the fields are actually being used by the rest of the code
+        # in tests). Although, which fields are signed does need to be
+        # checked somewhere.
+        basic_fields = ['return_to', 'assoc_handle', 'sig', 'signed']
+        basic_sig_fields = ['return_to', 'identity']
+
+        require_fields = {
+            OPENID2_NS: basic_fields + ['op_endpoint'],
+            OPENID1_NS: basic_fields + ['identity'],
+            }
+
+        require_sigs = {
+            OPENID2_NS: basic_sig_fields + ['response_nonce',
+                                            'claimed_id',
+                                            'assoc_handle',
+                                            'op_endpoint',],
+            OPENID1_NS: basic_sig_fields,
+            }
+
+        for field in require_fields[message.getOpenIDNamespace()]:
+            if not message.hasKey(OPENID_NS, field):
+                raise ProtocolError('Missing required field %r' % (field,))
+
+        signed_list_str = message.getArg(OPENID_NS, 'signed', no_default)
+        signed_list = signed_list_str.split(',')
+
+        for field in require_sigs[message.getOpenIDNamespace()]:
+            # Field is present and not in signed list
+            if message.hasKey(OPENID_NS, field) and field not in signed_list:
+                raise ProtocolError('"%s" not signed' % (field,))
+
+
+    def _verifyReturnToArgs(query):
+        """Verify that the arguments in the return_to URL are present in this
+        response.
+        """
+        message = Message.fromPostArgs(query)
+        return_to = message.getArg(OPENID_NS, 'return_to')
+
+        if return_to is None:
+            raise ProtocolError('Response has no return_to')
+
+        parsed_url = urlparse(return_to)
+        rt_query = parsed_url[4]
+        parsed_args = cgi.parse_qsl(rt_query)
+
+        for rt_key, rt_value in parsed_args:
+            try:
+                value = query[rt_key]
+                if rt_value != value:
+                    format = ("parameter %s value %r does not match "
+                              "return_to's value %r")
+                    raise ProtocolError(format % (rt_key, value, rt_value))
+            except KeyError:
+                format = "return_to parameter %s absent from query %r"
+                raise ProtocolError(format % (rt_key, query))
+
+        # Make sure all non-OpenID arguments in the response are also
+        # in the signed return_to.
+        bare_args = message.getArgs(BARE_NS)
+        for pair in bare_args.iteritems():
+            if pair not in parsed_args:
+                raise ProtocolError("Parameter %s not in return_to URL" % (pair[0],))
+
+    _verifyReturnToArgs = staticmethod(_verifyReturnToArgs)
+
+    def _verifyDiscoveryResults(self, resp_msg, endpoint=None):
+        """
+        Extract the information from an OpenID assertion message and
+        verify it against the original
+
+        @param endpoint: The endpoint that resulted from doing discovery
+        @param resp_msg: The id_res message object
+
+        @returns: the verified endpoint
+        """
+        if resp_msg.getOpenIDNamespace() == OPENID2_NS:
+            return self._verifyDiscoveryResultsOpenID2(resp_msg, endpoint)
+        else:
+            return self._verifyDiscoveryResultsOpenID1(resp_msg, endpoint)
+
+
+    def _verifyDiscoveryResultsOpenID2(self, resp_msg, endpoint):
+        to_match = OpenIDServiceEndpoint()
+        to_match.type_uris = [OPENID_2_0_TYPE]
+        to_match.claimed_id = resp_msg.getArg(OPENID2_NS, 'claimed_id')
+        to_match.local_id = resp_msg.getArg(OPENID2_NS, 'identity')
+
+        # Raises a KeyError when the op_endpoint is not present
+        to_match.server_url = resp_msg.getArg(
+            OPENID2_NS, 'op_endpoint', no_default)
+
+        # claimed_id and identifier must both be present or both
+        # be absent
+        if (to_match.claimed_id is None and
+            to_match.local_id is not None):
+            raise ProtocolError(
+                'openid.identity is present without openid.claimed_id')
+
+        elif (to_match.claimed_id is not None and
+              to_match.local_id is None):
+            raise ProtocolError(
+                'openid.claimed_id is present without openid.identity')
+
+        # This is a response without identifiers, so there's really no
+        # checking that we can do, so return an endpoint that's for
+        # the specified `openid.op_endpoint'
+        elif to_match.claimed_id is None:
+            return OpenIDServiceEndpoint.fromOPEndpointURL(to_match.server_url)
+
+        # The claimed ID doesn't match, so we have to do discovery
+        # again. This covers not using sessions, OP identifier
+        # endpoints and responses that didn't match the original
+        # request.
+        if not endpoint:
+            oidutil.log('No pre-discovered information supplied.')
+            endpoint = self._discoverAndVerify(to_match.claimed_id, [to_match])
+        else:
+            # The claimed ID matches, so we use the endpoint that we
+            # discovered in initiation. This should be the most common
+            # case.
+            try:
+                self._verifyDiscoverySingle(endpoint, to_match)
+            except ProtocolError, e:
+                oidutil.log(
+                    "Error attempting to use stored discovery information: " +
+                    str(e))
+                oidutil.log("Attempting discovery to verify endpoint")
+                endpoint = self._discoverAndVerify(
+                    to_match.claimed_id, [to_match])
+
+        # The endpoint we return should have the claimed ID from the
+        # message we just verified, fragment and all.
+        if endpoint.claimed_id != to_match.claimed_id:
+            endpoint = copy.copy(endpoint)
+            endpoint.claimed_id = to_match.claimed_id
+        return endpoint
+
+    def _verifyDiscoveryResultsOpenID1(self, resp_msg, endpoint):
+        claimed_id = resp_msg.getArg(BARE_NS, self.openid1_return_to_identifier_name)
+
+        if endpoint is None and claimed_id is None:
+            raise RuntimeError(
+                'When using OpenID 1, the claimed ID must be supplied, '
+                'either by passing it through as a return_to parameter '
+                'or by using a session, and supplied to the GenericConsumer '
+                'as the argument to complete()')
+        elif endpoint is not None and claimed_id is None:
+            claimed_id = endpoint.claimed_id
+
+        to_match = OpenIDServiceEndpoint()
+        to_match.type_uris = [OPENID_1_1_TYPE]
+        to_match.local_id = resp_msg.getArg(OPENID1_NS, 'identity')
+        # Restore delegate information from the initiation phase
+        to_match.claimed_id = claimed_id
+
+        if to_match.local_id is None:
+            raise ProtocolError('Missing required field openid.identity')
+
+        to_match_1_0 = copy.copy(to_match)
+        to_match_1_0.type_uris = [OPENID_1_0_TYPE]
+
+        if endpoint is not None:
+            try:
+                try:
+                    self._verifyDiscoverySingle(endpoint, to_match)
+                except TypeURIMismatch:
+                    self._verifyDiscoverySingle(endpoint, to_match_1_0)
+            except ProtocolError, e:
+                oidutil.log("Error attempting to use stored discovery information: " +
+                            str(e))
+                oidutil.log("Attempting discovery to verify endpoint")
+            else:
+                return endpoint
+
+        # Endpoint is either bad (failed verification) or None
+        return self._discoverAndVerify(claimed_id, [to_match, to_match_1_0])
+
+    def _verifyDiscoverySingle(self, endpoint, to_match):
+        """Verify that the given endpoint matches the information
+        extracted from the OpenID assertion, and raise an exception if
+        there is a mismatch.
+
+        @type endpoint: openid.consumer.discover.OpenIDServiceEndpoint
+        @type to_match: openid.consumer.discover.OpenIDServiceEndpoint
+
+        @rtype: NoneType
+
+        @raises ProtocolError: when the endpoint does not match the
+            discovered information.
+        """
+        # Every type URI that's in the to_match endpoint has to be
+        # present in the discovered endpoint.
+        for type_uri in to_match.type_uris:
+            if not endpoint.usesExtension(type_uri):
+                raise TypeURIMismatch(type_uri, endpoint)
+
+        # Fragments do not influence discovery, so we can't compare a
+        # claimed identifier with a fragment to discovered information.
+        defragged_claimed_id, _ = urldefrag(to_match.claimed_id)
+        if defragged_claimed_id != endpoint.claimed_id:
+            raise ProtocolError(
+                'Claimed ID does not match (different subjects!), '
+                'Expected %s, got %s' %
+                (defragged_claimed_id, endpoint.claimed_id))
+
+        if to_match.getLocalID() != endpoint.getLocalID():
+            raise ProtocolError('local_id mismatch. Expected %s, got %s' %
+                                (to_match.getLocalID(), endpoint.getLocalID()))
+
+        # If the server URL is None, this must be an OpenID 1
+        # response, because op_endpoint is a required parameter in
+        # OpenID 2. In that case, we don't actually care what the
+        # discovered server_url is, because signature checking or
+        # check_auth should take care of that check for us.
+        if to_match.server_url is None:
+            assert to_match.preferredNamespace() == OPENID1_NS, (
+                """The code calling this must ensure that OpenID 2
+                responses have a non-none `openid.op_endpoint' and
+                that it is set as the `server_url' attribute of the
+                `to_match' endpoint.""")
+
+        elif to_match.server_url != endpoint.server_url:
+            raise ProtocolError('OP Endpoint mismatch. Expected %s, got %s' %
+                                (to_match.server_url, endpoint.server_url))
+
+    def _discoverAndVerify(self, claimed_id, to_match_endpoints):
+        """Given an endpoint object created from the information in an
+        OpenID response, perform discovery and verify the discovery
+        results, returning the matching endpoint that is the result of
+        doing that discovery.
+
+        @type to_match: openid.consumer.discover.OpenIDServiceEndpoint
+        @param to_match: The endpoint whose information we're confirming
+
+        @rtype: openid.consumer.discover.OpenIDServiceEndpoint
+        @returns: The result of performing discovery on the claimed
+            identifier in `to_match'
+
+        @raises DiscoveryFailure: when discovery fails.
+        """
+        oidutil.log('Performing discovery on %s' % (claimed_id,))
+        _, services = self._discover(claimed_id)
+        if not services:
+            raise DiscoveryFailure('No OpenID information found at %s' %
+                                   (claimed_id,), None)
+        return self._verifyDiscoveredServices(claimed_id, services,
+                                              to_match_endpoints)
+
+
+    def _verifyDiscoveredServices(self, claimed_id, services, to_match_endpoints):
+        """See @L{_discoverAndVerify}"""
+
+        # Search the services resulting from discovery to find one
+        # that matches the information from the assertion
+        failure_messages = []
+        for endpoint in services:
+            for to_match_endpoint in to_match_endpoints:
+                try:
+                    self._verifyDiscoverySingle(
+                        endpoint, to_match_endpoint)
+                except ProtocolError, why:
+                    failure_messages.append(str(why))
+                else:
+                    # It matches, so discover verification has
+                    # succeeded. Return this endpoint.
+                    return endpoint
+        else:
+            oidutil.log('Discovery verification failure for %s' %
+                        (claimed_id,))
+            for failure_message in failure_messages:
+                oidutil.log(' * Endpoint mismatch: ' + failure_message)
+
+            raise DiscoveryFailure(
+                'No matching endpoint found after discovering %s'
+                % (claimed_id,), None)
+
+    def _checkAuth(self, message, server_url):
+        """Make a check_authentication request to verify this message.
+
+        @returns: True if the request is valid.
+        @rtype: bool
+        """
+        oidutil.log('Using OpenID check_authentication')
+        request = self._createCheckAuthRequest(message)
+        if request is None:
+            return False
+        try:
+            response = self._makeKVPost(request, server_url)
+        except (fetchers.HTTPFetchingError, ServerError), e:
+            oidutil.log('check_authentication failed: %s' % (e[0],))
+            return False
+        else:
+            return self._processCheckAuthResponse(response, server_url)
+
+    def _createCheckAuthRequest(self, message):
+        """Generate a check_authentication request message given an
+        id_res message.
+        """
+        signed = message.getArg(OPENID_NS, 'signed')
+        if signed:
+            for k in signed.split(','):
+                oidutil.log(k)
+                val = message.getAliasedArg(k)
+
+                # Signed value is missing
+                if val is None:
+                    oidutil.log('Missing signed field %r' % (k,))
+                    return None
+
+        check_auth_message = message.copy()
+        check_auth_message.setArg(OPENID_NS, 'mode', 'check_authentication')
+        return check_auth_message
+
+    def _processCheckAuthResponse(self, response, server_url):
+        """Process the response message from a check_authentication
+        request, invalidating associations if requested.
+        """
+        is_valid = response.getArg(OPENID_NS, 'is_valid', 'false')
+
+        invalidate_handle = response.getArg(OPENID_NS, 'invalidate_handle')
+        if invalidate_handle is not None:
+            oidutil.log(
+                'Received "invalidate_handle" from server %s' % (server_url,))
+            if self.store is None:
+                oidutil.log('Unexpectedly got invalidate_handle without '
+                            'a store!')
+            else:
+                self.store.removeAssociation(server_url, invalidate_handle)
+
+        if is_valid == 'true':
+            return True
+        else:
+            oidutil.log('Server responds that checkAuth call is not valid')
+            return False
+
+    def _getAssociation(self, endpoint):
+        """Get an association for the endpoint's server_url.
+
+        First try seeing if we have a good association in the
+        store. If we do not, then attempt to negotiate an association
+        with the server.
+
+        If we negotiate a good association, it will get stored.
+
+        @returns: A valid association for the endpoint's server_url or None
+        @rtype: openid.association.Association or NoneType
+        """
+        assoc = self.store.getAssociation(endpoint.server_url)
+
+        if assoc is None or assoc.expiresIn <= 0:
+            assoc = self._negotiateAssociation(endpoint)
+            if assoc is not None:
+                self.store.storeAssociation(endpoint.server_url, assoc)
+
+        return assoc
+
+    def _negotiateAssociation(self, endpoint):
+        """Make association requests to the server, attempting to
+        create a new association.
+
+        @returns: a new association object
+
+        @rtype: L{openid.association.Association}
+        """
+        # Get our preferred session/association type from the negotiatior.
+        assoc_type, session_type = self.negotiator.getAllowedType()
+
+        try:
+            assoc = self._requestAssociation(
+                endpoint, assoc_type, session_type)
+        except ServerError, why:
+            supportedTypes = self._extractSupportedAssociationType(why,
+                                                                   endpoint,
+                                                                   assoc_type)
+            if supportedTypes is not None:
+                assoc_type, session_type = supportedTypes
+                # Attempt to create an association from the assoc_type
+                # and session_type that the server told us it
+                # supported.
+                try:
+                    assoc = self._requestAssociation(
+                        endpoint, assoc_type, session_type)
+                except ServerError, why:
+                    # Do not keep trying, since it rejected the
+                    # association type that it told us to use.
+                    oidutil.log('Server %s refused its suggested association '
+                                'type: session_type=%s, assoc_type=%s'
+                                % (endpoint.server_url, session_type,
+                                   assoc_type))
+                    return None
+                else:
+                    return assoc
+        else:
+            return assoc
+
+    def _extractSupportedAssociationType(self, server_error, endpoint,
+                                         assoc_type):
+        """Handle ServerErrors resulting from association requests.
+
+        @returns: If server replied with an C{unsupported-type} error,
+            return a tuple of supported C{association_type}, C{session_type}.
+            Otherwise logs the error and returns None.
+        @rtype: tuple or None
+        """
+        # Any error message whose code is not 'unsupported-type'
+        # should be considered a total failure.
+        if server_error.error_code != 'unsupported-type' or \
+               server_error.message.isOpenID1():
+            oidutil.log(
+                'Server error when requesting an association from %r: %s'
+                % (endpoint.server_url, server_error.error_text))
+            return None
+
+        # The server didn't like the association/session type
+        # that we sent, and it sent us back a message that
+        # might tell us how to handle it.
+        oidutil.log(
+            'Unsupported association type %s: %s' % (assoc_type,
+                                                     server_error.error_text,))
+
+        # Extract the session_type and assoc_type from the
+        # error message
+        assoc_type = server_error.message.getArg(OPENID_NS, 'assoc_type')
+        session_type = server_error.message.getArg(OPENID_NS, 'session_type')
+
+        if assoc_type is None or session_type is None:
+            oidutil.log('Server responded with unsupported association '
+                        'session but did not supply a fallback.')
+            return None
+        elif not self.negotiator.isAllowed(assoc_type, session_type):
+            fmt = ('Server sent unsupported session/association type: '
+                   'session_type=%s, assoc_type=%s')
+            oidutil.log(fmt % (session_type, assoc_type))
+            return None
+        else:
+            return assoc_type, session_type
+
+
+    def _requestAssociation(self, endpoint, assoc_type, session_type):
+        """Make and process one association request to this endpoint's
+        OP endpoint URL.
+
+        @returns: An association object or None if the association
+            processing failed.
+
+        @raises ServerError: when the remote OpenID server returns an error.
+        """
+        assoc_session, args = self._createAssociateRequest(
+            endpoint, assoc_type, session_type)
+
+        try:
+            response = self._makeKVPost(args, endpoint.server_url)
+        except fetchers.HTTPFetchingError, why:
+            oidutil.log('openid.associate request failed: %s' % (why[0],))
+            return None
+
+        try:
+            assoc = self._extractAssociation(response, assoc_session)
+        except KeyError, why:
+            oidutil.log('Missing required parameter in response from %s: %s'
+                        % (endpoint.server_url, why[0]))
+            return None
+        except ProtocolError, why:
+            oidutil.log('Protocol error parsing response from %s: %s' % (
+                endpoint.server_url, why[0]))
+            return None
+        else:
+            return assoc
+
+    def _createAssociateRequest(self, endpoint, assoc_type, session_type):
+        """Create an association request for the given assoc_type and
+        session_type.
+
+        @param endpoint: The endpoint whose server_url will be
+            queried. The important bit about the endpoint is whether
+            it's in compatiblity mode (OpenID 1.1)
+
+        @param assoc_type: The association type that the request
+            should ask for.
+        @type assoc_type: str
+
+        @param session_type: The session type that should be used in
+            the association request. The session_type is used to
+            create an association session object, and that session
+            object is asked for any additional fields that it needs to
+            add to the request.
+        @type session_type: str
+
+        @returns: a pair of the association session object and the
+            request message that will be sent to the server.
+        @rtype: (association session type (depends on session_type),
+                 openid.message.Message)
+        """
+        session_type_class = self.session_types[session_type]
+        assoc_session = session_type_class()
+
+        args = {
+            'mode': 'associate',
+            'assoc_type': assoc_type,