[snowy: 24/26] Fetch user attributes from sreg and ax for openid registration



commit 4f8cb9365b3f2c4c22500398303d05cb0392202b
Author: Leon Handreke <leon handreke gmail com>
Date:   Sat May 29 23:17:11 2010 +0200

    Fetch user attributes from sreg and ax for openid registration
    
    [django_openid_auth] Upgrade to newest version that supports attribute exchange

 accounts/forms.py                          |    1 +
 accounts/views.py                          |   20 ++++--
 lib/django_openid_auth/auth.py             |  113 +++++++++++++++++++---------
 lib/django_openid_auth/tests/test_views.py |   80 ++++++++++++++++++--
 lib/django_openid_auth/views.py            |   15 ++++-
 5 files changed, 177 insertions(+), 52 deletions(-)
---
diff --git a/accounts/forms.py b/accounts/forms.py
index 8ebd945..1134800 100644
--- a/accounts/forms.py
+++ b/accounts/forms.py
@@ -43,6 +43,7 @@ class RegistrationFormUniqueUser(RegistrationFormUniqueEmail):
         
         self.fields['username'].label = _(u'Username')
         self.fields['username'].help_text = _(u'Maximum of 30 characters in length.')
+        self.fields['username'].error_messages['invalid'] = _(u'Usernames may not contain special characters.')
 
         self.fields['email'].label = _(u'Email address')
 
diff --git a/accounts/views.py b/accounts/views.py
index bd88e89..d8e7d46 100644
--- a/accounts/views.py
+++ b/accounts/views.py
@@ -30,14 +30,22 @@ from django.conf import settings
 from snowy.accounts.models import UserProfile
 from snowy.accounts.forms import InternationalizationForm, OpenIDRegistrationFormUniqueUser
 
+from django_openid_auth import auth
+
 def openid_registration(request, template_name='registration/registration_form.html'):
+    registration_form = OpenIDRegistrationFormUniqueUser(request.POST or None)
+
     try:
         openid_response = request.session['openid_response']
-        # do sreg magic here
     except KeyError:
         return HttpResponseNotAllowed(_(u'No openid_response object for this session!'))
 
-    registration_form = OpenIDRegistrationFormUniqueUser(request.POST or None)
+    try:
+        attributes = auth._extract_user_details(openid_response)
+        registration_form.fields['username'].initial = attributes['nickname']
+        registration_form.fields['email'].initial = attributes['email']
+    except KeyError:
+        pass
 
     if registration_form.is_valid():
         user = authenticate(openid_response=openid_response,
@@ -47,13 +55,13 @@ def openid_registration(request, template_name='registration/registration_form.h
         del request.session['openid_response']
 
         if user is not None:
-            email = registration_form.cleaned_data.get('email', '')
+            email = registration_form.cleaned_data.get('email')
             if email:
                 user.email = email
 
-            display_name = registration_form.cleaned_data.get('display_name', '')
-            if display_name:
-                user.get_profile().display_name = display_name
+            #display_name = registration_form.cleaned_data.get('display_name')
+            #if display_name:
+            #    user.get_profile().display_name = display_name
 
             user.save()
             user.get_profile().save()
diff --git a/lib/django_openid_auth/auth.py b/lib/django_openid_auth/auth.py
index 1670e02..220f20c 100644
--- a/lib/django_openid_auth/auth.py
+++ b/lib/django_openid_auth/auth.py
@@ -33,7 +33,7 @@ __metaclass__ = type
 from django.conf import settings
 from django.contrib.auth.models import User, Group
 from openid.consumer.consumer import SUCCESS
-from openid.extensions import sreg
+from openid.extensions import ax, sreg
 
 from django_openid_auth import teams
 from django_openid_auth.models import UserOpenID
@@ -71,17 +71,13 @@ class OpenIDBackend:
             user_openid = UserOpenID.objects.get(
                 claimed_id__exact=openid_response.identity_url)
         except UserOpenID.DoesNotExist:
-            if kwargs.get('create_user'):
-                if kwargs.get('username'):
-                    user = self.create_user_from_openid(openid_response,
-                                                        username=kwargs.get('username'))
-                else:
-                    return None
+            if kwargs.get('create_user') and kwargs.get('username'):
+                user = self.create_user_from_openid(openid_response,
+                                                    username=kwargs.get('username'))
+            elif getattr(settings, 'OPENID_CREATE_USERS', False):
+                user = self.create_user_from_openid(openid_response)
             else:
-                if getattr(settings, 'OPENID_CREATE_USERS', False):
-                    user = self.create_user_from_openid(openid_response)
-                else:
-                    return None
+                return None
         else:
             user = user_openid.user
 
@@ -89,10 +85,8 @@ class OpenIDBackend:
             return None
 
         if getattr(settings, 'OPENID_UPDATE_DETAILS_FROM_SREG', False):
-            sreg_response = sreg.SRegResponse.fromSuccessResponse(
-                openid_response)
-            if sreg_response:
-                self.update_user_details_from_sreg(user, sreg_response)
+            details = _extract_user_details(openid_response)
+            self.update_user_details(user, details)
 
         teams_response = teams.TeamsResponse.fromSuccessResponse(
             openid_response)
@@ -102,27 +96,29 @@ class OpenIDBackend:
         return user
 
     def create_user_from_openid(self, openid_response, username='snowyuser', email=''):
-        sreg_response = sreg.SRegResponse.fromSuccessResponse(openid_response)
+        nickname = username
+        #email = details['email'] or ''
+
         # Pick a username for the user based on their nickname,
         # checking for conflicts.
         i = 1
         while True:
-            # use the name nickname here so we don't interfere with the username argument
-            nickname = username
+            username = nickname
             if i > 1:
-                nickname += str(i)
+                username += str(i)
             try:
-                User.objects.get(username__exact=nickname)
+                User.objects.get(username__exact=username)
             except User.DoesNotExist:
                 break
             i += 1
 
-        user = User.objects.create_user(nickname, email, password=None)
+        user = User.objects.create_user(username, email, password=None)
         user.get_profile().openid_user = True
         user.get_profile().save()
 
-        if sreg_response:
-            self.update_user_details_from_sreg(user, sreg_response)
+        if getattr(settings, 'OPENID_UPDATE_DETAILS_FROM_SREG', False):
+            details = _extract_user_details(openid_response)
+            self.update_user_details(user, details)
 
         self.associate_openid(user, openid_response)
         return user
@@ -147,20 +143,20 @@ class OpenIDBackend:
 
         return user_openid
 
-    def update_user_details_from_sreg(self, user, sreg_response):
-        fullname = sreg_response.get('fullname')
-        if fullname:
-            # Do our best here ...
-            if ' ' in fullname:
-                user.first_name, user.last_name = fullname.rsplit(None, 1)
-            else:
-                user.first_name = u''
-                user.last_name = fullname
-
-        email = sreg_response.get('email')
-        if email:
-            user.email = email
-        user.save()
+    def update_user_details(self, user, details):
+        updated = False
+        if details['first_name']:
+            user.first_name = details['first_name']
+            updated = True
+        if details['last_name']:
+            user.last_name = details['last_name']
+            updated = True
+        if details['email']:
+            user.email = details['email']
+            updated = True
+
+        if updated:
+            user.save()
 
     def update_groups_from_teams(self, user, teams_response):
         teams_mapping_auto = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO', False)
@@ -186,3 +182,46 @@ class OpenIDBackend:
             user.groups.remove(group)
         for group in desired_groups - current_groups:
             user.groups.add(group)
+
+# to be used outside of the backend
+def _extract_user_details(openid_response):
+    email = fullname = first_name = last_name = nickname = None
+    sreg_response = sreg.SRegResponse.fromSuccessResponse(openid_response)
+    if sreg_response:
+        email = sreg_response.get('email')
+        fullname = sreg_response.get('fullname')
+        nickname = sreg_response.get('nickname')
+
+    # If any attributes are provided via Attribute Exchange, use
+    # them in preference.
+    fetch_response = ax.FetchResponse.fromSuccessResponse(openid_response)
+    if fetch_response:
+        email = fetch_response.getSingle(
+            'http://axschema.org/contact/email', email)
+        fullname = fetch_response.getSingle(
+            'http://axschema.org/namePerson', fullname)
+        first_name = fetch_response.getSingle(
+            'http://axschema.org/namePerson/first', first_name)
+        last_name = fetch_response.getSingle(
+            'http://axschema.org/namePerson/last', last_name)
+        nickname = fetch_response.getSingle(
+            'http://axschema.org/namePerson/friendly', nickname)
+
+    if fullname and not (first_name or last_name):
+        # Django wants to store first and last names separately,
+        # so we do our best to split the full name.
+        if ' ' in fullname:
+            first_name, last_name = fullname.rsplit(None, 1)
+        else:
+            first_name = u''
+            last_name = fullname
+
+    if (first_name and last_name) and not fullname:
+        fullname = last_name + " " + last_name
+
+    if (first_name and last_name) and not nickname:
+        nickname = (first_name + last_name).lower()
+
+    return dict(email=email, nickname=nickname,
+                first_name=first_name, last_name=last_name,
+                fullname=fullname)
diff --git a/lib/django_openid_auth/tests/test_views.py b/lib/django_openid_auth/tests/test_views.py
index 0c6c546..26cd87c 100644
--- a/lib/django_openid_auth/tests/test_views.py
+++ b/lib/django_openid_auth/tests/test_views.py
@@ -34,7 +34,7 @@ import unittest
 from django.conf import settings
 from django.contrib.auth.models import User, Group
 from django.test import TestCase
-from openid.extensions.sreg import SRegRequest, SRegResponse
+from openid.extensions import ax, sreg
 from openid.fetchers import (
     HTTPFetcher, HTTPFetchingError, HTTPResponse, setDefaultFetcher)
 from openid.oidutil import importElementTree
@@ -58,10 +58,12 @@ class StubOpenIDProvider(HTTPFetcher):
         self.endpoint_url = base_url + 'endpoint'
         self.server = Server(self.store, self.endpoint_url)
         self.last_request = None
+        self.type_uris = ['http://specs.openid.net/auth/2.0/signon']
 
     def fetch(self, url, body=None, headers=None):
         if url == self.identity_url:
-            # Serve an XRDS document directly, which is the 
+            # Serve an XRDS document directly, pointing at our endpoint.
+            type_uris = ['<Type>%s</Type>' % uri for uri in self.type_uris]
             return HTTPResponse(
                 url, 200, {'content-type': 'application/xrds+xml'}, """\
 <?xml version="1.0"?>
@@ -70,13 +72,13 @@ class StubOpenIDProvider(HTTPFetcher):
     xmlns:xrds="xri://$xrds">
   <XRD>
     <Service priority="0">
-      <Type>http://specs.openid.net/auth/2.0/signon</Type>
+      %s
       <URI>%s</URI>
       <LocalID>%s</LocalID>
     </Service>
   </XRD>
 </xrds:XRDS>
-""" % (self.endpoint_url, self.localid_url))
+""" % ('\n'.join(type_uris), self.endpoint_url, self.localid_url))
         elif url.startswith(self.endpoint_url):
             # Gather query parameters
             query = {}
@@ -260,9 +262,9 @@ class RelyingPartyTests(TestCase):
         # Complete the request, passing back some simple registration
         # data.  The user is redirected to the next URL.
         openid_request = self.provider.parseFormPost(response.content)
-        sreg_request = SRegRequest.fromOpenIDRequest(openid_request)
+        sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request)
         openid_response = openid_request.answer(True)
-        sreg_response = SRegResponse.extractResponse(
+        sreg_response = sreg.SRegResponse.extractResponse(
             sreg_request, {'nickname': 'someuser', 'fullname': 'Some User',
                            'email': 'foo example com'})
         openid_response.addExtension(sreg_response)
@@ -298,9 +300,9 @@ class RelyingPartyTests(TestCase):
         # Complete the request, passing back some simple registration
         # data.  The user is redirected to the next URL.
         openid_request = self.provider.parseFormPost(response.content)
-        sreg_request = SRegRequest.fromOpenIDRequest(openid_request)
+        sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request)
         openid_response = openid_request.answer(True)
-        sreg_response = SRegResponse.extractResponse(
+        sreg_response = sreg.SRegResponse.extractResponse(
             sreg_request, {'nickname': 'someuser', 'fullname': 'Some User',
                            'email': 'foo example com'})
         openid_response.addExtension(sreg_response)
@@ -318,6 +320,68 @@ class RelyingPartyTests(TestCase):
         self.assertEquals(user.last_name, 'User')
         self.assertEquals(user.email, 'foo example com')
 
+    def test_login_attribute_exchange(self):
+        settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
+        user = User.objects.create_user('testuser', 'someone example com')
+        useropenid = UserOpenID(
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+        useropenid.save()
+
+        # Configure the provider to advertise attribute exchange
+        # protocol and start the authentication process:
+        self.provider.type_uris.append('http://openid.net/srv/ax/1.0')
+        response = self.client.post('/openid/login/',
+            {'openid_identifier': 'http://example.com/identity',
+             'next': '/getuser/'})
+        self.assertContains(response, 'OpenID transaction in progress')
+
+        # The resulting OpenID request uses the Attribute Exchange
+        # extension rather than the Simple Registration extension.
+        openid_request = self.provider.parseFormPost(response.content)
+        sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request)
+        self.assertEqual(sreg_request.required, [])
+        self.assertEqual(sreg_request.optional, [])
+
+        fetch_request = ax.FetchRequest.fromOpenIDRequest(openid_request)
+        self.assertTrue(fetch_request.has_key(
+                'http://axschema.org/contact/email'))
+        self.assertTrue(fetch_request.has_key(
+                'http://axschema.org/namePerson'))
+        self.assertTrue(fetch_request.has_key(
+                'http://axschema.org/namePerson/first'))
+        self.assertTrue(fetch_request.has_key(
+                'http://axschema.org/namePerson/last'))
+        self.assertTrue(fetch_request.has_key(
+                'http://axschema.org/namePerson/friendly'))
+
+        # Build up a response including AX data.
+        openid_response = openid_request.answer(True)
+        fetch_response = ax.FetchResponse(fetch_request)
+        fetch_response.addValue(
+            'http://axschema.org/contact/email', 'foo example com')
+        fetch_response.addValue(
+            'http://axschema.org/namePerson/first', 'Firstname')
+        fetch_response.addValue(
+            'http://axschema.org/namePerson/last', 'Lastname')
+        fetch_response.addValue(
+            'http://axschema.org/namePerson/friendly', 'someuser')
+        openid_response.addExtension(fetch_response)
+        response = self.complete(openid_response)
+        self.assertRedirects(response, 'http://testserver/getuser/')
+
+        # And they are now logged in as testuser (the passed in
+        # nickname has not caused the username to change).
+        response = self.client.get('/getuser/')
+        self.assertEquals(response.content, 'testuser')
+
+        # The user's full name and email have been updated.
+        user = User.objects.get(username='testuser')
+        self.assertEquals(user.first_name, 'Firstname')
+        self.assertEquals(user.last_name, 'Lastname')
+        self.assertEquals(user.email, 'foo example com')
+
     def test_login_teams(self):
         settings.OPENID_LAUNCHPAD_TEAMS_MAPPING = {'teamname': 'groupname',
                                                    'otherteam': 'othergroup'}
diff --git a/lib/django_openid_auth/views.py b/lib/django_openid_auth/views.py
index cc22aed..195da01 100644
--- a/lib/django_openid_auth/views.py
+++ b/lib/django_openid_auth/views.py
@@ -44,7 +44,7 @@ from django.template.loader import render_to_string
 from openid.consumer.consumer import (
     Consumer, SUCCESS, CANCEL, FAILURE)
 from openid.consumer.discover import DiscoveryFailure
-from openid.extensions import sreg
+from openid.extensions import sreg, ax
 
 from django_openid_auth import teams
 from django_openid_auth.forms import OpenIDLoginForm
@@ -164,6 +164,19 @@ def login_begin(request, template_name='openid/login.html',
             request, "OpenID discovery error: %s" % (str(exc),), status=500)
 
     # Request some user details.
+    fetch_request = ax.FetchRequest()
+    # We mark all the attributes as required, since Google ignores
+    # optional attributes.  We request both the full name and
+    # first/last components since some providers offer one but not
+    # the other.
+    for (attr, alias) in [
+        ('http://axschema.org/contact/email', 'email'),
+        ('http://axschema.org/namePerson', 'fullname'),
+        ('http://axschema.org/namePerson/first', 'firstname'),
+        ('http://axschema.org/namePerson/last', 'lastname'),
+        ('http://axschema.org/namePerson/friendly', 'nickname')]:
+        fetch_request.add(ax.AttrInfo(attr, alias=alias, required=True))
+    openid_request.addExtension(fetch_request)
     openid_request.addExtension(
         sreg.SRegRequest(optional=['email', 'fullname', 'nickname']))
 



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]