diff --git a/README.rst b/README.rst
index 873b03c9c..80c08e0ba 100644
--- a/README.rst
+++ b/README.rst
@@ -13,8 +13,8 @@ third parties.
--------
Features
--------
-This app provides user registration and login using social sites credentials,
-some features are:
+This application provides user registration and login using social sites
+credentials, some features are:
- Registration and Login using social sites using the following providers
at the moment:
@@ -50,7 +50,7 @@ Dependencies that must be meet to use the app:
------------
Installation
------------
-- Add social_auth app to PYTHONPATH and installed apps::
+- Add social_auth to PYTHONPATH and installed applications::
INSTALLED_APPS = (
...
@@ -63,6 +63,8 @@ Installation
'social_auth.backends.TwitterOAuthBackend',
'social_auth.backends.FacebookOAuthBackend',
'social_auth.backends.OrkutOAuthBackend',
+ 'social_auth.backends.GoogleOpenIDBackend',
+ 'social_auth.backends.YahooOpenIDBackend',
'social_auth.backends.OpenIDBackend',
'django.contrib.auth.backends.ModelBackend',
)
@@ -76,13 +78,13 @@ Installation
ORKUT_CONSUMER_KEY = ''
ORKUT_CONSUMER_SECRET = ''
-- Setup login urls::
+- Setup login URLs::
LOGIN_URL = '/login-form/'
LOGIN_REDIRECT_URL = '/logged-in/'
LOGIN_ERROR_URL = '/login-error/'
- Check Django documentation at `Login url`_ and `Login redirect url`_
+ Check Django documentation at `Login URL`_ and `Login redirect URL`_
- Configure authentication and association complete URL names to avoid
possible clashes::
@@ -90,7 +92,7 @@ Installation
SOCIAL_AUTH_COMPLETE_URL_NAME = 'namespace:complete'
SOCIAL_AUTH_ASSOCIATE_URL_NAME = 'namespace:association_complete'
-- Add urls entries::
+- Add URLs entries::
urlpatterns = patterns('',
...
@@ -118,40 +120,37 @@ Installation
SOCIAL_AUTH_EXTRA_DATA = False
-- It's possible to override the used User class if needed::
+- It's possible to override the used User model if needed::
SOCIAL_AUTH_USER_MODEL = 'myapp.CustomUser'
- this class must define the following fields::
+ This class *must* have a custom `Model Manager`_ with a create_user method
+ that resembles the one on `auth.UserManager`_.
+
+ Also, it's highly recommended that this class define the following fields::
username = CharField(...)
- email = EmailField(...)
- password = CharField(...)
last_login = DateTimeField(blank=True)
is_active = BooleanField(...)
- and the methods::
-
- is_authenticated()
-
- These are needed to ensure django-auth integration. AttributeError is raised
- if any of these are missing.
+ and the method::
- Also the following are recommended but not enforced::
+ is_authenticated():
+ ...
- first_name = CharField(...)
- last_name = CharField(...)
+ These are needed to ensure a better django-auth integration, in other case
+ `login_required`_ won't be usable. A warning is displayed if any of these are
+ missing. By default `auth.User`_ is used.
- By default `auth.User`_ is used. Check example application for
- implementation details, but first, please take a look to `User Profiles`_,
- it might solve your case.
+ Check example application for implementation details, but first, please take
+ a look to `User Profiles`_, it might be what you were looking for.
-------
Signals
-------
A pre_update signal is sent when user data is about to be updated with new
-values from auth service provider, this apply to new users and already
+values from authorization service provider, this apply to new users and already
existent ones. This is useful to update custom user fields or `User Profiles`_,
for example, to store user gender, location, etc. Example::
@@ -190,7 +189,7 @@ Twitter offers per application keys named "Consumer Key" and
"Consumer Secret". To enable Twitter these two keys are needed.
Further documentation at `Twitter development resources`_:
-- Register a new app at `Twitter App Creation`_,
+- Register a new application at `Twitter App Creation`_,
- mark the "Yes, use Twitter for login" checkbox, and
@@ -199,7 +198,7 @@ Further documentation at `Twitter development resources`_:
TWITTER_CONSUMER_KEY
TWITTER_CONSUMER_SECRET
-- You don't need to specify the url callback
+- You don't need to specify the URL callback
--------
@@ -209,7 +208,7 @@ Facebook works similar to Twitter but it's simpler to setup and
redirect URL is passed as a parameter when issuing an authorization.
Further documentation at `Facebook development resources`_:
-- Register a new app at `Facebook App Creation`_, and
+- Register a new application at `Facebook App Creation`_, and
- fill "App Id" and "App Secret" values in values::
@@ -262,22 +261,25 @@ Base work is copyrighted by:
Copyright (C) 2007 Simon Willison
Copyright (C) 2008-2010 Canonical Ltd.
+.. _Model Manager: http://docs.djangoproject.com/en/dev/topics/db/managers/#managers
+.. _Login URL: http://docs.djangoproject.com/en/dev/ref/settings/?from=olddocs#login-url
+.. _Login redirect URL: http://docs.djangoproject.com/en/dev/ref/settings/?from=olddocs#login-redirect-url
+.. _AUTHENTICATION_BACKENDS: http://docs.djangoproject.com/en/dev/ref/settings/?from=olddocs#authentication-backends
+.. _auth.User: http://code.djangoproject.com/browser/django/trunk/django/contrib/auth/models.py#L186
+.. _auth.UserManager: http://code.djangoproject.com/browser/django/trunk/django/contrib/auth/models.py#L114
+.. _login_required: http://code.djangoproject.com/browser/django/trunk/django/contrib/auth/decorators.py#L39
+.. _User Profiles: http://www.djangobook.com/en/1.0/chapter12/#cn222
.. _OpenId: http://openid.net/
.. _OAuth: http://oauth.net/
.. _django-twitter-oauth: https://github.com/henriklied/django-twitter-oauth
.. _django-openid-auth: https://launchpad.net/django-openid-auth
.. _python-openid: http://pypi.python.org/pypi/python-openid/
.. _python-oauth: https://github.com/leah/python-oauth
-.. _Login url: http://docs.djangoproject.com/en/dev/ref/settings/?from=olddocs#login-url
-.. _Login redirect url: http://docs.djangoproject.com/en/dev/ref/settings/?from=olddocs#login-redirect-url
.. _Twitter development resources: http://dev.twitter.com/pages/auth
.. _Twitter App Creation: http://twitter.com/apps/new
.. _dnsmasq: http://www.thekelleys.org.uk/dnsmasq/doc.html
.. _Facebook development resources: http://developers.facebook.com/docs/authentication/
.. _Facebook App Creation: http://developers.facebook.com/setup/
-.. _AUTHENTICATION_BACKENDS: http://docs.djangoproject.com/en/dev/ref/settings/?from=olddocs#authentication-backends
-.. _auth.User: http://code.djangoproject.com/browser/django/trunk/django/contrib/auth/models.py#L186
-.. _User Profiles: http://www.djangobook.com/en/1.0/chapter12/#cn222
.. _caioariede: https://github.com/caioariede
.. _Google support: http://www.google.com/support/a/bin/answer.py?hl=en&answer=162105
.. _Orkut API: http://code.google.com/apis/orkut/docs/rest/developers_guide_protocol.html#Authenticating
diff --git a/example/app/models.py b/example/app/models.py
index 45ecf2435..8c28f48f6 100644
--- a/example/app/models.py
+++ b/example/app/models.py
@@ -1,15 +1,17 @@
# Define a custom User class to work with django-social-auth
-#
-# from django.db import models
-#
-# class CustomUser(models.Model):
-# username = models.CharField(max_length=128)
-# email = models.EmailField()
-# password = models.CharField(max_length=128)
-# last_login = models.DateTimeField(blank=True, null=True)
-# first_name = models.CharField(max_length=128, blank=True)
-# last_name = models.CharField(max_length=128, blank=True)
-# is_active = models.BooleanField(default=True)
-#
-# def is_authenticated(self):
-# return True
+from django.db import models
+
+
+class CustomUserManager(models.Manager):
+ def create_user(self, username, email):
+ return self.model._default_manager.create(username=username)
+
+
+class CustomUser(models.Model):
+ username = models.CharField(max_length=128)
+ last_login = models.DateTimeField(blank=True, null=True)
+
+ objects = CustomUserManager()
+
+ def is_authenticated(self):
+ return True
diff --git a/example/app/views.py b/example/app/views.py
index 57cf0517b..690c06954 100644
--- a/example/app/views.py
+++ b/example/app/views.py
@@ -5,6 +5,8 @@
def home(request):
+ if request.user.is_authenticated():
+ return HttpResponseRedirect('done')
return HttpResponse(Template(
"""
@@ -44,6 +46,7 @@ def home(request):
@login_required
def done(request):
+ names = request.user.social_auth.values_list('provider', flat=True)
return HttpResponse(Template(
"""
@@ -65,11 +68,11 @@ def done(request):
Associate new credentials:
- """).render(RequestContext(request)),
+ """).render(RequestContext(request, dict((name.lower(), True)
+ for name in names))),
content_type='text/html;charset=UTF-8')
diff --git a/example/settings.py b/example/settings.py
index c99965eb0..efb2b2cdb 100644
--- a/example/settings.py
+++ b/example/settings.py
@@ -95,6 +95,8 @@
AUTHENTICATION_BACKENDS = (
'social_auth.backends.TwitterBackend',
'social_auth.backends.FacebookBackend',
+ 'social_auth.backends.GoogleBackend',
+ 'social_auth.backends.YahooBackend',
'social_auth.backends.OpenIDBackend',
'django.contrib.auth.backends.ModelBackend',
)
diff --git a/social_auth/auth.py b/social_auth/auth.py
index c3ea7a977..f91d55d19 100644
--- a/social_auth/auth.py
+++ b/social_auth/auth.py
@@ -15,7 +15,7 @@
from .store import DjangoOpenIDStore
from .backends import TwitterBackend, OrkutBackend, FacebookBackend, \
- OpenIDBackend
+ OpenIDBackend, GoogleBackend, YahooBackend
from .conf import AX_ATTRS, SREG_ATTR, OPENID_ID_FIELD, SESSION_NAME, \
OPENID_GOOGLE_URL, OPENID_YAHOO_URL, TWITTER_SERVER, \
TWITTER_REQUEST_TOKEN_URL, TWITTER_ACCESS_TOKEN_URL, \
@@ -54,7 +54,12 @@ def uses_redirect(self):
class OpenIdAuth(BaseAuth):
- """OpenId process handling"""
+ """
+ OpenId process handling
+ @AUTH_BACKEND Authorization backend related with this service
+ """
+ AUTH_BACKEND = OpenIDBackend
+
def auth_url(self):
openid_request = self.setup_request()
# Construct completion URL, including page we should redirect to
@@ -78,7 +83,7 @@ def auth_complete(self, *args, **kwargs):
if not response:
raise ValueError, 'This is an OpenID relying party endpoint'
elif response.status == SUCCESS:
- kwargs.update({'response': response, OpenIDBackend.name: True})
+ kwargs.update({'response': response, self.AUTH_BACKEND.name: True})
return authenticate(*args, **kwargs)
elif response.status == FAILURE:
raise ValueError, 'OpenID authentication failed: %s' % response.message
@@ -140,6 +145,8 @@ def openid_url(self):
class GoogleAuth(OpenIdAuth):
"""Google OpenID authentication"""
+ AUTH_BACKEND = GoogleBackend
+
def openid_url(self):
"""Return Google OpenID service url"""
return OPENID_GOOGLE_URL
@@ -147,6 +154,8 @@ def openid_url(self):
class YahooAuth(OpenIdAuth):
"""Yahoo OpenID authentication"""
+ AUTH_BACKEND = YahooBackend
+
def openid_url(self):
"""Return Yahoo OpenID service url"""
return OPENID_YAHOO_URL
diff --git a/social_auth/backends.py b/social_auth/backends.py
index c921aaf15..f4ae7dde6 100644
--- a/social_auth/backends.py
+++ b/social_auth/backends.py
@@ -7,12 +7,13 @@
from django.conf import settings
from django.contrib.auth.backends import ModelBackend
-from django.contrib.auth.models import UNUSABLE_PASSWORD
from .models import UserSocialAuth
from .conf import OLD_AX_ATTRS, AX_SCHEMA_ATTRS
from .signals import pre_update
+USERNAME = 'username'
+
# get User class, could not be auth.User
User = UserSocialAuth._meta.get_field('user').rel.to
@@ -38,20 +39,28 @@ def authenticate(self, *args, **kwargs):
response = kwargs.get('response')
details = self.get_user_details(response)
uid = self.get_user_id(details, response)
+ new_user = False
try:
- auth_user = UserSocialAuth.objects.select_related('user')\
- .get(provider=self.name,
- uid=uid)
+ social_user = UserSocialAuth.objects.select_related('user')\
+ .get(provider=self.name,
+ uid=uid)
except UserSocialAuth.DoesNotExist:
- if not getattr(settings, 'SOCIAL_AUTH_CREATE_USERS', False):
- return None
- user = self.create_user(details=details, *args, **kwargs)
+ user = kwargs.get('user')
+ if user is None: # new user
+ if not getattr(settings, 'SOCIAL_AUTH_CREATE_USERS', True):
+ return None
+ username = self.username(details)
+ email = details.get('email')
+ user = User.objects.create_user(username=username, email=email)
+ new_user = True
+ social_user = self.associate_auth(user, uid, response, details)
else:
- user = auth_user.user
- self.update_user_details(user, response, details)
+ user = social_user.user
+
+ self.update_user_details(user, response, details, new_user=new_user)
return user
- def get_username(self, details):
+ def username(self, details):
"""Return an unique username, if SOCIAL_AUTH_FORCE_RANDOM_USERNAME
setting is True, then username will be a random 30 chars md5 hash
"""
@@ -61,8 +70,8 @@ def get_random_username():
if getattr(settings, 'SOCIAL_AUTH_FORCE_RANDOM_USERNAME', False):
username = get_random_username()
- elif 'username' in details:
- username = details['username']
+ elif USERNAME in details:
+ username = details[USERNAME]
elif hasattr(settings, 'SOCIAL_AUTH_DEFAULT_USERNAME'):
username = settings.SOCIAL_AUTH_DEFAULT_USERNAME
if callable(username):
@@ -81,54 +90,27 @@ def get_random_username():
break
return username
- def create_user(self, response, details, *args, **kwargs):
- """Create user with unique username. New social credentials are
- associated with @user if this parameter is not None."""
- user = kwargs.get('user')
- if user is None: # create user, otherwise associate the new credential
- username = self.get_username(details)
- email = details.get('email', '')
-
- if hasattr(User.objects, 'create_user'): # auth.User
- user = User.objects.create_user(username, email)
- else: # create user setting password to an unusable value
- user = User.objects.create(username=username, email=email,
- password=UNUSABLE_PASSWORD)
-
- # update details and associate account with social credentials
- self.update_user_details(user, response, details)
- self.associate_auth(user, response, details)
- return user
-
- def associate_auth(self, user, response, details):
- """Associate an OAuth with a user account."""
- # Check to see if this OAuth has already been claimed.
- uid = self.get_user_id(details, response)
- try:
- user_social = UserSocialAuth.objects.select_related('user')\
- .get(provider=self.name,
- uid=uid)
- except UserSocialAuth.DoesNotExist:
- if getattr(settings, 'SOCIAL_AUTH_EXTRA_DATA', True):
- extra_data = self.extra_data(user, uid, response, details)
- else:
- extra_data = ''
- user_social = UserSocialAuth.objects.create(user=user, uid=uid,
- provider=self.name,
- extra_data=extra_data)
- else:
- if user_social.user != user:
- raise ValueError, 'Identity already claimed'
- return user_social
+ def associate_auth(self, user, uid, response, details):
+ """Associate a Social Auth with an user account."""
+ extra_data = '' if not getattr(settings, 'SOCIAL_AUTH_EXTRA_DATA',
+ False) \
+ else self.extra_data(user, uid, response, details)
+ return UserSocialAuth.objects.create(user=user, uid=uid,
+ provider=self.name,
+ extra_data=extra_data)
def extra_data(self, user, uid, response, details):
"""Return default blank user extra data"""
return ''
- def update_user_details(self, user, response, details):
- """Update user details with new (maybe) data"""
+ def update_user_details(self, user, response, details, new_user=False):
+ """Update user details with (maybe) new data. Username is not
+ changed if associating a new credential."""
changed = False
for name, value in details.iteritems():
+ # not update username if user already exists
+ if not new_user and name == USERNAME:
+ continue
if value and value != getattr(user, name, value):
setattr(user, name, value)
changed = True
@@ -137,7 +119,7 @@ def update_user_details(self, user, response, details):
# user instance (created or retrieved from database), service
# response and processed details, signal handlers must return
# True or False to signal that something has changed
- updated = filter(bool, pre_update.send(sender=self, user=user,
+ updated = filter(None, pre_update.send(sender=self, user=user,
response=response,
details=details))
if changed or len(updated) > 0:
@@ -149,8 +131,8 @@ def get_user_id(self, details, response):
def get_user_details(self, response):
"""Must return user details in a know internal struct:
- {'email': ,
- 'username': ,
+ {USERNAME: ,
+ 'email': ,
'fullname': ,
'first_name': ,
'last_name': }
@@ -182,8 +164,8 @@ class TwitterBackend(OAuthBackend):
def get_user_details(self, response):
"""Return user details from Twitter account"""
- return {'email': '', # not supplied
- 'username': response['screen_name'],
+ return {USERNAME: response['screen_name'],
+ 'email': '', # not supplied
'fullname': response['name'],
'first_name': response['name'],
'last_name': ''}
@@ -195,8 +177,8 @@ class OrkutBackend(OAuthBackend):
def get_user_details(self, response):
"""Return user details from Orkut account"""
- return {'email': response['emails'][0]['value'],
- 'username': response['displayName'],
+ return {USERNAME: response['displayName'],
+ 'email': response['emails'][0]['value'],
'fullname': response['displayName'],
'firstname': response['name']['givenName'],
'lastname': response['name']['familyName']}
@@ -208,8 +190,8 @@ class FacebookBackend(OAuthBackend):
def get_user_details(self, response):
"""Return user details from Facebook account"""
- return {'email': response.get('email', ''),
- 'username': response['name'],
+ return {USERNAME: response['name'],
+ 'email': response.get('email', ''),
'fullname': response['name'],
'first_name': response.get('first_name', ''),
'last_name': response.get('last_name', '')}
@@ -225,11 +207,8 @@ def get_user_id(self, details, response):
def get_user_details(self, response):
"""Return user details from an OpenID request"""
- values = {'email': '',
- 'username': '',
- 'fullname': '',
- 'first_name': '',
- 'last_name': ''}
+ values = {USERNAME: '', 'email': '', 'fullname': '',
+ 'first_name': '', 'last_name': ''}
resp = sreg.SRegResponse.fromSuccessResponse(response)
if resp:
@@ -254,9 +233,17 @@ def get_user_details(self, response):
except ValueError:
last_name = fullname
- values.update({'fullname': fullname,
- 'first_name': first_name,
+ values.update({'fullname': fullname, 'first_name': first_name,
'last_name': last_name,
- 'username': values.get('username') or \
+ USERNAME: values.get(USERNAME) or \
(first_name.title() + last_name.title())})
return values
+
+
+class GoogleBackend(OpenIDBackend):
+ """Google OpenID authentication backend"""
+ name = 'google'
+
+class YahooBackend(OpenIDBackend):
+ """Yahoo OpenID authentication backend"""
+ name = 'yahoo'
diff --git a/social_auth/models.py b/social_auth/models.py
index 8ef3a96f5..2008dafbf 100644
--- a/social_auth/models.py
+++ b/social_auth/models.py
@@ -1,30 +1,33 @@
"""Social auth models"""
+import warnings
+
from django.db import models
from django.conf import settings
# If User class is overrided, it must provide the following fields,
# or it won't be playing nicely with auth module:
#
-# username = CharField()
-# email = EmailField()
-# password = CharField()
+# username = CharField()
+# last_login = DateTimeField()
# is_active = BooleanField()
#
# and methods:
#
# def is_authenticated():
# ...
-MANDATORY_FIELDS = ('username', 'email', 'password', 'last_login')
-MANDATORY_METHODS = ('is_authenticated',)
+RECOMMENDED_FIELDS = ('username', 'last_login', 'is_active')
+RECOMMENDED_METHODS = ('is_authenticated',)
-try: # try to import User model override and validate needed fields
+if getattr(settings, 'SOCIAL_AUTH_USER_MODEL', None):
User = models.get_model(*settings.SOCIAL_AUTH_USER_MODEL.split('.'))
- if not all(User._meta.get_field(name) for name in MANDATORY_FIELDS):
- raise AttributeError, 'Some mandatory field missing'
- if not all(callable(getattr(User, name, None))
- for name in MANDATORY_METHODS):
- raise AttributeError, 'Some mandatory methods missing'
-except AttributeError: # fail silently on missing setting
+ missing = list(set(RECOMMENDED_FIELDS) -
+ set(User._meta.get_all_field_names())) + \
+ [name for name in RECOMMENDED_METHODS
+ if not callable(getattr(User, name, None))]
+ if missing:
+ warnings.warn('Missing recommended attributes or methods '\
+ 'in custom User model: "%s"' % ', '.join(missing))
+else:
from django.contrib.auth.models import User
@@ -35,6 +38,10 @@ class UserSocialAuth(models.Model):
uid = models.CharField(max_length=255)
extra_data = models.TextField(default='', blank=True)
+ def __unicode__(self):
+ """Return associated user unicode representation"""
+ return unicode(self.user)
+
class Meta:
"""Meta data"""
unique_together = ('provider', 'uid')
diff --git a/social_auth/views.py b/social_auth/views.py
index bb476891b..c0e8500d1 100644
--- a/social_auth/views.py
+++ b/social_auth/views.py
@@ -34,7 +34,7 @@ def complete(request, backend):
return HttpResponseServerError('Incorrect authentication service')
backend = BACKENDS[backend](request, request.path)
user = backend.auth_complete()
- if user and user.is_active:
+ if user and getattr(user, 'is_active', True):
login(request, user)
url = request.session.pop(REDIRECT_FIELD_NAME, '') or \
getattr(settings, 'LOGIN_REDIRECT_URL', '')
@@ -58,7 +58,7 @@ def associate_complete(request, backend):
if backend not in BACKENDS:
return HttpResponseServerError('Incorrect authentication service')
backend = BACKENDS[backend](request, request.path)
- user = backend.auth_complete(user=request.user)
+ backend.auth_complete(user=request.user)
url = request.session.pop(REDIRECT_FIELD_NAME, '') or \
getattr(settings, 'LOGIN_REDIRECT_URL', '')
return HttpResponseRedirect(url)