From: Matías Aguirre Date: Thu, 1 Mar 2012 15:16:05 +0000 (-0200) Subject: Differentiate exceptions raised. Closes #119. Refs #175 X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=0db8f14ab0778c0849d4070c377e0a6b89fbe490;p=django-social-auth.git Differentiate exceptions raised. Closes #119. Refs #175 --- diff --git a/README.rst b/README.rst index c8cde76..23eb502 100644 --- a/README.rst +++ b/README.rst @@ -333,9 +333,36 @@ Configuration 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 @@ -802,7 +829,7 @@ Instagram uses OAuth v2 for Authentication Testing ------- -To test the app just run:: +To test the application just run:: ./manage.py test social_auth @@ -891,8 +918,8 @@ Miscellaneous ------------- 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 diff --git a/doc/configuration.rst b/doc/configuration.rst index 8650151..01e9204 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -229,15 +229,43 @@ Configuration 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:: diff --git a/doc/miscellaneous.rst b/doc/miscellaneous.rst index ea69ac8..ce0ee67 100644 --- a/doc/miscellaneous.rst +++ b/doc/miscellaneous.rst @@ -2,8 +2,8 @@ Miscellaneous ============= 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:: diff --git a/doc/testing.rst b/doc/testing.rst index 18f5266..f4b3fb9 100644 --- a/doc/testing.rst +++ b/doc/testing.rst @@ -3,7 +3,7 @@ Testing 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 diff --git a/social_auth/backends/__init__.py b/social_auth/backends/__init__.py index ce762bf..79469a8 100644 --- a/social_auth/backends/__init__.py +++ b/social_auth/backends/__init__.py @@ -29,7 +29,10 @@ from django.utils.importlib import import_module 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'): @@ -418,7 +421,7 @@ class OpenIdAuth(BaseAuth): 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, @@ -427,13 +430,11 @@ class OpenIdAuth(BaseAuth): }) 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""" @@ -474,14 +475,14 @@ class OpenIdAuth(BaseAuth): 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] @@ -521,17 +522,17 @@ class ConsumerBasedOAuth(BaseOAuth): 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 @@ -641,7 +642,7 @@ class BaseOAuth2(BaseOAuth): """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 @@ -656,11 +657,11 @@ class BaseOAuth2(BaseOAuth): 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({ diff --git a/social_auth/backends/browserid.py b/social_auth/backends/browserid.py index ca038c4..2baadeb 100644 --- a/social_auth/backends/browserid.py +++ b/social_auth/backends/browserid.py @@ -11,6 +11,7 @@ from django.utils import simplejson 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 @@ -60,7 +61,7 @@ class BrowserIDAuth(BaseAuth): 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'], @@ -75,7 +76,7 @@ class BrowserIDAuth(BaseAuth): else: if response.get('status') == 'failure': log('debug', 'Authentication failed.') - raise ValueError('Authentication failed') + raise AuthFailed(self) kwargs.update({ 'auth': self, diff --git a/social_auth/backends/contrib/gae.py b/social_auth/backends/contrib/gae.py index fa6622c..72640f5 100644 --- a/social_auth/backends/contrib/gae.py +++ b/social_auth/backends/contrib/gae.py @@ -9,6 +9,7 @@ from django.contrib.auth import authenticate from django.core.urlresolvers import reverse from social_auth.backends import SocialAuthBackend, BaseAuth, USERNAME +from social_auth.backends.exceptions import AuthException class GAEBackend(SocialAuthBackend): @@ -44,7 +45,7 @@ class GAEAuth(BaseAuth): 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({ diff --git a/social_auth/backends/contrib/github.py b/social_auth/backends/contrib/github.py index 269810d..c80c7c0 100644 --- a/social_auth/backends/contrib/github.py +++ b/social_auth/backends/contrib/github.py @@ -12,20 +12,20 @@ By default account id and token expiration time are stored in extra_data 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): @@ -49,49 +49,54 @@ class GithubAuth(BaseOAuth): 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 diff --git a/social_auth/backends/contrib/livejournal.py b/social_auth/backends/contrib/livejournal.py index ce9e538..53cf076 100644 --- a/social_auth/backends/contrib/livejournal.py +++ b/social_auth/backends/contrib/livejournal.py @@ -7,6 +7,7 @@ username.livejournal.com. Username is retrieved from the identity url. import urlparse from social_auth.backends import OpenIDBackend, OpenIdAuth, USERNAME +from social_auth.backends.exceptions import AuthMissingParameter # LiveJournal conf @@ -38,7 +39,7 @@ class LiveJournalAuth(OpenIdAuth): 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] diff --git a/social_auth/backends/exceptions.py b/social_auth/backends/exceptions.py index 8518b31..0e8b73a 100644 --- a/social_auth/backends/exceptions.py +++ b/social_auth/backends/exceptions.py @@ -1,5 +1,55 @@ -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 diff --git a/social_auth/backends/facebook.py b/social_auth/backends/facebook.py index 70f8798..9b9e79c 100644 --- a/social_auth/backends/facebook.py +++ b/social_auth/backends/facebook.py @@ -13,17 +13,20 @@ field, check OAuthBackend class for details on how to extend it. """ 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): @@ -67,41 +70,49 @@ class FacebookAuth(BaseOAuth2): 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): diff --git a/social_auth/backends/google.py b/social_auth/backends/google.py index f9d56a5..d926f55 100644 --- a/social_auth/backends/google.py +++ b/social_auth/backends/google.py @@ -23,6 +23,7 @@ from django.utils import simplejson 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 @@ -49,7 +50,7 @@ class GoogleOAuthBackend(OAuthBackend): 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): @@ -81,7 +82,7 @@ class GoogleBackend(OpenIDBackend): 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'] @@ -199,18 +200,20 @@ def googleapis_email(url, params): 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 diff --git a/social_auth/backends/pipeline/associate.py b/social_auth/backends/pipeline/associate.py index 18c9c46..602521b 100644 --- a/social_auth/backends/pipeline/associate.py +++ b/social_auth/backends/pipeline/associate.py @@ -3,6 +3,7 @@ from django.core.exceptions import MultipleObjectsReturned 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): @@ -13,11 +14,11 @@ 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 diff --git a/social_auth/backends/pipeline/social.py b/social_auth/backends/pipeline/social.py index d843616..708b4de 100644 --- a/social_auth/backends/pipeline/social.py +++ b/social_auth/backends/pipeline/social.py @@ -3,13 +3,14 @@ from django.db.utils import IntegrityError 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')\ @@ -20,7 +21,7 @@ def social_auth_user(backend, uid, user=None, *args, **kwargs): 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} diff --git a/social_auth/backends/twitter.py b/social_auth/backends/twitter.py index 13fc7d1..bee56ed 100644 --- a/social_auth/backends/twitter.py +++ b/social_auth/backends/twitter.py @@ -14,6 +14,7 @@ class for details on how to extend it. from django.utils import simplejson from social_auth.backends import ConsumerBasedOAuth, OAuthBackend, USERNAME +from social_auth.backends.exceptions import AuthCanceled # Twitter configuration @@ -67,7 +68,7 @@ class TwitterAuth(ConsumerBasedOAuth): 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) diff --git a/social_auth/utils.py b/social_auth/utils.py index a77f948..747ec3b 100644 --- a/social_auth/utils.py +++ b/social_auth/utils.py @@ -145,6 +145,14 @@ def clean_partial_pipeline(request): 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() diff --git a/social_auth/views.py b/social_auth/views.py index fa35359..b2fdc75 100644 --- a/social_auth/views.py +++ b/social_auth/views.py @@ -13,6 +13,7 @@ from django.core.urlresolvers import reverse 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 @@ -24,6 +25,8 @@ DEFAULT_REDIRECT = setting('SOCIAL_AUTH_LOGIN_REDIRECT_URL') or \ 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): @@ -50,17 +53,18 @@ 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)