From 7dd4273f9fcc6d12516c699f6eb8dfda8242ac8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Aguirre?= Date: Sat, 4 Apr 2015 03:47:42 -0300 Subject: [PATCH] Improve http error handling on auth_complete/do_auth. Fixes #304 --- social/backends/beats.py | 25 ++++++++----------------- social/backends/facebook.py | 3 ++- social/backends/google.py | 24 +++++++++--------------- social/backends/jawbone.py | 23 ++++++++--------------- social/backends/lastfm.py | 2 ++ social/backends/oauth.py | 35 ++++++++++++----------------------- social/backends/persona.py | 2 ++ social/backends/pocket.py | 2 ++ social/backends/shopify.py | 9 ++------- social/backends/yahoo.py | 26 +++++++++----------------- social/exceptions.py | 6 ++++++ social/utils.py | 20 +++++++++++++++++++- 12 files changed, 81 insertions(+), 96 deletions(-) diff --git a/social/backends/beats.py b/social/backends/beats.py index d5801eb8b..d1d877252 100644 --- a/social/backends/beats.py +++ b/social/backends/beats.py @@ -4,9 +4,7 @@ """ import base64 -from requests import HTTPError - -from social.exceptions import AuthCanceled, AuthUnknownError +from social.utils import handle_http_errors from social.backends.oauth import BaseOAuth2 @@ -30,23 +28,16 @@ def auth_headers(self): )) } + @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" self.process_error(self.data) - try: - response = self.request_access_token( - self.ACCESS_TOKEN_URL, - data=self.auth_complete_params(self.validate_state()), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self) - else: - raise - except KeyError: - raise AuthUnknownError(self) + response = self.request_access_token( + self.ACCESS_TOKEN_URL, + data=self.auth_complete_params(self.validate_state()), + headers=self.auth_headers(), + method=self.ACCESS_TOKEN_METHOD + ) self.process_error(response) # mashery wraps in jsonrpc if response.get('jsonrpc', None): diff --git a/social/backends/facebook.py b/social/backends/facebook.py index 17b523818..3de7573d3 100644 --- a/social/backends/facebook.py +++ b/social/backends/facebook.py @@ -8,7 +8,7 @@ import base64 import hashlib -from social.utils import parse_qs, constant_time_compare +from social.utils import parse_qs, constant_time_compare, handle_http_errors from social.backends.oauth import BaseOAuth2 from social.exceptions import AuthException, AuthCanceled, AuthUnknownError, \ AuthMissingParameter @@ -62,6 +62,7 @@ def process_error(self, data): raise AuthCanceled(self, data.get('error_message') or data.get('error_code')) + @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" self.process_error(self.data) diff --git a/social/backends/google.py b/social/backends/google.py index 253615269..a58549aa1 100644 --- a/social/backends/google.py +++ b/social/backends/google.py @@ -2,11 +2,10 @@ Google OpenId, OAuth2, OAuth1, Google+ Sign-in backends, docs at: http://psa.matiasaguirre.net/docs/backends/google.html """ -from requests import HTTPError - +from social.utils import handle_http_errors from social.backends.open_id import OpenIdAuth, OpenIdConnectAuth from social.backends.oauth import BaseOAuth2, BaseOAuth1 -from social.exceptions import AuthMissingParameter, AuthCanceled +from social.exceptions import AuthMissingParameter class BaseGoogleAuth(object): @@ -135,6 +134,7 @@ def auth_complete_params(self, state=None): params['redirect_uri'] = 'postmessage' return params + @handle_http_errors def auth_complete(self, *args, **kwargs): if 'access_token' in self.data and 'code' not in self.data: raise AuthMissingParameter(self, 'access_token or code') @@ -147,18 +147,12 @@ def auth_complete(self, *args, **kwargs): params={'access_token': token} )) - try: - response = self.request_access_token( - self.ACCESS_TOKEN_URL, - data=self.auth_complete_params(), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self) - else: - raise + response = self.request_access_token( + self.ACCESS_TOKEN_URL, + data=self.auth_complete_params(), + headers=self.auth_headers(), + method=self.ACCESS_TOKEN_METHOD + ) self.process_error(response) return self.do_auth(response['access_token'], response=response, *args, **kwargs) diff --git a/social/backends/jawbone.py b/social/backends/jawbone.py index cf6735904..52c84ce80 100644 --- a/social/backends/jawbone.py +++ b/social/backends/jawbone.py @@ -2,7 +2,7 @@ Jawbone OAuth2 backend, docs at: http://psa.matiasaguirre.net/docs/backends/jawbone.html """ -from requests import HTTPError +from social.utils import handle_http_errors from social.backends.oauth import BaseOAuth2 from social.exceptions import AuthCanceled, AuthUnknownError @@ -62,23 +62,16 @@ def auth_complete_params(self, state=None): 'client_secret': client_secret, } + @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" self.process_error(self.data) - try: - response = self.request_access_token( - self.ACCESS_TOKEN_URL, - params=self.auth_complete_params(self.validate_state()), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self) - else: - raise - except KeyError: - raise AuthUnknownError(self) + response = self.request_access_token( + self.ACCESS_TOKEN_URL, + params=self.auth_complete_params(self.validate_state()), + headers=self.auth_headers(), + method=self.ACCESS_TOKEN_METHOD + ) self.process_error(response) return self.do_auth(response['access_token'], response=response, *args, **kwargs) diff --git a/social/backends/lastfm.py b/social/backends/lastfm.py index c9ea837bc..0f1acf9db 100644 --- a/social/backends/lastfm.py +++ b/social/backends/lastfm.py @@ -1,5 +1,6 @@ import hashlib +from social.utils import handle_http_errors from social.backends.base import BaseAuth @@ -21,6 +22,7 @@ class LastFmAuth(BaseAuth): def auth_url(self): return self.AUTH_URL.format(api_key=self.setting('KEY')) + @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" key, secret = self.get_key_and_secret() diff --git a/social/backends/oauth.py b/social/backends/oauth.py index 717092dc0..0fa17ccdd 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -1,11 +1,10 @@ import six -from requests import HTTPError from requests_oauthlib import OAuth1 from oauthlib.oauth1 import SIGNATURE_TYPE_AUTH_HEADER from social.p3 import urlencode, unquote -from social.utils import url_add_parameters, parse_qs +from social.utils import url_add_parameters, parse_qs, handle_http_errors from social.exceptions import AuthFailed, AuthCanceled, AuthUnknownError, \ AuthMissingParameter, AuthStateMissing, \ AuthStateForbidden, AuthTokenError @@ -170,21 +169,17 @@ def process_error(self, data): raise AuthCanceled(self, 'User refused the access') raise AuthUnknownError(self, 'Error was ' + data['oauth_problem']) + @handle_http_errors def auth_complete(self, *args, **kwargs): """Return user, might be logged in""" # Multiple unauthorized tokens are supported (see #521) self.process_error(self.data) self.validate_state() token = self.get_unauthorized_token() - try: - access_token = self.access_token(token) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self) - else: - raise + access_token = self.access_token(token) return self.do_auth(access_token, *args, **kwargs) + @handle_http_errors def do_auth(self, access_token, *args, **kwargs): """Finish the auth process once the access_token was retrieved""" if not isinstance(access_token, dict): @@ -356,28 +351,22 @@ def process_error(self, data): elif 'denied' in data: raise AuthCanceled(self, data['denied']) + @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" state = self.validate_state() self.process_error(self.data) - try: - response = self.request_access_token( - self.access_token_url(), - data=self.auth_complete_params(state), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self) - else: - raise - except KeyError: - raise AuthUnknownError(self) + response = self.request_access_token( + self.access_token_url(), + data=self.auth_complete_params(state), + headers=self.auth_headers(), + method=self.ACCESS_TOKEN_METHOD + ) self.process_error(response) return self.do_auth(response['access_token'], response=response, *args, **kwargs) + @handle_http_errors def do_auth(self, access_token, *args, **kwargs): """Finish the auth process once the access_token was retrieved""" data = self.user_data(access_token, *args, **kwargs) diff --git a/social/backends/persona.py b/social/backends/persona.py index 060715ca6..054ea2fed 100644 --- a/social/backends/persona.py +++ b/social/backends/persona.py @@ -2,6 +2,7 @@ Mozilla Persona authentication backend, docs at: http://psa.matiasaguirre.net/docs/backends/persona.html """ +from social.utils import handle_http_errors from social.backends.base import BaseAuth from social.exceptions import AuthFailed, AuthMissingParameter @@ -33,6 +34,7 @@ def extra_data(self, user, uid, response, details): return {'audience': response['audience'], 'issuer': response['issuer']} + @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" if 'assertion' not in self.data: diff --git a/social/backends/pocket.py b/social/backends/pocket.py index 90bc5a9bf..0852d312a 100644 --- a/social/backends/pocket.py +++ b/social/backends/pocket.py @@ -3,6 +3,7 @@ http://psa.matiasaguirre.net/docs/backends/pocket.html """ from social.backends.base import BaseAuth +from social.utils import handle_http_errors class PocketAuth(BaseAuth): @@ -33,6 +34,7 @@ def auth_url(self): bits = (self.AUTHORIZATION_URL, token, self.redirect_uri) return '%s?request_token=%s&redirect_uri=%s' % bits + @handle_http_errors def auth_complete(self, *args, **kwargs): data = { 'consumer_key': self.setting('POCKET_CONSUMER_KEY'), diff --git a/social/backends/shopify.py b/social/backends/shopify.py index 407856769..214fd1845 100644 --- a/social/backends/shopify.py +++ b/social/backends/shopify.py @@ -5,8 +5,7 @@ import imp import six -from requests import HTTPError - +from social.utils import handle_http_errors from social.backends.oauth import BaseOAuth2 from social.exceptions import AuthFailed, AuthCanceled @@ -61,6 +60,7 @@ def auth_url(self): redirect_uri=redirect_uri ) + @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" self.process_error(self.data) @@ -73,11 +73,6 @@ def auth_complete(self, *args, **kwargs): access_token = shopify_session.token except self.shopifyAPI.ValidationException: raise AuthCanceled(self) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self) - else: - raise else: if not access_token: raise AuthFailed(self, 'Authentication Failed') diff --git a/social/backends/yahoo.py b/social/backends/yahoo.py index 7ab046db5..15fc9d17d 100644 --- a/social/backends/yahoo.py +++ b/social/backends/yahoo.py @@ -2,12 +2,11 @@ Yahoo OpenId, OAuth1 and OAuth2 backends, docs at: http://psa.matiasaguirre.net/docs/backends/yahoo.html """ -from requests import HTTPError from requests.auth import HTTPBasicAuth +from social.utils import handle_http_errors from social.backends.open_id import OpenIdAuth from social.backends.oauth import BaseOAuth2, BaseOAuth1 -from social.exceptions import AuthCanceled, AuthUnknownError class YahooOpenId(OpenIdAuth): @@ -113,24 +112,17 @@ def user_data(self, access_token, *args, **kwargs): 'Authorization': 'Bearer {0}'.format(access_token) }, method='GET')['profile'] + @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" self.process_error(self.data) - try: - response = self.request_access_token( - self.ACCESS_TOKEN_URL, - auth=HTTPBasicAuth(*self.get_key_and_secret()), - data=self.auth_complete_params(self.validate_state()), - headers=self.auth_headers(), - method=self.ACCESS_TOKEN_METHOD - ) - except HTTPError as err: - if err.response.status_code == 400: - raise AuthCanceled(self) - else: - raise - except KeyError: - raise AuthUnknownError(self) + response = self.request_access_token( + self.ACCESS_TOKEN_URL, + auth=HTTPBasicAuth(*self.get_key_and_secret()), + data=self.auth_complete_params(self.validate_state()), + headers=self.auth_headers(), + method=self.ACCESS_TOKEN_METHOD + ) self.process_error(response) return self.do_auth(response['access_token'], response=response, *args, **kwargs) diff --git a/social/exceptions.py b/social/exceptions.py index b4b8b2f73..88e11551c 100644 --- a/social/exceptions.py +++ b/social/exceptions.py @@ -98,6 +98,12 @@ def __str__(self): return 'Your credentials aren\'t allowed' +class AuthUnreachableProvider(AuthException): + """Cannot reach the provider""" + def __str__(self): + return 'The authentication provider could not be reached' + + class InvalidEmail(AuthException): def __str__(self): return 'Email couldn\'t be validated' diff --git a/social/utils.py b/social/utils.py index 0ceb54fe8..266dc71d0 100644 --- a/social/utils.py +++ b/social/utils.py @@ -2,10 +2,13 @@ import sys import unicodedata import collections -import six +import functools +import six +import requests import social +from social.exceptions import AuthCanceled, AuthUnreachableProvider from social.p3 import urlparse, urlunparse, urlencode, \ parse_qs as battery_parse_qs @@ -187,3 +190,18 @@ def setting_url(backend, *names): value = backend.setting(name) if is_url(value): return value + + +def handle_http_errors(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except requests.HTTPError as err: + if err.response.status_code == 400: + raise AuthCanceled(args[0]) + elif err.response.status_code == 503: + raise AuthUnreachableProvider(args[0]) + else: + raise + return wrapper