Udi Bauman avatar Udi Bauman committed 6bfa008

changed facebook integration to use django-SocialAuth

Comments (0)

Files changed (195)

Binary file modified.

django_ultrasound/app.yaml

 builtins:
 - remote_api: on
 
+
 inbound_services:
 - warmup
 

django_ultrasound/customers/templates/home.html

 </head>
 <body>
 
+<p>
+    Welcome, {{ username }} <img src="http://graph.facebook.com/{{ fb_id }}/picture " alt="{{ username }}"/>
+</p>
+<p>
+    Your friends: {{ friends_list }}
+</p>
+<hr/>
+
 {{ content }}
 
 

django_ultrasound/customers/views.py

-from django.contrib.auth.models import User
 from django.http import HttpResponseRedirect
 from django.contrib.auth.decorators import login_required
 from django.shortcuts import render_to_response
+from django.conf import settings
 from customers.forms import CustomerMoreDetailsForm
-from customers.models import CANDIDATE, CustomerStatus
+from customers.models import CANDIDATE
 from common.utils import evaluate_template
 from employees.models import Employee
 from customers.models import Customer, CustomerStatus, REQUESTING_TO_BE_CANDIDATE
-from django.contrib import auth
+import logging
+from socialauth import facebook
 
+@login_required
 def customer_home(request):
     user = request.user
 
 
     try:
         customer = user.customer.get()
-    except:
-        contrib_user = User()
-        contrib_user.save()
-        contrib_user.username = u"user_%s" % contrib_user.id
-        contrib_user.is_staff = True
-        contrib_user.is_superuser = True
-        password = "tukyrtxtubs"
-        contrib_user.set_password(password)
-        contrib_user.save()
+        status = "found existing customer: %d" % customer.id
+    except Customer.DoesNotExist:
+#        contrib_user = User()
+#        contrib_user.save()
+#        contrib_user.username = u"user_%s" % contrib_user.id
+#        contrib_user.is_staff = True
+#        contrib_user.is_superuser = True
+#        password = "tukyrtxtubs"
+#        contrib_user.set_password(password)
+#        contrib_user.save()
 
         # create Customer
         customer = Customer()
-        customer.user = contrib_user
-        customer.fb_key = contrib_user.id # fb.facebook_id
+        customer.user = user
+        customer.fb_key = user.username # fb.facebook_id  # TODO get id from facebook user profile
         #customer.link_to_page = "http://www.facebook.com/?uid=%s" % customer.fb_key
         customer.customer_status = CustomerStatus.objects.get(code=REQUESTING_TO_BE_CANDIDATE)
         customer.save()
 
-        status = "created new user"
+        status = "created new customer: %d" % customer.id
+        
+    logging.info(status)
 
-        authenticated_user = auth.authenticate(
-                                     username=contrib_user.username,
-                                     password=password)
-        auth.login(request, authenticated_user)
-
+#        authenticated_user = auth.authenticate(
+#                                     username=contrib_user.username,
+#                                     password=password)
+#        auth.login(request, authenticated_user)
 
 
     customer_status = customer.customer_status
     data["content"] = evaluate_template(customer_status.html_page.content, data)
     if customer.customer_status == CANDIDATE:
         data = customer_more_details_form_home(data)
+
+    data["fb_id"] = user.username
+
+    if "access_token" not in request.session:
+        FACEBOOK_APP_ID = getattr(settings, 'FACEBOOK_APP_ID', '')
+        FACEBOOK_SECRET_KEY = getattr(settings, 'FACEBOOK_SECRET_KEY', '')
+        cookie = facebook.get_user_from_cookie(request.COOKIES,
+                                       FACEBOOK_APP_ID,
+                                       FACEBOOK_SECRET_KEY)
+        if cookie:
+            access_token = cookie['access_token']
+            request.session['access_token'] = access_token
+
+    if "access_token" in request.session:
+        graph = facebook.GraphAPI(request.session["access_token"])
+        profile = graph.get_object("me")
+        data["username"] = profile["name"]
+        friends = graph.get_connections("me", "friends")
+        try:
+            logging.info(friends)
+        except:
+            pass
+        data["friends_list"] = ", ".join([f["name"] for f in friends["data"]])
+
     return render_to_response("home.html", data)
 
 

django_ultrasound/employees/views.py

 from django.contrib.auth.decorators import login_required
 from employees.models import *
 from django.utils.translation import ugettext as _
-
+from django.conf import settings
 
 
 
 def employee_home(request):
 
     user = request.user
-    
-    employee = Employee.objects.get(user=request.user)
+
+    try:
+        employee = Employee.objects.get(user=request.user)
+    except Employee.DoesNotExist:
+        return HttpResponse(_("We couldn't recognize you as an employee. Please consult with") + " <a href='mailto:%s'>%s</a>" % (settings.ADMINS[0][1], settings.ADMINS[0][0]))
     if employee.employee_type.code == CLUB_MANAGER:
         app_list = [
                 { "name": _("activities"),

django_ultrasound/facebook.py

+#!/usr/bin/env python
+#
+# Copyright 2010 Facebook
+#
+# 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
+#
+#     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.
+
+"""Python client library for the Facebook Platform.
+
+This client library is designed to support the Graph API and the official
+Facebook JavaScript SDK, which is the canonical way to implement
+Facebook authentication. Read more about the Graph API at
+http://developers.facebook.com/docs/api. You can download the Facebook
+JavaScript SDK at http://github.com/facebook/connect-js/.
+
+If your application is using Google AppEngine's webapp framework, your
+usage of this module might look like this:
+
+    user = facebook.get_user_from_cookie(self.request.cookies, key, secret)
+    if user:
+        graph = facebook.GraphAPI(user["access_token"])
+        profile = graph.get_object("me")
+        friends = graph.get_connections("me", "friends")
+
+"""
+
+import cgi
+import hashlib
+import time
+import urllib
+
+# Find a JSON parser
+try:
+    import json
+    _parse_json = lambda s: json.loads(s)
+except ImportError:
+    try:
+        import simplejson
+        _parse_json = lambda s: simplejson.loads(s)
+    except ImportError:
+        # For Google AppEngine
+        from django.utils import simplejson
+        _parse_json = lambda s: simplejson.loads(s)
+
+
+class GraphAPI(object):
+    """A client for the Facebook Graph API.
+
+    See http://developers.facebook.com/docs/api for complete documentation
+    for the API.
+
+    The Graph API is made up of the objects in Facebook (e.g., people, pages,
+    events, photos) and the connections between them (e.g., friends,
+    photo tags, and event RSVPs). This client provides access to those
+    primitive types in a generic way. For example, given an OAuth access
+    token, this will fetch the profile of the active user and the list
+    of the user's friends:
+
+       graph = facebook.GraphAPI(access_token)
+       user = graph.get_object("me")
+       friends = graph.get_connections(user["id"], "friends")
+
+    You can see a list of all of the objects and connections supported
+    by the API at http://developers.facebook.com/docs/reference/api/.
+
+    You can obtain an access token via OAuth or by using the Facebook
+    JavaScript SDK. See http://developers.facebook.com/docs/authentication/
+    for details.
+
+    If you are using the JavaScript SDK, you can use the
+    get_user_from_cookie() method below to get the OAuth access token
+    for the active user from the cookie saved by the SDK.
+    """
+    def __init__(self, access_token=None):
+        self.access_token = access_token
+
+    def get_object(self, id, **args):
+        """Fetchs the given object from the graph."""
+        return self.request(id, args)
+
+    def get_objects(self, ids, **args):
+        """Fetchs all of the given object from the graph.
+
+        We return a map from ID to object. If any of the IDs are invalid,
+        we raise an exception.
+        """
+        args["ids"] = ",".join(ids)
+        return self.request("", args)
+
+    def get_connections(self, id, connection_name, **args):
+        """Fetchs the connections for given object."""
+        return self.request(id + "/" + connection_name, args)
+
+    def put_object(self, parent_object, connection_name, **data):
+        """Writes the given object to the graph, connected to the given parent.
+
+        For example,
+
+            graph.put_object("me", "feed", message="Hello, world")
+
+        writes "Hello, world" to the active user's wall. Likewise, this
+        will comment on a the first post of the active user's feed:
+
+            feed = graph.get_connections("me", "feed")
+            post = feed["data"][0]
+            graph.put_object(post["id"], "comments", message="First!")
+
+        See http://developers.facebook.com/docs/api#publishing for all of
+        the supported writeable objects.
+
+        Most write operations require extended permissions. For example,
+        publishing wall posts requires the "publish_stream" permission. See
+        http://developers.facebook.com/docs/authentication/ for details about
+        extended permissions.
+        """
+        assert self.access_token, "Write operations require an access token"
+        return self.request(parent_object + "/" + connection_name, post_args=data)
+
+    def put_wall_post(self, message, attachment={}, profile_id="me"):
+        """Writes a wall post to the given profile's wall.
+
+        We default to writing to the authenticated user's wall if no
+        profile_id is specified.
+
+        attachment adds a structured attachment to the status message being
+        posted to the Wall. It should be a dictionary of the form:
+
+            {"name": "Link name"
+             "link": "http://www.example.com/",
+             "caption": "{*actor*} posted a new review",
+             "description": "This is a longer description of the attachment",
+             "picture": "http://www.example.com/thumbnail.jpg"}
+
+        """
+        return self.put_object(profile_id, "feed", message=message, **attachment)
+
+    def put_comment(self, object_id, message):
+        """Writes the given comment on the given post."""
+        return self.put_object(object_id, "comments", message=message)
+
+    def put_like(self, object_id):
+        """Likes the given post."""
+        return self.put_object(object_id, "likes")
+
+    def delete_object(self, id):
+        """Deletes the object with the given ID from the graph."""
+        self.request(id, post_args={"method": "delete"})
+
+    def request(self, path, args=None, post_args=None):
+        """Fetches the given path in the Graph API.
+
+        We translate args to a valid query string. If post_args is given,
+        we send a POST request to the given path with the given arguments.
+        """
+        if not args: args = {}
+        if self.access_token:
+            if post_args is not None:
+                post_args["access_token"] = self.access_token
+            else:
+                args["access_token"] = self.access_token
+        post_data = None if post_args is None else urllib.urlencode(post_args)
+        file = urllib.urlopen("https://graph.facebook.com/" + path + "?" +
+                              urllib.urlencode(args), post_data)
+        try:
+            response = _parse_json(file.read())
+        finally:
+            file.close()
+        if response.get("error"):
+            raise GraphAPIError(response["error"]["type"],
+                                response["error"]["message"])
+        return response
+
+
+class GraphAPIError(Exception):
+    def __init__(self, type, message):
+        Exception.__init__(self, message)
+        self.type = type
+
+
+def get_user_from_cookie(cookies, app_id, app_secret):
+    """Parses the cookie set by the official Facebook JavaScript SDK.
+
+    cookies should be a dictionary-like object mapping cookie names to
+    cookie values.
+
+    If the user is logged in via Facebook, we return a dictionary with the
+    keys "uid" and "access_token". The former is the user's Facebook ID,
+    and the latter can be used to make authenticated requests to the Graph API.
+    If the user is not logged in, we return None.
+
+    Download the official Facebook JavaScript SDK at
+    http://github.com/facebook/connect-js/. Read more about Facebook
+    authentication at http://developers.facebook.com/docs/authentication/.
+    """
+    cookie = cookies.get("fbs_" + app_id, "")
+    if not cookie: return None
+    args = dict((k, v[-1]) for k, v in cgi.parse_qs(cookie.strip('"')).items())
+    payload = "".join(k + "=" + args[k] for k in sorted(args.keys())
+                      if k != "sig")
+    sig = hashlib.md5(payload + app_secret).hexdigest()
+    expires = int(args["expires"])
+    if sig == args.get("sig") and (expires == 0 or time.time() < expires):
+        return args
+    else:
+        return None
Add a comment to this file

django_ultrasound/facebookconnect/__init__.py

Empty file removed.

django_ultrasound/facebookconnect/models.py

-from django.contrib.auth.models import User
-from django.db import models
-
-class FacebookUser(models.Model):
-    facebook_id = models.CharField(max_length=100, unique=True)
-    contrib_user = models.OneToOneField(User)
-    contrib_password = models.CharField(max_length=100)

django_ultrasound/facebookconnect/templates/test.html

-<html xmlns="http://www.w3.org/1999/xhtml"
-      xmlns:fb="http://www.facebook.com/2008/fbml">
-<body>
-
-<p>Login via facebook!</p>
-
-{% load facebookconnect %}
-{% facebook_connect_login_button %}
-
-{% facebook_connect_script %}
-
-</body>
-</html>

django_ultrasound/facebookconnect/templates/xd_receiver.htm

-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" >
-<body>
-    <script src="http://static.ak.connect.facebook.com/js/api_lib/v0.4/XdCommReceiver.js" type="text/javascript"></script>
-</body>
-</html>

django_ultrasound/facebookconnect/templatetags/__init__.py

-__author__ = 'udibauman'
-  

django_ultrasound/facebookconnect/templatetags/facebookconnect.py

-from django import template
-from django.conf import settings
-from django.core.urlresolvers import reverse
-
-register = template.Library()
-
-class FacebookScriptNode(template.Node):
-        def render(self, context):
-            return """
-            <script src="http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php" type="text/javascript"></script>
-
-            <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
-
-            <script type="text/javascript"> FB.init("%s", "%s");
-                function facebook_onlogin() {
-                    var uid = FB.Facebook.apiClient.get_session().uid;
-                    var session_key = FB.Facebook.apiClient.get_session().session_key;
-                    var expires = FB.Facebook.apiClient.get_session().expires;
-                    var secret = FB.Facebook.apiClient.get_session().secret;
-                    var sig = FB.Facebook.apiClient.get_session().sig;
-
-                    fb_connect_ajax(expires, session_key, secret, uid, sig);
-
-                }
-
-                function fb_connect_ajax(expires, session_key, ss, user, sig) {
-
-                    var post_string = 'expires=' + expires;
-                    post_string = post_string + '&session_key=' + session_key;
-                    post_string = post_string + '&ss=' + ss;
-                    post_string = post_string + '&user=' + user;
-                    post_string = post_string + '&sig=' + sig;
-
-                    $.ajax({
-                        type: "POST",
-                        url: "%s",
-                        data: post_string,
-                        success: function(msg) {
-                            window.location = '%s'; //.reload()
-                        }
-                    });
-                }
-            </script>
-            """ % (settings.FACEBOOK_API_KEY, reverse('xd_receiver'), reverse('facebook_connect_ajax'), settings.LOGIN_REDIRECT_URL)
-
-
-def facebook_connect_script(parser, token): return FacebookScriptNode()
-
-register.tag(facebook_connect_script)
-
-class FacebookLoginNode(template.Node):
-    def render(self, context):
-        return "<fb:login-button onlogin='facebook_onlogin();'></fb:login-button>"
-
-def facebook_connect_login_button(parser, token): return FacebookLoginNode()
-
-register.tag(facebook_connect_login_button)

django_ultrasound/facebookconnect/tests.py

-"""
-This file demonstrates two different styles of tests (one doctest and one
-unittest). These will both pass when you run "manage.py test".
-
-Replace these with more appropriate tests for your application.
-"""
-
-from django.test import TestCase
-
-class SimpleTest(TestCase):
-    def test_basic_addition(self):
-        """
-        Tests that 1 + 1 always equals 2.
-        """
-        self.failUnlessEqual(1 + 1, 2)
-
-__test__ = {"doctest": """
-Another way to test that 1 + 1 is equal to 2.
-
->>> 1 + 1 == 2
-True
-"""}
-

django_ultrasound/facebookconnect/urls.py

-from django.conf.urls.defaults import patterns, url
-from django.views.generic.simple import direct_to_template
-
-urlpatterns = patterns('facebookconnect.views',
-    url(r'^xd_receiver\.htm$', direct_to_template, {'template': 'xd_receiver.htm'}, name='xd_receiver'),
-    url(r'^login_facebook_connect/$', 'login_facebook_connect', name='facebook_connect_ajax'),
-)

django_ultrasound/facebookconnect/views.py

-import datetime
-import hashlib
-import logging
-
-from django.conf import settings
-from django.contrib import auth
-from django.contrib.auth.models import User
-from django.http import HttpResponse
-from django.shortcuts import render_to_response
-
-from facebookconnect.models import FacebookUser
-from customers.models import Customer, CustomerStatus, REQUESTING_TO_BE_CANDIDATE
-
-def login_facebook_connect(request):
-    status = 'unknown failure'
-#    try:
-    expires = request.POST['expires']
-    ss = request.POST['ss']
-    session_key = request.POST['session_key']
-    user = request.POST['user']
-    sig = request.POST['sig']
-
-    pre_hash_string = "expires=%ssession_key=%sss=%suser=%s%s" % (
-        expires,
-        session_key,
-        ss,
-        user,
-        settings.FACEBOOK_APPLICATION_SECRET,
-    )
-    post_hash_string = hashlib.new('md5')
-    post_hash_string.update(pre_hash_string)
-    if post_hash_string.hexdigest() == sig:
-        try:
-            fb = FacebookUser.objects.get(facebook_id=user)
-            status = "logged in existing user"
-        except FacebookUser.DoesNotExist:
-            contrib_user = User()
-            contrib_user.save()
-            contrib_user.username = u"fbuser_%s" % contrib_user.id
-            contrib_user.is_staff = True
-            contrib_user.is_superuser = True
-
-
-            fb = FacebookUser()
-            fb.facebook_id = user
-            fb.contrib_user = contrib_user
-
-            temp = hashlib.new('sha1')
-            temp.update(str(datetime.datetime.now()))
-            password = temp.hexdigest()
-
-            contrib_user.set_password(password)
-            fb.contrib_password = password
-            fb.save()
-            contrib_user.save()
-
-            # create Customer
-            customer = Customer()
-            customer.user = contrib_user
-            customer.fb_key = fb.facebook_id
-            customer.link_to_page = "http://www.facebook.com/?uid=%s" % customer.fb_key
-            customer.customer_status = CustomerStatus.objects.get(code=REQUESTING_TO_BE_CANDIDATE)
-            customer.save()
-
-            status = "created new user"
-
-        authenticated_user = auth.authenticate(
-                                     username=fb.contrib_user.username,
-                                     password=fb.contrib_password)
-        auth.login(request, authenticated_user)
-    else:
-        status = 'wrong hash sig'
-
-        logging.debug("FBConnect: user %s with exit status %s" % (user, status))
-
-#    except Exception, e:
-#        logging.debug("Exception thrown in the FBConnect ajax call: %s" % e)
-
-    return HttpResponse("%s" % status)
-
-def xd_receiver(request):
-        return render_to_response('facebookconnect/xd_receiver.html')

django_ultrasound/index.yaml

   properties:
   - name: __key__
     direction: desc
+
+- kind: events_htmlpage
+  properties:
+  - name: __key__
+    direction: desc
Add a comment to this file

django_ultrasound/initial_auth_data.json

File contents unchanged.

django_ultrasound/localsettings.py

+OPENID_REDIRECT_NEXT = '/accounts/openid/done/'
+
+OPENID_SREG = {"requred": "nickname, email, fullname",
+               "optional":"postcode, country",
+               "policy_url": ""}
+
+#example should be something more like the real thing, i think
+OPENID_AX = [{"type_uri": "http://axschema.org/contact/email",
+              "count": 1,
+              "required": True,
+              "alias": "email"},
+             {"type_uri": "http://axschema.org/schema/fullname",
+              "count":1 ,
+              "required": False,
+              "alias": "fname"}]
+
+OPENID_AX_PROVIDER_MAP = {'Google': {'email': 'http://axschema.org/contact/email',
+                                     'firstname': 'http://axschema.org/namePerson/first',
+                                     'lastname': 'http://axschema.org/namePerson/last'},
+                          'Default': {'email': 'http://axschema.org/contact/email',
+                                      'fullname': 'http://axschema.org/namePerson',
+                                      'nickname': 'http://axschema.org/namePerson/friendly'}
+                          }
+
+TWITTER_CONSUMER_KEY = ''
+TWITTER_CONSUMER_SECRET = ''
+
+FACEBOOK_APP_ID = '161522493893246'
+FACEBOOK_API_KEY = '0f2cd1705e6eed547f35151ac8e817b2'
+FACEBOOK_SECRET_KEY = 'ef796081cc54f631ec50671174f7ba86'
+
+LINKEDIN_CONSUMER_KEY = ''
+LINKEDIN_CONSUMER_SECRET = ''
+
+## if any of this information is desired for your app
+FACEBOOK_EXTENDED_PERMISSIONS = (
+    'publish_stream',
+    'create_event',
+    #'rsvp_event',
+    #'sms',
+    #'offline_access',
+    'email',
+    #'read_stream',
+    'user_about_me',
+    #'user_activites',
+    'user_birthday',
+    #'user_education_history',
+    #'user_events',
+    #'user_groups',
+    'user_hometown',
+    'user_interests',
+    #'user_likes',
+    #'user_location',
+    #'user_notes',
+    #'user_online_presence',
+    #'user_photo_video_tags',
+    'user_photos',
+    #'user_relationships',
+    #'user_religion_politics',
+    #'user_status',
+    #'user_videos',
+    #'user_website',
+    #'user_work_history',
+    'read_friendlists',
+    #'read_requests',
+    'friend_about_me',
+    #'friend_activites',
+    #'friend_birthday',
+    #'friend_education_history',
+    #'friend_events',
+    #'friend_groups',
+    'friend_hometown',
+    'friend_interests',
+    #'friend_likes',
+    #'friend_location',
+    #'friend_notes',
+    #'friend_online_presence',
+    #'friend_photo_video_tags',
+    'friend_photos',
+    #'friend_relationships',
+    #'friend_religion_politics',
+    #'friend_status',
+    #'friend_videos',
+    #'friend_website',
+    #'friend_work_history',
+)
+
+
+AUTHENTICATION_BACKENDS = (
+    'django.contrib.auth.backends.ModelBackend',
+#    'socialauth.auth_backends.OpenIdBackend',
+#    'socialauth.auth_backends.TwitterBackend',
+    'socialauth.auth_backends.FacebookBackend',
+#    'socialauth.auth_backends.LinkedInBackend',
+)
+
+
+ADMINS = (
+    ('Rami', 'rami.zelingher@gmail.com'),
+)

django_ultrasound/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{http://openid.net/developers/dev-mailing-lists/
+    <http://openid.net/developers/dev-mailing-lists/}
+
+@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)

django_ultrasound/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)

django_ultrasound/openid/consumer/__init__.py

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

django_ultrasound/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 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 a 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 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):