]> git.parisson.com Git - django-social-auth.git/commitdiff
Differentiate exceptions raised. Closes #119. Refs #175
authorMatías Aguirre <matiasaguirre@gmail.com>
Thu, 1 Mar 2012 15:16:05 +0000 (13:16 -0200)
committerMatías Aguirre <matiasaguirre@gmail.com>
Thu, 1 Mar 2012 15:16:05 +0000 (13:16 -0200)
17 files changed:
README.rst
doc/configuration.rst
doc/miscellaneous.rst
doc/testing.rst
social_auth/backends/__init__.py
social_auth/backends/browserid.py
social_auth/backends/contrib/gae.py
social_auth/backends/contrib/github.py
social_auth/backends/contrib/livejournal.py
social_auth/backends/exceptions.py
social_auth/backends/facebook.py
social_auth/backends/google.py
social_auth/backends/pipeline/associate.py
social_auth/backends/pipeline/social.py
social_auth/backends/twitter.py
social_auth/utils.py
social_auth/views.py

index c8cde76b9f5abb9d0c8e8b5fa215853c8b4a20cb..23eb502565a41bac25d78d3b0e0364bfadcd4889 100644 (file)
@@ -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
index 8650151520898464aed665324aecf509cad088ee..01e9204523d40d70b4249498d0feb69e7cb07613 100644 (file)
@@ -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::
 
index ea69ac8b43a3e3dd9674a44805b8ef4622a8f184..ce0ee677baa4c3e26c2e1c0e69538a93b11f3ca9 100644 (file)
@@ -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::
 
index 18f526678cc06aac69aa93d7693f9e9b175b8153..f4b3fb917b439530ba14c69e4e9ab35943da4669 100644 (file)
@@ -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
 
index ce762bf83bd1bfeee479ae9cbdfd9f019e2c6ba9..79469a86c949f5e4ec58b9ad910dbf46bf7a60cd 100644 (file)
@@ -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({
index ca038c447d580af931ee575afe096d72cdba583c..2baadebb054dcc16874f9a3c42650ed54055150b 100644 (file)
@@ -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,
index fa6622c442880ec9fbf2c7d71891a35ea62dbef0..72640f5996267ef0cba947e28c08725b32cded4b 100644 (file)
@@ -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({
index 269810d49aacc52c6fa76799f068a3318177eaf6..c80c7c0356783d64bff0bd1e537c227efa900e1f 100644 (file)
@@ -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
 
index ce9e538d9d5bb40f81511a9fbc862b2048748c08..53cf07684048b7962d8ce31d0e0f442b9d6605e6 100644 (file)
@@ -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]
 
 
index 8518b314b8ae1d88c6b0597bcf60e2b40b5ce896..0e8b73a8af5c1031f53cfe83b8a9de12971e86e7 100644 (file)
@@ -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
index 70f87988b9ef4dfeeea524d885a043d9a68d03f3..9b9e79c6783be939dc1cd638d0958acab6a44351 100644 (file)
@@ -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):
index f9d56a5d1570213f28edca5ab959c1368051c600..d926f5556464fe456680bae0fb576167860a257b 100644 (file)
@@ -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
index 18c9c46993d6c3aafb35b4a769e6ecc5d1827bdf..602521be121d97d13ef08869523bd7c5f9b2bada 100644 (file)
@@ -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
index d8436166d20c6cd8165cec91a32bbbbb47cac7bb..708b4de1792dc73bed0a89c928cb54cd7456c410 100644 (file)
@@ -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}
index 13fc7d13769622ade496f5c40eb7eb92f9862e9d..bee56edeb06ab723814b9299bcb9193880e816be 100644 (file)
@@ -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)
 
index a77f948561fb4b77c1b6241ca07087f64c3c8c2f..747ec3b62e0971fad4a94a8641b624ba0c55f473 100644 (file)
@@ -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()
index fa35359edd7fafd1eebba96eb705e820598e8329..b2fdc75a5a2d779db8c8861b741656b55ca6b7ca 100644 (file)
@@ -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)