]> git.parisson.com Git - django-social-auth.git/commitdiff
OAuth improvements.
authorMatías Aguirre <matiasaguirre@gmail.com>
Tue, 23 Nov 2010 15:33:33 +0000 (13:33 -0200)
committerMatías Aguirre <matiasaguirre@gmail.com>
Tue, 23 Nov 2010 15:33:33 +0000 (13:33 -0200)
Changes:
    - Parametrize backend name
    - Common base class between Twitter and Orkut authentication
      mechanism
    - Updated Doc
    - Updated example application with Orkut

README.rst
example/app/views.py
social_auth/auth.py
social_auth/backends.py
social_auth/base.py
social_auth/conf.py

index 060302944b5667b54d4381fe1357cd6001dbc772..1b6cc3e2955f703b714b1d97b862f3710e4fb77e 100644 (file)
@@ -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
index 125227f981e2a74dc102da6574586cae6aeb5126..07af2bcb1e085047bd5effa713484f1d3abfcd00 100644 (file)
@@ -19,6 +19,7 @@ def home(request):
             <ul>
               <li><a href="/login/twitter/">Twitter</a></li>
               <li><a href="/login/facebook/">Facebook</a></li>
+              <li><a href="/login/orkut/">Orkut</a></li>
             </ul>
           </div>
           <div>
index 61a5ef9c1d55597483c9736dab0984e523894cb5..a17a58fae87a51ab3d147c7222640b0d38883d1a 100644 (file)
@@ -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'
 
index f6640ceaff6e15c1b8b21c3c1e676e0fcd5dc9ea..724fe0dbe9d04525eebb956bbd8eb8097654a757 100644 (file)
@@ -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
index ed9929eff98ffcb1a9c6519a62ab5c14f53db5d7..422f9b21377d29ecd52694c13a55f6c70c18a8c0 100644 (file)
@@ -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:
index 86b8f1a8caa6ed17588fce3c1e95c4a2cfad73d6..91cf7a5a3a4e4a687ffc2a107effdfc620ce542e 100644 (file)
@@ -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 = [