From: Matías Aguirre Date: Wed, 4 Jul 2012 06:38:55 +0000 (-0300) Subject: Add state argument to oauth2 process to secure process. Refs #386 X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=f3bc0f12c1dfa046a3b3c44763741f7db88d9fd7;p=django-social-auth.git Add state argument to oauth2 process to secure process. Refs #386 --- diff --git a/social_auth/backends/__init__.py b/social_auth/backends/__init__.py index fb439a4..d70f51e 100644 --- a/social_auth/backends/__init__.py +++ b/social_auth/backends/__init__.py @@ -24,6 +24,8 @@ from django.contrib.auth import authenticate from django.contrib.auth.backends import ModelBackend from django.utils import simplejson from django.utils.importlib import import_module +from django.utils.crypto import constant_time_compare, get_random_string +from django.middleware.csrf import CSRF_KEY_LENGTH from social_auth.utils import setting, log, model_to_ctype, ctype_to_model, \ clean_partial_pipeline @@ -31,7 +33,8 @@ from social_auth.store import DjangoOpenIDStore from social_auth.backends.exceptions import StopPipeline, AuthException, \ AuthFailed, AuthCanceled, \ AuthUnknownError, AuthTokenError, \ - AuthMissingParameter + AuthMissingParameter, \ + AuthForbidden from social_auth.backends.utils import build_consumer_oauth_request @@ -663,12 +666,22 @@ class BaseOAuth2(BaseOAuth): RESPONSE_TYPE = 'code' SCOPE_VAR_NAME = None DEFAULT_SCOPE = None + FORCE_STATE_CHECK = True + + def csrf_token(self): + """Generate csrf token to include as state parameter.""" + return get_random_string(CSRF_KEY_LENGTH) def auth_url(self): """Return redirect url""" client_id, client_secret = self.get_key_and_secret() args = {'client_id': client_id, 'redirect_uri': self.redirect_uri} + if self.FORCE_STATE_CHECK: + state = self.csrf_token() + args['state'] = state + self.request.session[self.AUTH_BACKEND.name + '_state'] = state + scope = self.get_scope() if scope: args['scope'] = self.SCOPE_SEPARATOR.join(self.get_scope()) @@ -684,6 +697,13 @@ class BaseOAuth2(BaseOAuth): error = self.data.get('error_description') or self.data['error'] raise AuthFailed(self, error) + if self.FORCE_STATE_CHECK: + if 'state' not in self.data: + raise AuthMissingParameter(self, 'state') + state = self.request.session[self.AUTH_BACKEND.name + '_state'] + if not constant_time_compare(self.data['state'], state): + raise AuthForbidden(self) + client_id, client_secret = self.get_key_and_secret() params = {'grant_type': 'authorization_code', # request auth code 'code': self.data.get('code', ''), # server response code diff --git a/social_auth/backends/contrib/github.py b/social_auth/backends/contrib/github.py index 0580b2f..af9371a 100644 --- a/social_auth/backends/contrib/github.py +++ b/social_auth/backends/contrib/github.py @@ -53,6 +53,8 @@ class GithubAuth(BaseOAuth2): SCOPE_SEPARATOR = ',' # Look at http://developer.github.com/v3/oauth/ SCOPE_VAR_NAME = 'GITHUB_EXTENDED_PERMISSIONS' + # Github doesn't return the state paramenter if specified :( + FORCE_STATE_CHECK = False def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" diff --git a/social_auth/backends/contrib/instagram.py b/social_auth/backends/contrib/instagram.py index faa2e82..3cf1689 100644 --- a/social_auth/backends/contrib/instagram.py +++ b/social_auth/backends/contrib/instagram.py @@ -37,6 +37,7 @@ class InstagramAuth(BaseOAuth2): AUTH_BACKEND = InstagramBackend SETTINGS_KEY_NAME = 'INSTAGRAM_CLIENT_ID' SETTINGS_SECRET_NAME = 'INSTAGRAM_CLIENT_SECRET' + FORCE_STATE_CHECK = False def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" diff --git a/social_auth/backends/exceptions.py b/social_auth/backends/exceptions.py index 654ff22..35f76e0 100644 --- a/social_auth/backends/exceptions.py +++ b/social_auth/backends/exceptions.py @@ -24,7 +24,6 @@ class AuthException(SocialAuthBaseException): class AuthFailed(AuthException): """Auth process failed for some reason.""" def __unicode__(self): - if self.message == 'access_denied': return ugettext(u'Authentication process was cancelled') else: @@ -60,3 +59,9 @@ class AuthMissingParameter(AuthException): def __unicode__(self): return u'Missing needed parameter %s' % self.parameter + + +class AuthForbidden(AuthException): + """State parameter is incorrect.""" + def __unicode__(self): + return u'Wrong state parameter given.'