third parties.
+--------
+Features
+--------
+This app provides user registration and login using social sites credetials,
+some features are:
+
+- Registration and Login using social sites using the following providers
+ at the moment:
+
+ * `Google OpenID`_
+ * `Yahoo OpenID`_
+ * OpenID like myOpenID_
+ * `Twitter OAuth`_
+ * `Facebook OAuth`_
+ * `Orkut OAuth`_
+
+- Basic user data population
+
+- Multiple social accounts association to single users
+
+- Custom User model override if needed (`auth.User`_ by default)
+
+
------------
Dependencies
------------
Check Django documentation at `Login url`_ and `Login redirect url`_
-- Configure complete url name to avoid possible clashes::
+- Configure authentication and association complete URL names to avoid
+ possible clashes::
- SOCIAL_AUTH_COMPLETE_URL_NAME = 'namespace:complete'
+ SOCIAL_AUTH_COMPLETE_URL_NAME = 'namespace:complete'
+ SOCIAL_AUTH_ASSOCIATE_URL_NAME = 'namespace:association_complete'
- Add urls entries::
------
OpenId
------
-OpenId support is simpler to implement than OAuth, by Google and Yahoo
+OpenId support is simpler to implement than OAuth, by Google and Yahoo
providers are supported by default, others are supported by POST method
providing endpoint Url.
------------
Attributions to whom deserves:
-- caioariede_ (Caio Ariede)
+- caioariede_ (Caio Ariede)
----------
.. _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
+.. _Google OpenID: http://code.google.com/apis/accounts/docs/OpenID.html
+.. _Yahoo OpenID: http://openid.yahoo.com/
+.. _Twitter OAuth: http://dev.twitter.com/pages/oauth_faq
+.. _Facebook OAuth: http://developers.facebook.com/docs/authentication/
+.. _Orkut OAuth: http://code.google.com/apis/orkut/docs/rest/developers_guide_protocol.html#Authenticating
+.. _myOpenID: https://www.myopenid.com/
@login_required
def done(request):
- user = request.user
return HttpResponse(Template(
"""
<html>
<tr><th>Last name:</th> <td>{{ user.last_name|default:"Not provided" }}</td></tr>
</table>
<p><a href="/logout/">Logout</a></p>
+
+ <h2>Associate new credentials:</h2>
+ <div>
+ <ul>
+ <li><a href="/associate/twitter/">Twitter</a></li>
+ <li><a href="/associate/facebook/">Facebook</a></li>
+ <li><a href="/associate/orkut/">Orkut</a></li>
+ <li><a href="/associate/google/">Google</a></li>
+ <li><a href="/associate/yahoo/">Yahoo</a></li>
+ <li>
+ <form action="/associate/openid/" method="post">{% csrf_token %}
+ <label for="openid_identifier">Other provider:</label>
+ <input id="openid_identifier" type="text" value="" name="openid_identifier" />
+ <input type="submit" />
+ </form>
+ </li>
+ </ul>
+ </div>
</body>
</html>
- """).render(Context({'user':user})),
+ """).render(RequestContext(request)),
content_type='text/html;charset=UTF-8')
url(r'^done/$', done, name='done'),
url(r'^error/$', error, name='error'),
url(r'^logout/$', logout, name='logout'),
- url(r'', include('social_auth.urls', namespace='social')),
url(r'^admin/', include(admin.site.urls)),
+ url(r'', include('social_auth.urls', namespace='social')),
)
return openid_request.htmlMarkup(trust_root, return_to,
form_tag_attrs=form_tag)
- def auth_complete(self):
+ def auth_complete(self, *args, **kwargs):
response = self.consumer().complete(dict(self.request.REQUEST.items()),
self.request.build_absolute_uri())
if not response:
raise ValueError, 'This is an OpenID relying party endpoint'
elif response.status == SUCCESS:
- return authenticate(**{'response': response,
- OpenIDBackend.name: True})
+ kwargs.update({'response': response, OpenIDBackend.name: True})
+ return authenticate(*args, **kwargs)
elif response.status == FAILURE:
raise ValueError, 'OpenID authentication failed: %s' % response.message
elif response.status == CANCEL:
self.request.session[name] = token.to_string()
return self.oauth_request(token, self.AUTHORIZATION_URL).to_url()
- def auth_complete(self):
+ def auth_complete(self, *args, **kwargs):
"""Returns user, might be logged in"""
name = self.AUTH_BACKEND.name + 'unauthorized_token_name'
unauthed_token = self.request.session.get(name)
if data is not None:
data['access_token'] = access_token.to_string()
- return authenticate(**{'response': data, self.AUTH_BACKEND.name: True})
+ kwargs.update({'response': data, self.AUTH_BACKEND.name: True})
+ return authenticate(*args, **kwargs)
def unauthorized_token(self):
"""Return request for unauthorized token (first stage)"""
'redirect_uri': self.redirect_uri}
return FACEBOOK_AUTHORIZATION_URL + '?' + urllib.urlencode(args)
- def auth_complete(self):
+ def auth_complete(self, *args, **kwargs):
"""Returns user, might be logged in"""
if 'code' in self.request.GET:
- args = {'client_id': settings.FACEBOOK_APP_ID,
- 'redirect_uri': self.redirect_uri,
- 'client_secret': settings.FACEBOOK_API_SECRET,
- 'code': self.request.GET['code']}
- url = FACEBOOK_ACCESS_TOKEN_URL + '?' + urllib.urlencode(args)
+ url = FACEBOOK_ACCESS_TOKEN_URL + '?' + \
+ urllib.urlencode({'client_id': settings.FACEBOOK_APP_ID,
+ 'redirect_uri': self.redirect_uri,
+ 'client_secret': settings.FACEBOOK_API_SECRET,
+ 'code': self.request.GET['code']})
response = cgi.parse_qs(urllib.urlopen(url).read())
-
+
access_token = response['access_token'][0]
data = self.user_data(access_token)
if data is not None:
if 'error' in data:
raise ValueError, 'Authentication error'
data['access_token'] = access_token
- return authenticate(**{'response': data,
- FacebookBackend.name: True})
+
+ kwargs.update({'response': data, FacebookBackend.name: True})
+ return authenticate(*args, **kwargs)
else:
raise ValueError, 'Authentication error'
'first_name': response.get('first_name', ''),
'last_name': response.get('last_name', '')}
-
+
class OpenIDBackend(SocialAuthBackend):
"""Generic OpenID authentication backend"""
name = 'openid'
def auth_url(self):
"""Must return redirect URL to auth provider"""
raise NotImplementedError, 'Implement in subclass'
-
+
def auth_html(self):
"""Must return login HTML content returned by provider"""
raise NotImplementedError, 'Implement in subclass'
-
- def auth_complete(self):
+
+ def auth_complete(self, *args, **kwargs):
"""Completes loging process, must return user instance"""
raise NotImplementedError, 'Implement in subclass'
a authentication provider response"""
name = '' # provider name, it's stored in database
- def authenticate(self, **kwargs):
+ def authenticate(self, *args, **kwargs):
"""Authenticate user using social credentials
Authentication is made if this is the correct backend, backend
verification is made by kwargs inspection for current backend
name presence.
"""
-
# Validate backend and arguments. Require that the OAuth response
# be passed in as a keyword argument, to make sure we don't match
# the username/password calling conventions of authenticate.
except UserSocialAuth.DoesNotExist:
if not getattr(settings, 'SOCIAL_AUTH_CREATE_USERS', False):
return None
- user = self.create_user(response, details)
+ user = self.create_user(details=details, *args, **kwargs)
else:
user = auth_user.user
self.update_user_details(user, details)
return user
-
+
def get_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
break
return username
- def create_user(self, response, details):
- """Create user with unique username"""
- 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)
-
- self.update_user_details(user, details) # load details
- self.associate_auth(user, response, details) # save account association
+ 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, details)
+ self.associate_auth(user, response, details)
return user
def associate_auth(self, user, response, details):
# Check to see if this OAuth has already been claimed.
uid = self.get_user_id(details, response)
try:
- user_oauth = UserSocialAuth.objects.select_related('user')\
- .get(provider=self.name,
- uid=uid)
+ 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_oauth = UserSocialAuth.objects.create(user=user, uid=uid,
- provider=self.name,
- extra_data=extra_data)
+ user_social = UserSocialAuth.objects.create(user=user, uid=uid,
+ provider=self.name,
+ extra_data=extra_data)
else:
- if user_oauth.user != user:
+ if user_social.user != user:
raise ValueError, 'Identity already claimed'
- return user_oauth
+ return user_social
def extra_data(self, user, uid, response, details):
"""Return default blank user extra data"""
def update_user_details(self, user, details):
"""Update user details with new (maybe) data"""
- fields = user._meta.get_all_field_names()
+ fields = (name for name in ('first_name', 'last_name', 'email')
+ if user._meta.get_field(name))
changed = False
- for name in ('first_name', 'last_name', 'email'):
+ for name in fields:
value = details.get(name)
- if name in fields and value != getattr(user, name, value):
+ if value and value != getattr(user, name, value):
setattr(user, name, value)
changed = True
"""Social auth models"""
-from django.db import models
+from django.db import models
from django.conf import settings
# If User class is overrided, it must provide the following fields:
"""URLs module"""
from django.conf.urls.defaults import patterns, url
-from .views import auth, complete
+from .views import auth, complete, associate, associate_complete
urlpatterns = patterns('',
- url(r'^login/(?P<backend>[^/]+)/$', auth, name='begin'),
- url(r'^complete/(?P<backend>[^/]+)/$', complete, name='complete'),
+ url(r'^login/(?P<backend>[^/]+)/$', auth, name='begin'),
+ url(r'^complete/(?P<backend>[^/]+)/$', complete, name='complete'),
+ url(r'^associate/(?P<backend>[^/]+)/$', associate, name='associate_begin'),
+ url(r'^associate/complete/(?P<backend>[^/]+)/$', associate_complete,
+ name='associate_complete'),
)
HttpResponseServerError
from django.core.urlresolvers import reverse
from django.contrib.auth import login, REDIRECT_FIELD_NAME
+from django.contrib.auth.decorators import login_required
-from .auth import TwitterAuth, FacebookAuth, OpenIdAuth, GoogleAuth, YahooAuth, OrkutAuth
+from .auth import TwitterAuth, FacebookAuth, OpenIdAuth, GoogleAuth, \
+ YahooAuth, OrkutAuth
-# Authenticatin backends
+# Authentication backends
BACKENDS = {
'twitter': TwitterAuth,
'facebook': FacebookAuth,
'orkut': OrkutAuth,
}
-def auth(request, backend):
- """Authentication starting process"""
- if backend not in BACKENDS:
- return HttpResponseServerError('Incorrect authentication service')
- request.session[REDIRECT_FIELD_NAME] = request.GET.get(REDIRECT_FIELD_NAME,
- settings.LOGIN_REDIRECT_URL)
- redirect = reverse(getattr(settings, 'SOCIAL_AUTH_COMPLETE_URL_NAME',
- 'complete'),
- args=(backend,))
- backend = BACKENDS[backend](request, redirect)
- if backend.uses_redirect:
- return HttpResponseRedirect(backend.auth_url())
- else:
- return HttpResponse(backend.auth_html(),
- content_type='text/html;charset=UTF-8')
+def auth(request, backend):
+ """Start authentication process"""
+ complete = getattr(settings, 'SOCIAL_AUTH_COMPLETE_URL_NAME', 'complete')
+ redirect = getattr(settings, 'LOGIN_REDIRECT_URL', '')
+ return auth_process(request, backend, complete, redirect)
def complete(request, backend):
user = backend.auth_complete()
if user and user.is_active:
login(request, user)
- url = request.session.pop(REDIRECT_FIELD_NAME,
- settings.LOGIN_REDIRECT_URL)
+ url = request.session.pop(REDIRECT_FIELD_NAME, '') or \
+ getattr(settings, 'LOGIN_REDIRECT_URL', '')
else:
url = getattr(settings, 'LOGIN_ERROR_URL', settings.LOGIN_URL)
return HttpResponseRedirect(url)
+
+
+@login_required
+def associate(request, backend):
+ """Authentication starting process"""
+ complete = getattr(settings, 'SOCIAL_AUTH_ASSOCIATE_URL_NAME',
+ 'associate_complete')
+ redirect = getattr(settings, 'LOGIN_REDIRECT_URL', '')
+ return auth_process(request, backend, complete, redirect)
+
+
+@login_required
+def associate_complete(request, backend):
+ """Authentication complete process"""
+ if backend not in BACKENDS:
+ return HttpResponseServerError('Incorrect authentication service')
+ backend = BACKENDS[backend](request, request.path)
+ user = backend.auth_complete(user=request.user)
+ url = request.session.pop(REDIRECT_FIELD_NAME, '') or \
+ getattr(settings, 'LOGIN_REDIRECT_URL', '')
+ return HttpResponseRedirect(url)
+
+
+def auth_process(request, backend, complete_url_name, default_final_url):
+ """Authenticate using social backend"""
+ if backend not in BACKENDS:
+ return HttpResponseServerError('Incorrect authentication service')
+ request.session[REDIRECT_FIELD_NAME] = request.GET.get(REDIRECT_FIELD_NAME,
+ default_final_url)
+ redirect = reverse(complete_url_name, args=(backend,))
+ backend = BACKENDS[backend](request, redirect)
+ if backend.uses_redirect:
+ return HttpResponseRedirect(backend.auth_url())
+ else:
+ return HttpResponse(backend.auth_html(),
+ content_type='text/html;charset=UTF-8')