Commit 920848a3 by James Henstridge

Add support for attribute exchange as a way to query user details.

If the endpoint claims to support attribute exchange, use that extension 
to request the details instead of simple registration.

We use the standard axschema.org attribute names, and ask for the user's 
full name in both complete and split first/last name versions.  This 
should let us support both the Google and Yahoo provider 
implementations.
parent 524454e9
......@@ -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
......@@ -80,10 +80,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 = self._extract_user_details(openid_response)
self.update_user_details_from_sreg(user, details)
teams_response = teams.TeamsResponse.fromSuccessResponse(
openid_response)
......@@ -92,14 +90,45 @@ class OpenIDBackend:
return user
def create_user_from_openid(self, openid_response):
def _extract_user_details(self, openid_response):
email = fullname = first_name = last_name = nickname = None
sreg_response = sreg.SRegResponse.fromSuccessResponse(openid_response)
if sreg_response:
nickname = sreg_response.get('nickname', 'openiduser')
email = sreg_response.get('email', '')
else:
nickname = 'openiduser'
email = ''
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
return dict(email=email, nickname=nickname,
first_name=first_name, last_name=last_name)
def create_user_from_openid(self, openid_response):
details = self._extract_user_details(openid_response)
nickname = details['nickname'] or 'openiduser'
email = details['email'] or ''
# Pick a username for the user based on their nickname,
# checking for conflicts.
......@@ -115,9 +144,7 @@ class OpenIDBackend:
i += 1
user = User.objects.create_user(username, email, password=None)
if sreg_response:
self.update_user_details_from_sreg(user, sreg_response)
self.update_user_details_from_sreg(user, details)
self.associate_openid(user, openid_response)
return user
......@@ -142,20 +169,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_from_sreg(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)
......
......@@ -34,6 +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 import ax
from openid.extensions.sreg import SRegRequest, SRegResponse
from openid.fetchers import (
HTTPFetcher, HTTPFetchingError, HTTPResponse, setDefaultFetcher)
......@@ -58,10 +59,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 +73,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 = {}
......@@ -318,6 +321,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 = 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'}
......
......@@ -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
......@@ -163,9 +163,25 @@ def login_begin(request, template_name='openid/login.html',
return render_failure(
request, "OpenID discovery error: %s" % (str(exc),), status=500)
# Request some user details.
openid_request.addExtension(
sreg.SRegRequest(optional=['email', 'fullname', 'nickname']))
# Request some user details. If the provider advertises support
# for attribute exchange, use that.
if openid_request.endpoint.supportsType(ax.AXMessage.ns_uri):
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)
else:
openid_request.addExtension(
sreg.SRegRequest(optional=['email', 'fullname', 'nickname']))
# Request team info
teams_mapping_auto = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO', False)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment