From: Matías Aguirre Date: Tue, 23 Nov 2010 15:33:33 +0000 (-0200) Subject: OAuth improvements. X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=b1edd69af9619a4ae6eacf97b6cd437b2f879b71;p=django-social-auth.git OAuth improvements. Changes: - Parametrize backend name - Common base class between Twitter and Orkut authentication mechanism - Updated Doc - Updated example application with Orkut --- diff --git a/README.rst b/README.rst index 0603029..1b6cc3e 100644 --- a/README.rst +++ b/README.rst @@ -13,7 +13,6 @@ third parties. ------------ Dependencies ------------ - Dependencies that must be meet to use the app: - OpenId support depends on python-openid_ @@ -27,7 +26,6 @@ Dependencies that must be meet to use the app: ------------ Installation ------------ - - Add social_auth app to PYTHONPATH and installed apps:: INSTALLED_APPS = ( @@ -51,8 +49,8 @@ Installation TWITTER_CONSUMER_SECRET = '' FACEBOOK_APP_ID = '' FACEBOOK_API_SECRET = '' - ORKUT_CONSUMER_KEY = '' - ORKUT_CONSUMER_SECRET = '' + ORKUT_CONSUMER_KEY = '' + ORKUT_CONSUMER_SECRET = '' - Setup login urls:: @@ -133,8 +131,9 @@ providing endpoint Url. OAuth ----- OAuth communication demands a set of keys exchange to validate the client -authenticity prior to user approbation, Twitter, Facebook and Orkut facilitates -these keys by application registration, see next sections for details. +authenticity prior to user approbation. Twitter, Facebook and Orkut +facilitates these keys by application registration, see next sections for +details. ------- @@ -155,6 +154,7 @@ Further documentation at `Twitter development resources`_: - You don't need to specify the url callback + -------- Facebook -------- @@ -169,22 +169,22 @@ Further documentation at `Facebook development resources`_: FACEBOOK_APP_ID FACEBOOK_API_SECRET -------- + +----- Orkut -------- +----- Orkut offers per application keys named "Consumer Key" and "Consumer Secret". To enable Orkut these two keys are needed. -For getting your consumer_key and consumer_secret, you may look -the following link for more information: - -http://www.google.com/support/a/bin/answer.py?hl=en&answer=162105 +Check `Google support`_ and `Orkut API`_ for details on getting +your consumer_key and consumer_secret keys. - fill "Consumer Key" and "Consumer Secret" settings:: ORKUT_CONSUMER_KEY ORKUT_CONSUMER_SECRET + ---- Bugs ---- @@ -194,7 +194,6 @@ Several, maybe, please report :-) ------------ Contributors ------------ - Attributions to whom deserves: - caioariede_ (Caio Ariede) @@ -205,12 +204,12 @@ Copyrights ---------- Base work is copyrighted by: -django-twitter-oauth:: +- django-twitter-oauth:: Original Copyright goes to Henrik Lied (henriklied) Code borrowed from https://github.com/henriklied/django-twitter-oauth -django-openid-auth:: +- django-openid-auth:: django-openid-auth - OpenID integration for django.contrib.auth Copyright (C) 2007 Simon Willison @@ -231,3 +230,5 @@ django-openid-auth:: .. _auth.User: http://code.djangoproject.com/browser/django/trunk/django/contrib/auth/models.py#L186 .. _User Profiles: http://www.djangobook.com/en/1.0/chapter12/#cn222 .. _caioariede: https://github.com/caioariede +.. _Google support: http://www.google.com/support/a/bin/answer.py?hl=en&answer=162105 +.. _Orkut API: http://code.google.com/apis/orkut/docs/rest/developers_guide_protocol.html#Authenticating diff --git a/example/app/views.py b/example/app/views.py index 125227f..07af2bc 100644 --- a/example/app/views.py +++ b/example/app/views.py @@ -19,6 +19,7 @@ def home(request):
diff --git a/social_auth/auth.py b/social_auth/auth.py index 61a5ef9..a17a58f 100644 --- a/social_auth/auth.py +++ b/social_auth/auth.py @@ -15,15 +15,17 @@ from django.contrib.auth import authenticate from .base import BaseAuth from .store import DjangoOpenIDStore +from .backends import TwitterBackend, OrkutBackend, FacebookBackend, \ + OpenIDBackend from .conf import AX_ATTRS, SREG_ATTR, OPENID_ID_FIELD, SESSION_NAME, \ OPENID_GOOGLE_URL, OPENID_YAHOO_URL, TWITTER_SERVER, \ TWITTER_REQUEST_TOKEN_URL, TWITTER_ACCESS_TOKEN_URL, \ TWITTER_AUTHORIZATION_URL, TWITTER_CHECK_AUTH, \ - TWITTER_UNAUTHORIZED_TOKEN_NAME, FACEBOOK_CHECK_AUTH, \ - FACEBOOK_AUTHORIZATION_URL, FACEBOOK_ACCESS_TOKEN_URL, \ - ORKUT_SERVER, ORKUT_SCOPE, ORKUT_UNAUTHORIZED_TOKEN_NAME, \ + FACEBOOK_CHECK_AUTH, FACEBOOK_AUTHORIZATION_URL, \ + FACEBOOK_ACCESS_TOKEN_URL, ORKUT_SERVER, ORKUT_SCOPE, \ ORKUT_REQUEST_TOKEN_URL, ORKUT_ACCESS_TOKEN_URL, \ - ORKUT_AUTHORIZATION_URL, ORKUT_REST_ENDPOINT, ORKUT_EXTRA_DATA + ORKUT_AUTHORIZATION_URL, ORKUT_REST_ENDPOINT, \ + ORKUT_EXTRA_DATA class OpenIdAuth(BaseAuth): @@ -51,7 +53,8 @@ class OpenIdAuth(BaseAuth): if not response: raise ValueError, 'This is an OpenID relying party endpoint' elif response.status == SUCCESS: - return authenticate(response=response, openid=True) + return authenticate(**{'response': response, + OpenIDBackend.name: True}) elif response.status == FAILURE: raise ValueError, 'OpenID authentication failed: %s' % response.message elif response.status == CANCEL: @@ -132,39 +135,60 @@ class BaseOAuth(BaseAuth): self.redirect_uri = self.request.build_absolute_uri(self.redirect) -class OrkutAuth(BaseOAuth): - """Orkut OAuth authentication mechanism""" +class ConsumerBasedOAuth(BaseOAuth): + """Consumer based mechanism OAuth authentication, fill the needed + parameters to communicate properly with authentication service. + + @AUTHORIZATION_URL Authorization service url + @REQUEST_TOKEN_URL Request token URL + @ACCESS_TOKEN_URL Access token URL + @SERVER_URL Authorization server URL + @AUTH_BACKEND Authorization backend related with + this service + """ + AUTHORIZATION_URL = '' + REQUEST_TOKEN_URL = '' + ACCESS_TOKEN_URL = '' + SERVER_URL = '' + AUTH_BACKEND = None + def auth_url(self): """Returns redirect url""" token = self.unauthorized_token() - self.request.session[ORKUT_UNAUTHORIZED_TOKEN_NAME] = token.to_string() - return self.oauth_request(token, ORKUT_AUTHORIZATION_URL).to_url() + name = self.AUTH_BACKEND.name + 'unauthorized_token_name' + self.request.session[name] = token.to_string() + return self.oauth_request(token, self.AUTHORIZATION_URL).to_url() def auth_complete(self): """Returns user, might be logged in""" - unauthed_token = self.request.session.get(ORKUT_UNAUTHORIZED_TOKEN_NAME) + name = self.AUTH_BACKEND.name + 'unauthorized_token_name' + unauthed_token = self.request.session.get(name) if not unauthed_token: raise ValueError, 'Missing unauthorized token' token = OAuthToken.from_string(unauthed_token) if token.key != self.request.GET.get('oauth_token', 'no-token'): raise ValueError, 'Incorrect tokens' + access_token = self.access_token(token) data = self.user_data(access_token) if data is not None: data['access_token'] = access_token.to_string() - return authenticate(response=data, orkut=True) + + return authenticate(**{'response': data, self.AUTH_BACKEND.name: True}) def unauthorized_token(self): """Return request for unauthorized token (first stage)""" - request = self.oauth_request(token=None, url=ORKUT_REQUEST_TOKEN_URL) + request = self.oauth_request(token=None, url=self.REQUEST_TOKEN_URL) response = self.fetch_response(request) return OAuthToken.from_string(response) - def oauth_request(self, token, url, params={}): + def oauth_request(self, token, url, extra_params=None): """Generate OAuth request, setups callback url""" - params.update({'oauth_callback': self.redirect_uri, \ - 'scope': ORKUT_SCOPE}) + params = {'oauth_callback': self.redirect_uri} + if extra_params: + params.update(extra_params) + if 'oauth_verifier' in self.request.GET: params['oauth_verifier'] = self.request.GET['oauth_verifier'] request = OAuthRequest.from_consumer_and_token(self.consumer, @@ -183,30 +207,19 @@ class OrkutAuth(BaseOAuth): def access_token(self, token): """Return request for access token value""" - request = self.oauth_request(token, ORKUT_ACCESS_TOKEN_URL) + request = self.oauth_request(token, self.ACCESS_TOKEN_URL) return OAuthToken.from_string(self.fetch_response(request)) def user_data(self, access_token): """Loads user data from service""" - params = {'method': 'people.get', \ - 'id': 'myself', \ - 'userId': '@me', \ - 'groupId': '@self', \ - 'fields': 'name,displayName,emails,%s' % ORKUT_EXTRA_DATA} - request = self.oauth_request(access_token, ORKUT_REST_ENDPOINT, params) - response = urllib.urlopen(request.to_url()).read() - try: - json = simplejson.loads(response) - return json['data'] - except simplejson.JSONDecodeError: - return None + raise NotImplementedError, 'Implement in subclass' @property def connection(self): """Setups connection""" conn = getattr(self, '_connection', None) if conn is None: - conn = httplib.HTTPSConnection(ORKUT_SERVER) + conn = httplib.HTTPSConnection(self.SERVER_URL) setattr(self, '_connection', conn) return conn @@ -215,45 +228,56 @@ class OrkutAuth(BaseOAuth): """Setups consumer""" cons = getattr(self, '_consumer', None) if cons is None: - cons = OAuthConsumer(settings.ORKUT_CONSUMER_KEY, - settings.ORKUT_CONSUMER_SECRET) + cons = OAuthConsumer(*self.get_key_and_secret()) setattr(self, '_consumer', cons) return cons + def get_key_and_secret(self): + """Return tuple with Consumer Key and Consumer Secret for current + service provider. Must return (key, secret), order must be respected. + """ + raise NotImplementedError, 'Implement in subclass' -class TwitterAuth(BaseOAuth): - """Twitter OAuth authentication mechanism""" - def auth_url(self): - """Returns redirect url""" - token = self.unauthorized_token() - self.request.session[TWITTER_UNAUTHORIZED_TOKEN_NAME] = token.to_string() - return self.oauth_request(token, TWITTER_AUTHORIZATION_URL).to_url() - def auth_complete(self): - """Returns user, might be logged in""" - unauthed_token = self.request.session.get(TWITTER_UNAUTHORIZED_TOKEN_NAME) - if not unauthed_token: - raise ValueError, 'Missing unauthorized token' +class OrkutAuth(ConsumerBasedOAuth): + """Orkut OAuth authentication mechanism""" + AUTHORIZATION_URL = ORKUT_AUTHORIZATION_URL + REQUEST_TOKEN_URL = ORKUT_REQUEST_TOKEN_URL + ACCESS_TOKEN_URL = ORKUT_ACCESS_TOKEN_URL + SERVER_URL = ORKUT_SERVER + AUTH_BACKEND = OrkutBackend - token = OAuthToken.from_string(unauthed_token) - if token.key != self.request.GET.get('oauth_token', 'no-token'): - raise ValueError, 'Incorrect tokens' - access_token = self.access_token(token) - data = self.user_data(access_token) - if data is not None: - data['access_token'] = access_token.to_string() - return authenticate(response=data, twitter=True) + def user_data(self, access_token): + """Loads user data from Orkut service""" + fields = 'name,displayName,emails' + if ORKUT_EXTRA_DATA: + fields += ',' + ORKUT_EXTRA_DATA + params = {'method': 'people.get', + 'id': 'myself', + 'userId': '@me', + 'groupId': '@self', + 'fields': fields, + 'scope': ORKUT_SCOPE} + request = self.oauth_request(access_token, ORKUT_REST_ENDPOINT, params) + response = urllib.urlopen(request.to_url()).read() + try: + json = simplejson.loads(response) + return json['data'] + except simplejson.JSONDecodeError: + return None - def unauthorized_token(self): - """Return request for unauthorized token (first stage)""" - request = self.oauth_request(token=None, url=TWITTER_REQUEST_TOKEN_URL) - response = self.fetch_response(request) - return OAuthToken.from_string(response) + def get_key_and_secret(self): + """Return Orkut Consumer Key and Consumer Secret pair""" + return settings.ORKUT_CONSUMER_KEY, settings.ORKUT_CONSUMER_SECRET - def access_token(self, token): - """Return request for access token value""" - request = self.oauth_request(token, TWITTER_ACCESS_TOKEN_URL) - return OAuthToken.from_string(self.fetch_response(request)) + +class TwitterAuth(ConsumerBasedOAuth): + """Twitter OAuth authentication mechanism""" + AUTHORIZATION_URL = TWITTER_AUTHORIZATION_URL + REQUEST_TOKEN_URL = TWITTER_REQUEST_TOKEN_URL + ACCESS_TOKEN_URL = TWITTER_ACCESS_TOKEN_URL + SERVER_URL = TWITTER_SERVER + AUTH_BACKEND = TwitterBackend def user_data(self, access_token): """Return user data provided""" @@ -264,43 +288,9 @@ class TwitterAuth(BaseOAuth): except simplejson.JSONDecodeError: return None - def oauth_request(self, token, url): - """Generate OAuth request, setups callback url""" - params = {'oauth_callback': self.redirect_uri} - if 'oauth_verifier' in self.request.GET: - params['oauth_verifier'] = self.request.GET['oauth_verifier'] - request = OAuthRequest.from_consumer_and_token(self.consumer, - token=token, - http_url=url, - parameters=params) - request.sign_request(OAuthSignatureMethod_HMAC_SHA1(), self.consumer, - token) - return request - - def fetch_response(self, request): - """Executes request and fetchs service response""" - self.connection.request(request.http_method, request.to_url()) - response = self.connection.getresponse() - return response.read() - - @property - def connection(self): - """Setups connection""" - conn = getattr(self, '_connection', None) - if conn is None: - conn = httplib.HTTPSConnection(TWITTER_SERVER) - setattr(self, '_connection', conn) - return conn - - @property - def consumer(self): - """Setups consumer""" - cons = getattr(self, '_consumer', None) - if cons is None: - cons = OAuthConsumer(settings.TWITTER_CONSUMER_KEY, - settings.TWITTER_CONSUMER_SECRET) - setattr(self, '_consumer', cons) - return cons + def get_key_and_secret(self): + """Return Twitter Consumer Key and Consumer Secret pair""" + return settings.TWITTER_CONSUMER_KEY, settings.TWITTER_CONSUMER_SECRET class FacebookAuth(BaseOAuth): @@ -333,7 +323,8 @@ class FacebookAuth(BaseOAuth): if 'error' in data: raise ValueError, 'Authentication error' data['access_token'] = access_token - return authenticate(response=data, facebook=True) + return authenticate(**{'response': data, + FacebookBackend.name: True}) else: raise ValueError, 'Authentication error' diff --git a/social_auth/backends.py b/social_auth/backends.py index f6640ce..724fe0d 100644 --- a/social_auth/backends.py +++ b/social_auth/backends.py @@ -22,11 +22,6 @@ class TwitterBackend(OAuthBackend): """Twitter OAuth authentication backend""" name = 'twitter' - def authenticate(self, **kwargs): - """Authenticate user only if this was a Twitter request""" - if kwargs.pop('twitter', False): - return super(TwitterBackend, self).authenticate(**kwargs) - def get_user_details(self, response): """Return user details from Twitter account""" return {'email': '', # not supplied @@ -40,11 +35,6 @@ class OrkutBackend(OAuthBackend): """Orkut OAuth authentication backend""" name = 'orkut' - def authenticate(self, **kwargs): - """Authenticate user only if this was a Orkut request""" - if kwargs.pop('orkut', False): - return super(OrkutBackend, self).authenticate(**kwargs) - def get_user_details(self, response): """Return user details from Orkut account""" return {'email': response['emails'][0]['value'], @@ -58,11 +48,6 @@ class FacebookBackend(OAuthBackend): """Facebook OAuth authentication backend""" name = 'facebook' - def authenticate(self, **kwargs): - """Authenticate user only if this was a Facebook request""" - if kwargs.pop('facebook', False): - return super(FacebookBackend, self).authenticate(**kwargs) - def get_user_details(self, response): """Return user details from Facebook account""" return {'email': response.get('email', ''), @@ -76,11 +61,6 @@ class OpenIDBackend(SocialAuthBackend): """Generic OpenID authentication backend""" name = 'openid' - def authenticate(self, **kwargs): - """Authenticate the user based on an OpenID response.""" - if kwargs.pop('openid', False): - return super(OpenIDBackend, self).authenticate(**kwargs) - def get_user_id(self, details, response): """Return user unique id provided by service""" return response.identity_url diff --git a/social_auth/base.py b/social_auth/base.py index ed9929e..422f9b2 100644 --- a/social_auth/base.py +++ b/social_auth/base.py @@ -44,14 +44,20 @@ class SocialAuthBackend(ModelBackend): name = '' # provider name, it's stored in database def authenticate(self, **kwargs): - """Authenticate the user based on an OAuth response.""" - # Require that the OAuth response be passed in as a keyword - # argument, to make sure we don't match the username/password - # calling conventions of authenticate. - response = kwargs.get('response') - if response is None: + """Authenticate user using social credentials + + Authentication is made if this is the correct backend, backend + verification is made by kwargs inspection for current backend + name presence. + """ + + # Validate backend and arguments. Require that the OAuth response + # be passed in as a keyword argument, to make sure we don't match + # the username/password calling conventions of authenticate. + if not (self.name and kwargs.get(self.name) and 'response' in kwargs): return None + response = kwargs.get('response') details = self.get_user_details(response) uid = self.get_user_id(details, response) try: diff --git a/social_auth/conf.py b/social_auth/conf.py index 86b8f1a..91cf7a5 100644 --- a/social_auth/conf.py +++ b/social_auth/conf.py @@ -5,7 +5,6 @@ TWITTER_REQUEST_TOKEN_URL = 'https://%s/oauth/request_token' % TWITTER_SER TWITTER_ACCESS_TOKEN_URL = 'https://%s/oauth/access_token' % TWITTER_SERVER TWITTER_AUTHORIZATION_URL = 'http://%s/oauth/authorize' % TWITTER_SERVER TWITTER_CHECK_AUTH = 'https://twitter.com/account/verify_credentials.json' -TWITTER_UNAUTHORIZED_TOKEN_NAME = 'twitter_unauthorized_token' # Facebook configuration FACEBOOK_SERVER = 'graph.facebook.com' @@ -20,8 +19,7 @@ ORKUT_ACCESS_TOKEN_URL = 'https://%s/accounts/OAuthGetAccessToken' % ORKU ORKUT_AUTHORIZATION_URL = 'https://%s/accounts/OAuthAuthorizeToken' % ORKUT_SERVER ORKUT_SCOPE = 'http://orkut.gmodules.com/social/' ORKUT_REST_ENDPOINT = 'http://www.orkut.com/social/rpc' -ORKUT_UNAUTHORIZED_TOKEN_NAME = 'orkut_unauthorized_token' -ORKUT_EXTRA_DATA = 'age,gender,currentLocation' +ORKUT_EXTRA_DATA = '' # OpenID configuration OLD_AX_ATTRS = [