Defaults to ``LOGIN_ERROR_URL``.
-- The app catches any exception and logs errors to ``logger`` or
- ``django.contrib.messagess`` app. Having tracebacks is really useful when
- debugging, for that purpose this setting was defined::
+- The application catches any exception and logs errors to ``logger`` or
+ ``django.contrib.messagess`` application by default. But it's possible to
+ override the default behavior by defining a function to process the
+ exceptions using this setting::
+
+ SOCIAL_AUTH_PROCESS_EXCEPTIONS = 'social_auth.utils.process_exceptions'
+
+ The function parameters will ``request`` holding the current request object,
+ ``backend`` with the current backend and ``err`` which is the exception
+ instance.
+
+ Recently this set of exceptions were introduce to describe the situations
+ a bit more than the old ``ValueError`` usually raised::
+
+ AuthException - Base exception class
+ AuthFailed - Authentication failed for some reason
+ AuthCanceled - Authentication was canceled by the user
+ AuthUnknownError - An unknown error stoped the authentication
+ process
+ AuthTokenError - Unauthorized or access token error, it was
+ invalid, impossible to authenticate or user
+ removed permissions to it.
+ AuthMissingParameter - A needed parameter to continue the process was
+ missing, usually raised by the services that
+ need some POST data like myOpenID
+
+ These are a subclass of ``ValueError`` to keep backward compatibility.
+
+ Having tracebacks is really useful when debugging, for that purpose this
+ setting was defined::
SOCIAL_AUTH_RAISE_EXCEPTIONS = DEBUG
Testing
-------
-To test the app just run::
+To test the application just run::
./manage.py test social_auth
-------------
Join to `django-social-auth discussion list`_ and bring any questions or suggestions
-that would improve this app. Convore_ discussion group is deprecated since the
-service is going to be shut down on April 1st.
+that would improve this application. Convore_ discussion group is deprecated since
+the service is going to be shut down on April 1st.
If defining a custom user model, do not import social_auth from any models.py
that would finally import from the models.py that defines your User class or it
Defaults to ``LOGIN_ERROR_URL``.
-- The app catches any exception and logs errors to ``logger`` or
- ``django.contrib.messagess`` app. Having tracebacks is really useful when
- debugging, for that purpose this setting was defined::
+- The application catches any exception and logs errors to ``logger`` or
+ ``django.contrib.messagess`` application by default. But it's possible to
+ override the default behavior by defining a function to process the
+ exceptions using this setting::
+
+ SOCIAL_AUTH_PROCESS_EXCEPTIONS = 'social_auth.utils.process_exceptions'
+
+ The function parameters will ``request`` holding the current request object,
+ ``backend`` with the current backend and ``err`` which is the exception
+ instance.
+
+ Recently this set of exceptions were introduce to describe the situations
+ a bit more than the old ``ValueError`` usually raised::
+
+ AuthException - Base exception class
+ AuthFailed - Authentication failed for some reason
+ AuthCanceled - Authentication was canceled by the user
+ AuthUnknownError - An unknown error stoped the authentication
+ process
+ AuthTokenError - Unauthorized or access token error, it was
+ invalid, impossible to authenticate or user
+ removed permissions to it.
+ AuthMissingParameter - A needed parameter to continue the process was
+ missing, usually raised by the services that
+ need some POST data like myOpenID
+
+ These are a subclass of ``ValueError`` to keep backward compatibility.
+
+ Having tracebacks is really useful when debugging, for that purpose this
+ setting was defined::
SOCIAL_AUTH_RAISE_EXCEPTIONS = DEBUG
It's default value is ``DEBUG``, so you need to set it to ``False`` to avoid
tracebacks when ``DEBUG = True``.
+
Some settings can be tweak by backend by adding the backend name prefix (all
uppercase and replace ``-`` with ``_``), here's the supported settings so far::
=============
Join to `django-social-auth discussion list`_ and bring any questions or suggestions
-that would improve this app. Convore_ discussion group is deprecated since the
-service is going to be shut down on April 1st.
+that would improve this application. Convore_ discussion group is deprecated since
+the service is going to be shut down on April 1st.
South_ users should add this rule to enable migrations::
Django-social-auth aims to be a fully tested project, some partial test are
present at the moment and others are being worked.
-To test the app just run::
+To test the application just run::
./manage.py test social_auth
from social_auth.utils import setting, log, model_to_ctype, ctype_to_model, \
clean_partial_pipeline
from social_auth.store import DjangoOpenIDStore
-from social_auth.backends.exceptions import StopPipeline
+from social_auth.backends.exceptions import StopPipeline, AuthException, \
+ AuthFailed, AuthCanceled, \
+ AuthUnknownError, AuthTokenError, \
+ AuthMissingParameter
if setting('SOCIAL_AUTH_USER_MODEL'):
response = self.consumer().complete(dict(self.data.items()),
self.request.build_absolute_uri())
if not response:
- raise ValueError('This is an OpenID relying party endpoint')
+ raise AuthException(self, 'OpenID relying party endpoint')
elif response.status == SUCCESS:
kwargs.update({
'auth': self,
})
return authenticate(*args, **kwargs)
elif response.status == FAILURE:
- raise ValueError('OpenID authentication failed: %s' % \
- response.message)
+ raise AuthFailed(self, response.message)
elif response.status == CANCEL:
- raise ValueError('Authentication cancelled')
+ raise AuthCanceled(self)
else:
- raise ValueError('Unknown OpenID response type: %r' % \
- response.status)
+ raise AuthUnknownError(self, response.status)
def setup_request(self, extra_params=None):
"""Setup request"""
try:
return self.consumer().begin(openid_url)
except DiscoveryFailure, err:
- raise ValueError('OpenID discovery error: %s' % err)
+ raise AuthException(self, 'OpenID discovery error: %s' % err)
def openid_url(self):
"""Return service provider URL.
This base class is generic accepting a POST parameter that specifies
provider URL."""
if OPENID_ID_FIELD not in self.data:
- raise ValueError('Missing openid identifier')
+ raise AuthMissingParameter(self, OPENID_ID_FIELD)
return self.data[OPENID_ID_FIELD]
name = self.AUTH_BACKEND.name + 'unauthorized_token_name'
unauthed_token = self.request.session.get(name)
if not unauthed_token:
- raise ValueError('Missing unauthorized token')
+ raise AuthTokenError('Missing unauthorized token')
token = Token.from_string(unauthed_token)
if token.key != self.data.get('oauth_token', 'no-token'):
- raise ValueError('Incorrect tokens')
+ raise AuthTokenError('Incorrect tokens')
try:
access_token = self.access_token(token)
except HTTPError, e:
if e.code == 400:
- raise ValueError('User denied access')
+ raise AuthCanceled(self)
else:
raise
"""Completes loging process, must return user instance"""
if self.data.get('error'):
error = self.data.get('error_description') or self.data['error']
- raise ValueError('OAuth2 authentication failed: %s' % error)
+ raise AuthFailed(self, error)
client_id, client_secret = self.get_key_and_secret()
params = {'grant_type': 'authorization_code', # request auth code
try:
response = simplejson.loads(urlopen(request).read())
except (ValueError, KeyError):
- raise ValueError('Unknown OAuth2 response type')
+ raise AuthUnknownError(self)
if response.get('error'):
error = response.get('error_description') or response.get('error')
- raise ValueError('OAuth2 authentication failed: %s' % error)
+ raise AuthFailed(self, error)
else:
response.update(self.user_data(response['access_token']) or {})
kwargs.update({
from social_auth.backends import SocialAuthBackend, BaseAuth, USERNAME
from social_auth.utils import log, setting
+from social_auth.backends.exceptions import AuthFailed, AuthMissingParameter
# BrowserID verification server
def auth_complete(self, *args, **kwargs):
"""Completes loging process, must return user instance"""
if not 'assertion' in self.data:
- raise ValueError('Missing assertion parameter')
+ raise AuthMissingParameter(self, 'assertion')
data = urlencode({
'assertion': self.data['assertion'],
else:
if response.get('status') == 'failure':
log('debug', 'Authentication failed.')
- raise ValueError('Authentication failed')
+ raise AuthFailed(self)
kwargs.update({
'auth': self,
from django.core.urlresolvers import reverse
from social_auth.backends import SocialAuthBackend, BaseAuth, USERNAME
+from social_auth.backends.exceptions import AuthException
class GAEBackend(SocialAuthBackend):
def auth_complete(self, *args, **kwargs):
"""Completes login process, must return user instance."""
if not users.get_current_user():
- raise ValueError('Authentication error')
+ raise AuthException('Authentication error')
# Setting these two are necessary for BaseAuth.authenticate to work
kwargs.update({
field, check OAuthBackend class for details on how to extend it.
"""
import cgi
-import urllib
+from urllib import urlencode, urlopen
from django.utils import simplejson
from django.contrib.auth import authenticate
from social_auth.utils import setting
from social_auth.backends import BaseOAuth, OAuthBackend, USERNAME
+from social_auth.backends.exceptions import AuthFailed
# GitHub configuration
-GITHUB_SERVER = 'github.com'
-GITHUB_AUTHORIZATION_URL = 'https://%s/login/oauth/authorize' % GITHUB_SERVER
-GITHUB_ACCESS_TOKEN_URL = 'https://%s/login/oauth/access_token' % GITHUB_SERVER
-GITHUB_API_URL = 'https://api.%s' % GITHUB_SERVER
+GITHUB_AUTHORIZATION_URL = 'https://github.com/login/oauth/authorize?'
+GITHUB_ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token?'
+GITHUB_USER_DATA_URL = 'https://api.github.com/user?'
class GithubBackend(OAuthBackend):
def auth_url(self):
"""Returns redirect url"""
- args = {'client_id': setting('GITHUB_APP_ID'),
- 'redirect_uri': self.redirect_uri}
+ args = {
+ 'client_id': setting('GITHUB_APP_ID'),
+ 'redirect_uri': self.redirect_uri
+ }
if setting('GITHUB_EXTENDED_PERMISSIONS'):
args['scope'] = ','.join(setting('GITHUB_EXTENDED_PERMISSIONS'))
args.update(self.auth_extra_arguments())
- return GITHUB_AUTHORIZATION_URL + '?' + urllib.urlencode(args)
+ return GITHUB_AUTHORIZATION_URL + urlencode(args)
def auth_complete(self, *args, **kwargs):
"""Returns user, might be logged in"""
- if 'code' in self.data:
- url = GITHUB_ACCESS_TOKEN_URL + '?' + urllib.urlencode({
- 'client_id': setting('GITHUB_APP_ID'),
- 'redirect_uri': self.redirect_uri,
- 'client_secret': setting('GITHUB_API_SECRET'),
- 'code': self.data['code']
- })
- response = cgi.parse_qs(urllib.urlopen(url).read())
- if response.get('error'):
- error = self.data.get('error') or 'unknown error'
- raise ValueError('Authentication error: %s' % error)
- access_token = response['access_token'][0]
- data = self.user_data(access_token)
- if data is not None:
- if 'error' in data:
- error = self.data.get('error') or 'unknown error'
- raise ValueError('Authentication error: %s' % error)
- data['access_token'] = access_token
- kwargs.update({
- 'auth': self,
- 'response': data,
- GithubBackend.name: True
- })
- return authenticate(*args, **kwargs)
- else:
+ if 'code' not in self.data:
+ error = self.data.get('error') or 'unknown error'
+ raise AuthFailed(self, error)
+
+ url = GITHUB_ACCESS_TOKEN_URL + urlencode({
+ 'client_id': setting('GITHUB_APP_ID'),
+ 'redirect_uri': self.redirect_uri,
+ 'client_secret': setting('GITHUB_API_SECRET'),
+ 'code': self.data['code']
+ })
+ response = cgi.parse_qs(urlopen(url).read())
+ if response.get('error'):
error = self.data.get('error') or 'unknown error'
- raise ValueError('Authentication error: %s' % error)
+ raise AuthFailed(self, error)
+
+ access_token = response['access_token'][0]
+ data = self.user_data(access_token)
+ if data is not None:
+ if 'error' in data:
+ error = self.data.get('error') or 'unknown error'
+ raise AuthFailed(self, error)
+ data['access_token'] = access_token
+
+ kwargs.update({
+ 'auth': self,
+ 'response': data,
+ self.AUTH_BACKEND.name: True
+ })
+ return authenticate(*args, **kwargs)
def user_data(self, access_token):
"""Loads user data from service"""
- params = {'access_token': access_token}
- url = GITHUB_API_URL + '/user?' + urllib.urlencode(params)
+ url = GITHUB_USER_DATA_URL + urlencode({
+ 'access_token': access_token
+ })
try:
- return simplejson.load(urllib.urlopen(url))
+ return simplejson.load(urlopen(url))
except ValueError:
return None
import urlparse
from social_auth.backends import OpenIDBackend, OpenIdAuth, USERNAME
+from social_auth.backends.exceptions import AuthMissingParameter
# LiveJournal conf
def openid_url(self):
"""Returns LiveJournal authentication URL"""
if not self.data.get(LIVEJOURNAL_USER_FIELD):
- raise ValueError('Missing LiveJournal user identifier')
+ raise AuthMissingParameter(self, LIVEJOURNAL_USER_FIELD)
return LIVEJOURNAL_URL % self.data[LIVEJOURNAL_USER_FIELD]
-class StopPipeline(Exception):
+class SocialAuthBaseException(ValueError):
+ """Base class for pipeline exceptions."""
+ pass
+
+
+class StopPipeline(SocialAuthBaseException):
"""Stop pipeline process exception.
Raise this exception to stop the rest of the pipeline process.
"""
- pass
+ def __unicode__(self):
+ return u'Stop pipeline'
+
+
+class AuthException(SocialAuthBaseException):
+ """Auth process exception."""
+ def __init__(self, backend, *args, **kwargs):
+ self.backend = backend
+ super(AuthException, self).__init__(*args, **kwargs)
+
+
+class AuthFailed(AuthException):
+ """Auth process failed for some reason."""
+ def __unicode__(self):
+ msg = super(AuthFailed, self).__unicode__()
+ return u'Authentication process failed %s' % msg
+
+
+class AuthCanceled(AuthException):
+ """Auth process was canceled by user."""
+ def __unicode__(self):
+ return u'Authentication process canceled'
+
+
+class AuthUnknownError(AuthException):
+ """Unknown auth process error."""
+ def __unicode__(self):
+ msg = super(AuthFailed, self).__unicode__()
+ return u'An unknown error happened while authenticating %s' % msg
+
+
+class AuthTokenError(AuthException):
+ """Auth token error."""
+ def __unicode__(self):
+ msg = super(AuthFailed, self).__unicode__()
+ return u'Token error: %s' % msg
+
+
+class AuthMissingParameter(AuthException):
+ """Missing parameter needed to start or complete the process."""
+ def __init__(self, backend, parameter, *args, **kwargs):
+ self.parameter = parameter
+ super(AuthMissingParameter, self).__init__(backend, *args, **kwargs)
+
+ def __unicode__(self):
+ return u'Missing needed parameter %s' % self.parameter
"""
import cgi
from urllib import urlencode
-from urllib2 import urlopen
+from urllib2 import urlopen, HTTPError
from django.utils import simplejson
from django.contrib.auth import authenticate
from social_auth.backends import BaseOAuth2, OAuthBackend, USERNAME
from social_auth.utils import sanitize_log_data, setting, log
+from social_auth.backends.exceptions import AuthException, AuthCanceled, \
+ AuthFailed, AuthTokenError
# Facebook configuration
FACEBOOK_ME = 'https://graph.facebook.com/me?'
+ACCESS_TOKEN = 'https://graph.facebook.com/oauth/access_token?'
class FacebookBackend(OAuthBackend):
extra = {'access_token': sanitize_log_data(access_token)}
log('error', 'Could not load user data from Facebook.',
exc_info=True, extra=extra)
+ except HTTPError:
+ extra = {'access_token': sanitize_log_data(access_token)}
+ log('error', 'Error validating access token.',
+ exc_info=True, extra=extra)
+ raise AuthTokenError(self)
else:
log('debug', 'Found user data for token %s',
- sanitize_log_data(access_token),
- extra=dict(data=data))
+ sanitize_log_data(access_token), extra={'data': data})
return data
def auth_complete(self, *args, **kwargs):
"""Completes loging process, must return user instance"""
- if 'code' in self.data:
- url = 'https://graph.facebook.com/oauth/access_token?' + \
- urlencode({'client_id': setting('FACEBOOK_APP_ID'),
- 'redirect_uri': self.redirect_uri,
- 'client_secret': setting('FACEBOOK_API_SECRET'),
- 'code': self.data['code']})
+ if 'code' not in self.data:
+ if self.data.get('error') == 'access_denied':
+ raise AuthCanceled(self)
+ else:
+ raise AuthException(self)
+
+ url = ACCESS_TOKEN + urlencode({
+ 'client_id': setting('FACEBOOK_APP_ID'),
+ 'redirect_uri': self.redirect_uri,
+ 'client_secret': setting('FACEBOOK_API_SECRET'),
+ 'code': self.data['code']
+ })
+ try:
response = cgi.parse_qs(urlopen(url).read())
- access_token = response['access_token'][0]
- data = self.user_data(access_token)
- if data is not None:
- if 'error' in data:
- error = self.data.get('error') or 'unknown error'
- raise ValueError('Authentication error: %s' % error)
- data['access_token'] = access_token
- # expires will not be part of response if offline access
- # premission was requested
- if 'expires' in response:
- data['expires'] = response['expires'][0]
- kwargs.update({
- 'auth': self,
- 'response': data,
- self.AUTH_BACKEND.name: True
- })
- return authenticate(*args, **kwargs)
- else:
- error = self.data.get('error') or 'unknown error'
- raise ValueError('Authentication error: %s' % error)
+ except HTTPError:
+ raise AuthFailed(self, 'There was an error authenticating the app')
+
+ access_token = response['access_token'][0]
+ data = self.user_data(access_token)
+
+ if data is not None:
+ data['access_token'] = access_token
+ # expires will not be part of response if offline access
+ # premission was requested
+ if 'expires' in response:
+ data['expires'] = response['expires'][0]
+
+ kwargs.update({ 'auth': self,
+ 'response': data,
+ self.AUTH_BACKEND.name: True })
+ return authenticate(*args, **kwargs)
@classmethod
def enabled(cls):
from social_auth.utils import setting
from social_auth.backends import OpenIdAuth, ConsumerBasedOAuth, BaseOAuth2, \
OAuthBackend, OpenIDBackend, USERNAME
+from social_auth.backends.exceptions import AuthFailed
# Google OAuth base configuration
def get_user_id(self, details, response):
"Use google email as unique id"""
- validate_whitelists(details['email'])
+ validate_whitelists(self, details['email'])
return details['email']
def get_user_details(self, response):
is unique enought to flag a single user. Email comes from schema:
http://axschema.org/contact/email
"""
- validate_whitelists(details['email'])
+ validate_whitelists(self, details['email'])
return details['email']
return None
-def validate_whitelists(email):
- """Validates allowed domains and emails against the GOOGLE_WHITE_LISTED_DOMAINS
- and GOOGLE_WHITE_LISTED_EMAILS settings.
- Allows all domains or emails if setting is an empty list.
+def validate_whitelists(backend, email):
+ """
+ Validates allowed domains and emails against the following settings:
+ GOOGLE_WHITE_LISTED_DOMAINS
+ GOOGLE_WHITE_LISTED_EMAILS
+
+ All domains and emails are allowed if setting is an empty list.
"""
emails = setting('GOOGLE_WHITE_LISTED_EMAILS', [])
domains = setting('GOOGLE_WHITE_LISTED_DOMAINS', [])
if emails and email in emails:
- return # you're good
+ return # you're good
if domains and email.split('@', 1)[1] not in domains:
- raise ValueError('Domain not allowed')
-
+ raise AuthFailed(backend, 'Domain not allowed')
# Backend definition
from social_auth.utils import setting
from social_auth.models import User
from social_auth.backends.pipeline import warn_setting
+from social_auth.backends.exceptions import AuthException
def associate_by_email(details, *args, **kwargs):
if email and setting('SOCIAL_AUTH_ASSOCIATE_BY_MAIL'):
# try to associate accounts registered with the same email address,
- # only if it's a single object. ValueError is raised if multiple
+ # only if it's a single object. AuthException is raised if multiple
# objects are returned
try:
return {'user': User.objects.get(email=email)}
except MultipleObjectsReturned:
- raise ValueError('Not unique email address.')
+ raise AuthException(kwargs['backend'], 'Not unique email address.')
except User.DoesNotExist:
pass
from social_auth.utils import setting
from social_auth.models import UserSocialAuth
from social_auth.backends.pipeline import warn_setting
+from social_auth.backends.exceptions import AuthException
def social_auth_user(backend, uid, user=None, *args, **kwargs):
"""Return UserSocialAuth account for backend/uid pair or None if it
doesn't exists.
- Raise ValueError if UserSocialAuth entry belongs to another user.
+ Raise AuthException if UserSocialAuth entry belongs to another user.
"""
try:
social_user = UserSocialAuth.objects.select_related('user')\
if social_user:
if user and social_user.user != user:
- raise ValueError('Account already in use.', social_user)
+ raise AuthException(backend, 'Account already in use.')
elif not user:
user = social_user.user
return {'social_user': social_user, 'user': user}
from django.utils import simplejson
from social_auth.backends import ConsumerBasedOAuth, OAuthBackend, USERNAME
+from social_auth.backends.exceptions import AuthCanceled
# Twitter configuration
def auth_complete(self, *args, **kwargs):
"""Completes loging process, must return user instance"""
if 'denied' in self.data:
- raise ValueError('Authentication denied')
+ raise AuthCanceled(self)
else:
return super(TwitterAuth, self).auth_complete(*args, **kwargs)
request.session.pop(name, None)
+def log_exceptions_to_messages(request, backend, err):
+ """Log exception messages to messages app if it's installed."""
+ if 'django.contrib.messages' in setting('INSTALLED_APPS'):
+ from django.contrib.messages.api import error
+ name = backend.AUTH_BACKEND.name
+ error(request, unicode(err), extra_tags='social-auth %s' % name)
+
+
if __name__ == '__main__':
import doctest
doctest.testmod()
from django.contrib.auth import login, REDIRECT_FIELD_NAME
from django.contrib.auth.decorators import login_required
from django.contrib import messages
+from django.utils.importlib import import_module
from django.views.decorators.csrf import csrf_exempt
from social_auth.backends import get_backend
setting('LOGIN_REDIRECT_URL')
LOGIN_ERROR_URL = setting('LOGIN_ERROR_URL', setting('LOGIN_URL'))
RAISE_EXCEPTIONS = setting('SOCIAL_AUTH_RAISE_EXCEPTIONS', setting('DEBUG'))
+PROCESS_EXCEPTIONS = setting('SOCIAL_AUTH_PROCESS_EXCEPTIONS',
+ 'social_auth.utils.log_exceptions_to_messages')
def dsa_view(redirect_name=None):
except Exception, e: # some error ocurred
if RAISE_EXCEPTIONS:
raise
- backend_name = backend.AUTH_BACKEND.name
-
- log('error', unicode(e), exc_info=True,
- extra=dict(request=request))
-
- if 'django.contrib.messages' in setting('INSTALLED_APPS'):
- from django.contrib.messages.api import error
- error(request, unicode(e), extra_tags=backend_name)
+ log('error', unicode(e), exc_info=True, extra={
+ 'request': request
+ })
+
+ mod, func_name = PROCESS_EXCEPTIONS.rsplit('.', 1)
+ try:
+ process = getattr(import_module(mod), func_name,
+ lambda *args: None)
+ except ImportError:
+ pass
else:
- log('warn', 'Messages framework not in place, some '+
- 'errors have not been shown to the user.')
+ process(request, backend, e)
url = backend_setting(backend, 'SOCIAL_AUTH_BACKEND_ERROR_URL',
LOGIN_ERROR_URL)