Commit d2434a6b by James Henstridge Committed by Tarmac

Add support for requesting user details via the Attribute Exchange extension. …

Add support for requesting user details via the Attribute Exchange extension.  This allows us to retrieve user details from providers that don't implement the Simple Registration extension (e.g. Google).  Fixes bug #517393.
parents 524454e9 c64f47e7
......@@ -39,8 +39,9 @@ single signon systems.
OPENID_CREATE_USERS = True
5. To have user details updated from OpenID Simple Registration data
each time they log in, add the following:
5. To have user details updated from OpenID Simple Registration or
Attribute Exchange extension data each time they log in, add the
following:
OPENID_UPDATE_DETAILS_FROM_SREG = True
......
......@@ -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(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', '')
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:
nickname = 'openiduser'
email = ''
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(user, details)
self.associate_openid(user, openid_response)
return user
......@@ -142,19 +169,19 @@ 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
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):
......
......@@ -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'}
......
......@@ -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,7 +163,23 @@ 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.
# 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']))
......
......@@ -33,7 +33,7 @@ The library integrates with Django's built in authentication system, so
most applications require minimal changes to support OpenID llogin. The
library also includes the following features:
* Basic user details are transferred from the OpenID server via the
simple Registration extension.
Simple Registration extension or Attribute Exchange extension.
* can be configured to use a fixed OpenID server URL, for use in SSO.
* supports the launchpad.net teams extension to get team membership
info.
......
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