]> git.parisson.com Git - django-social-auth.git/commitdiff
Google OAuth2 support
authorMatías Aguirre <matiasaguirre@gmail.com>
Tue, 15 Mar 2011 21:27:26 +0000 (18:27 -0300)
committerMatías Aguirre <matiasaguirre@gmail.com>
Tue, 15 Mar 2011 21:27:26 +0000 (18:27 -0300)
README.rst
example/local_settings.py.template
example/settings.py
example/templates/done.html
example/templates/home.html
social_auth/backends/__init__.py
social_auth/backends/google.py

index 4602fdcc827fbbb04467dd6539b9a058151bddd9..fb81313bd8aaa0f8c501b55cbc5a1b6e4888f757 100644 (file)
@@ -102,6 +102,7 @@ Configuration
         'social_auth.backends.twitter.TwitterBackend',
         'social_auth.backends.facebook.FacebookBackend',
         'social_auth.backends.google.GoogleOAuthBackend',
+        'social_auth.backends.google.GoogleOAuth2Backend',
         'social_auth.backends.google.GoogleBackend',
         'social_auth.backends.yahoo.YahooBackend',
         'social_auth.backends.contrib.linkedin.LinkedinBackend',
@@ -415,7 +416,30 @@ anonymous values will be used if not configured as described in their
 
       GOOGLE_OAUTH_EXTRA_SCOPE = [...]
 
-check which Apps are included in their `Google Data Protocol Directory`_
+Check which applications can be included in their `Google Data Protocol Directory`_
+
+-------------
+Google OAuth2
+-------------
+Recently Google launched OAuth2 support following the definition at
+`OAuth2 draft`. It works in a similar way to plain OAuth mechanism, but
+developers *must* register an application and apply for a set of keys. Check
+`Google OAuth2`_ document for details.
+
+To enable OAuth2 support:
+
+- fill "Client Key" and "Client Secret" settings, these values can be obtained
+  easily as described on `OAuth2 Registering`_ doc::
+
+      GOOGLE_OAUTH2_CLIENT_KEY = ''
+      GOOGLE_OAUTH2_CLIENT_SECRET = ''
+
+- scopes are shared between OAuth mechanisms::
+
+      GOOGLE_OAUTH_EXTRA_SCOPE = [...]
+
+Check which applications can be included in their `Google Data Protocol Directory`_
+
 
 -------
 Testing
@@ -513,7 +537,10 @@ Base work is copyrighted by:
 .. _Orkut API:  http://code.google.com/apis/orkut/docs/rest/developers_guide_protocol.html#Authenticating
 .. _Google OpenID: http://code.google.com/apis/accounts/docs/OpenID.html
 .. _Google OAuth: http://code.google.com/apis/accounts/docs/OAuth.html
+.. _Google OAuth2: http://code.google.com/apis/accounts/docs/OAuth2.html
+.. _OAuth2 Registering: http://code.google.com/apis/accounts/docs/OAuth2.html#Registering
 .. _Google Data Protocol Directory: http://code.google.com/apis/gdata/docs/directory.html
+.. _OAuth2 draft: http://tools.ietf.org/html/draft-ietf-oauth-v2-10
 .. _OAuth reference: http://code.google.com/apis/accounts/docs/OAuth_ref.html#SigningOAuth
 .. _Yahoo OpenID: http://openid.yahoo.com/
 .. _Twitter OAuth: http://dev.twitter.com/pages/oauth_faq
index c4261cff1b66ce7ec7a32e0249dd9c8b7fed7568..a8f1f595baac495e92dafa4b953bfdde020c99e3 100644 (file)
@@ -6,6 +6,8 @@ LINKEDIN_CONSUMER_KEY             = ''
 LINKEDIN_CONSUMER_SECRET          = ''
 ORKUT_CONSUMER_KEY                = ''
 ORKUT_CONSUMER_SECRET             = ''
+GOOGLE_OAUTH2_CLIENT_KEY          = ''
+GOOGLE_OAUTH2_CLIENT_SECRET       = ''
 SOCIAL_AUTH_CREATE_USERS          = True
 SOCIAL_AUTH_FORCE_RANDOM_USERNAME = False
 SOCIAL_AUTH_DEFAULT_USERNAME      = 'socialauth_user'
index a0474cc9e1e76683c899172b56de3524a7b72a09..689f9d1b08955cb4f709213464abfd5bfc2a99b2 100644 (file)
@@ -65,6 +65,7 @@ AUTHENTICATION_BACKENDS = (
     'social_auth.backends.twitter.TwitterBackend',
     'social_auth.backends.facebook.FacebookBackend',
     'social_auth.backends.google.GoogleOAuthBackend',
+    'social_auth.backends.google.GoogleOAuth2Backend',
     'social_auth.backends.google.GoogleBackend',
     'social_auth.backends.yahoo.YahooBackend',
     'social_auth.backends.contrib.linkedin.LinkedinBackend',
index fa2bcdb6a86ffe4759adf1131751baa2da6a013b..b057342ed3d1d196ec30f2975202ce10b2b265c8 100644 (file)
       {% if orkut %}<span class="disconnect">(<a href="{% url disconnect "orkut" %}">disconnect</a>)</span>{% endif %}
     </li>
     <li>
-      <a rel="nofollow" href="{% url associate_begin "google-oauth" %}">Google</a>
+      <a rel="nofollow" href="{% url associate_begin "google-oauth" %}">Google OAuth</a>
       {% if google_oauth %}<span class="disconnect">(<a href="{% url disconnect "google-oauth" %}">disconnect</a>)</span>{% endif %}
     </li>
   </ul>
 
+  <h3>Associate new OAuth2 credentials:</h3>
+  <ul>
+    <li>
+      <a rel="nofollow" href="{% url associate_begin "google-oauth2" %}">Google OAuth2</a>
+      {% if google_oauth2 %}<span class="disconnect">(<a href="{% url disconnect "google-oauth2" %}">disconnect</a>)</span>{% endif %}
+    </li>
+  </ul>
+
   <h3>Associate new OpenID credentials:</h3>
   <ul>
     <li>
-      <a rel="nofollow" href="{% url associate_begin "google" %}">Google</a>
+      <a rel="nofollow" href="{% url associate_begin "google" %}">Google OpenID</a>
       {% if google %}<span class="disconnect">(<a href="{% url disconnect "google" %}">disconnect</a>)</span>{% endif %}
     </li>
     <li>
index d9dbf5149aa45842656aed31492d54978346b0e1..d778d83364bdd36256d8f8fdbbfa1cb5f49c7a27 100644 (file)
   </ul>
 </div>
 
+<div>
+  <h3>Login using <a href="http://tools.ietf.org/html/draft-ietf-oauth-v2-10" title="OAuth2">OAuth2</a> from:</h3>
+  <ul>
+    <li><a rel="nofollow" href="{% url begin "google-oauth2" %}">Google OAuth2</a></li>
+  </ul>
+</div>
+
 <div>
   <h3>Login using <a href="http://openid.net/" title="OpenId">OpenId</a> from:</h3>
   <ul>
index fa482bbca36a184a12e14bfa9be4137c9d202ca7..1ae3a9b4b2bea4caaf93247159f9c17bd278a19d 100644 (file)
@@ -11,20 +11,21 @@ enabled.
 """
 from os import urandom, walk
 from os.path import basename
+from urllib2 import Request, urlopen
+from urllib import urlencode
 from httplib import HTTPSConnection
 
 from openid.consumer.consumer import Consumer, SUCCESS, CANCEL, FAILURE
 from openid.consumer.discover import DiscoveryFailure
 from openid.extensions import sreg, ax
 
-from oauth2 import Consumer as OAuthConsumer, \
-                   Token as OAuthToken, \
-                   Request as OAuthRequest, \
-                   SignatureMethod_HMAC_SHA1 as OAuthSignatureMethod_HMAC_SHA1
+from oauth2 import Consumer as OAuthConsumer, Token, Request as OAuthRequest, \
+                   SignatureMethod_HMAC_SHA1
 
 from django.conf import settings
 from django.contrib.auth import authenticate
 from django.contrib.auth.backends import ModelBackend
+from django.utils import simplejson
 from django.utils.hashcompat import md5_constructor
 from django.utils.importlib import import_module
 
@@ -495,20 +496,20 @@ class ConsumerBasedOAuth(BaseOAuth):
     SETTINGS_SECRET_NAME = ''
 
     def auth_url(self):
-        """Returns redirect url"""
+        """Return redirect url"""
         token = self.unauthorized_token()
         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, *args, **kwargs):
-        """Returns user, might be logged in"""
+        """Return user, might be logged in"""
         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)
+        token = Token.from_string(unauthed_token)
         if token.key != self.data.get('oauth_token', 'no-token'):
             raise ValueError('Incorrect tokens')
 
@@ -524,7 +525,7 @@ class ConsumerBasedOAuth(BaseOAuth):
         """Return request for unauthorized token (first stage)"""
         request = self.oauth_request(token=None, url=self.REQUEST_TOKEN_URL)
         response = self.fetch_response(request)
-        return OAuthToken.from_string(response)
+        return Token.from_string(response)
 
     def oauth_request(self, token, url, extra_params=None):
         """Generate OAuth request, setups callback url"""
@@ -538,42 +539,32 @@ class ConsumerBasedOAuth(BaseOAuth):
                                                        token=token,
                                                        http_url=url,
                                                        parameters=params)
-        request.sign_request(OAuthSignatureMethod_HMAC_SHA1(), self.consumer,
-                             token)
+        request.sign_request(SignatureMethod_HMAC_SHA1(), self.consumer, token)
         return request
 
     def fetch_response(self, request):
         """Executes request and fetchs service response"""
-        self.connection.request(request.method, request.to_url())
-        response = self.connection.getresponse()
-        return response.read()
+        connection = HTTPSConnection(self.SERVER_URL)
+        connection.request(request.method.upper(), request.to_url())
+        return connection.getresponse().read()
 
     def access_token(self, token):
         """Return request for access token value"""
         request = self.oauth_request(token, self.ACCESS_TOKEN_URL)
-        return OAuthToken.from_string(self.fetch_response(request))
+        return Token.from_string(self.fetch_response(request))
 
     def user_data(self, access_token):
         """Loads user data from service"""
         raise NotImplementedError('Implement in subclass')
 
-    @property
-    def connection(self):
-        """Setups connection"""
-        conn = getattr(self, '_connection', None)
-        if conn is None:
-            conn = HTTPSConnection(self.SERVER_URL)
-            setattr(self, '_connection', conn)
-        return conn
-
     @property
     def consumer(self):
         """Setups consumer"""
-        cons = getattr(self, '_consumer', None)
-        if cons is None:
-            cons = OAuthConsumer(*self.get_key_and_secret())
-            setattr(self, '_consumer', cons)
-        return cons
+        consumer = getattr(self, '_consumer', None)
+        if consumer is None:
+            consumer = OAuthConsumer(*self.get_key_and_secret())
+            setattr(self, '_consumer', consumer)
+        return consumer
 
     def get_key_and_secret(self):
         """Return tuple with Consumer Key and Consumer Secret for current
@@ -586,8 +577,66 @@ class ConsumerBasedOAuth(BaseOAuth):
     def enabled(cls):
         """Return backend enabled status by checking basic settings"""
         return all(hasattr(settings, name) for name in
-                        (cls.SETTINGS_KEY_NAME,
-                         cls.SETTINGS_SECRET_NAME))
+                        (cls.SETTINGS_KEY_NAME, cls.SETTINGS_SECRET_NAME))
+
+
+class BaseOAuth2(BaseOAuth):
+    """Base class for OAuth2 providers.
+
+    OAuth2 draft details at:
+        http://tools.ietf.org/html/draft-ietf-oauth-v2-10
+
+    Attributes:
+        @AUTHORIZATION_URL       Authorization service url
+        @ACCESS_TOKEN_URL        Token URL
+    """
+    AUTHORIZATION_URL = None
+    ACCESS_TOKEN_URL = None
+
+    def auth_url(self):
+        """Return redirect url"""
+        client_id, client_secret = self.get_key_and_secret()
+        args = {'client_id': client_id,
+                'scope': ' '.join(self.get_scope()),
+                'redirect_uri': self.redirect_uri,
+                'response_type': 'code'}  # requesting code
+        return self.AUTHORIZATION_URL + '?' + urlencode(args)
+
+    def auth_complete(self, *args, **kwargs):
+        """Completes loging process, must return user instance"""
+        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
+                  'client_id': client_id,
+                  'client_secret': client_secret,
+                  'redirect_uri': self.redirect_uri}
+        headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+        request = Request(self.ACCESS_TOKEN_URL, data=urlencode(params),
+                          headers=headers)
+
+        try:
+            response = simplejson.loads(urlopen(request).read())
+        except (simplejson.JSONDecodeError, KeyError):
+            raise ValueError('Unknown OAuth2 response type')
+
+        if response.get('error'):
+            error = response.get('error_description') or response.get('error')
+            raise ValueError('OAuth2 authentication failed: %s' % error)
+        else:
+            response.update(self.user_data(response['access_token']) or {})
+            kwargs.update({'response': response, self.AUTH_BACKEND.name: True})
+            return authenticate(*args, **kwargs)
+
+    def get_scope(self):
+        """Return list with needed access scope"""
+        return []
+
+    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.
+        """
+        return getattr(settings, self.SETTINGS_KEY_NAME), \
+               getattr(settings, self.SETTINGS_SECRET_NAME)
 
 
 # import sources from where check for auth backends
index 981eb11c6516366535453d01875614dc9584efd2..215257bfbf1633a628a1daed55dca1dbbdbd5081 100644 (file)
@@ -8,28 +8,39 @@ and GOOGLE_CONSUMER_SECRET and they will be used in the auth process.
 Setting GOOGLE_OAUTH_EXTRA_SCOPE can be used to access different user
 related data, like calendar, contacts, docs, etc.
 
+OAuth2 works similar to OAuth but application must be defined on Google
+APIs console https://code.google.com/apis/console/ Identity option.
+
 OpenID also works straightforward, it doesn't need further configurations.
 """
+from urllib import urlencode
 from urllib2 import Request, urlopen
 
 from django.conf import settings
 from django.utils import simplejson
 
-from social_auth.backends import OpenIdAuth, ConsumerBasedOAuth, \
+from social_auth.backends import OpenIdAuth, ConsumerBasedOAuth, BaseOAuth2, \
                                  OAuthBackend, OpenIDBackend, USERNAME
 
 
 # Google OAuth base configuration
-GOOGLE_SERVER = 'www.google.com'
-GOOGLE_REQUEST_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetRequestToken'
-GOOGLE_ACCESS_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetAccessToken'
-GOOGLE_AUTHORIZATION_URL = 'https://www.google.com/accounts/OAuthAuthorizeToken'
+GOOGLE_OAUTH_SERVER = 'www.google.com'
+GOOGLE_OAUTH_AUTHORIZATION_URL = 'https://www.google.com/accounts/OAuthAuthorizeToken'
+GOOGLE_OAUTH_REQUEST_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetRequestToken'
+GOOGLE_OAUTH_ACCESS_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetAccessToken'
+
+# Google OAuth2 base configuration
+GOOGLE_OAUTH2_SERVER = 'accounts.google.com'
+GOOGLE_OATUH2_AUTHORIZATION_URL = 'https://accounts.google.com/o/oauth2/auth'
+
 # scope for user email, specify extra scopes in settings, for example:
 # GOOGLE_OAUTH_EXTRA_SCOPE = ['https://www.google.com/m8/feeds/']
 GOOGLE_OAUTH_SCOPE = ['https://www.googleapis.com/auth/userinfo#email']
 GOOGLEAPIS_EMAIL = 'https://www.googleapis.com/userinfo/email'
 GOOGLE_OPENID_URL = 'https://www.google.com/accounts/o8/id'
 
+EXPIRES_NAME = getattr(settings, 'SOCIAL_AUTH_EXPIRATION', 'expires')
+
 
 # Backends
 class GoogleOAuthBackend(OAuthBackend):
@@ -50,6 +61,13 @@ class GoogleOAuthBackend(OAuthBackend):
                 'last_name': ''}
 
 
+class GoogleOAuth2Backend(GoogleOAuthBackend):
+    """Google OAuth2 authentication backend"""
+    name = 'google-oauth2'
+    EXTRA_DATA = [('refresh_token', 'refresh_token'),
+                  ('expires_in', EXPIRES_NAME)]
+
+
 class GoogleBackend(OpenIDBackend):
     """Google OpenID authentication backend"""
     name = 'google'
@@ -67,10 +85,10 @@ class GoogleAuth(OpenIdAuth):
 
 class BaseGoogleOAuth(ConsumerBasedOAuth):
     """Base class for Google OAuth mechanism"""
-    AUTHORIZATION_URL = GOOGLE_AUTHORIZATION_URL
-    REQUEST_TOKEN_URL = GOOGLE_REQUEST_TOKEN_URL
-    ACCESS_TOKEN_URL = GOOGLE_ACCESS_TOKEN_URL
-    SERVER_URL = GOOGLE_SERVER
+    AUTHORIZATION_URL = GOOGLE_OAUTH_AUTHORIZATION_URL
+    REQUEST_TOKEN_URL = GOOGLE_OAUTH_REQUEST_TOKEN_URL
+    ACCESS_TOKEN_URL = GOOGLE_OAUTH_ACCESS_TOKEN_URL
+    SERVER_URL = GOOGLE_OAUTH_SERVER
 
     def user_data(self, access_token):
         """Loads user data from G service"""
@@ -84,23 +102,9 @@ class GoogleOAuth(BaseGoogleOAuth):
     SETTINGS_SECRET_NAME = 'GOOGLE_CONSUMER_SECRET'
 
     def user_data(self, access_token):
-        """Loads user data data from googleapis service, only email so far
-        as it's described in:
-            http://sites.google.com/site/oauthgoog/Home/emaildisplayscope
-        OAuth parameters needs to be passed in the queryset and
-        Authorization header, this behavior is listed in:
-            http://groups.google.com/group/oauth/browse_thread/thread/d15add9beb418ebc
-        """
-        url = self.oauth_request(access_token, GOOGLEAPIS_EMAIL,
-                                 {'alt': 'json'}).to_url()
-        params = url.split('?', 1)[1]
-        request = Request(url)
-        request.headers['Authorization'] = params  # setup header
-        response = urlopen(request).read()
-        try:
-            return simplejson.loads(response)['data']
-        except (simplejson.JSONDecodeError, KeyError):
-            return None
+        """Return user data from Google API"""
+        url = self.oauth_request(access_token, GOOGLEAPIS_EMAIL).to_url()
+        return googleapis_email(url.split('?')[1])
 
     def oauth_request(self, token, url, extra_params=None):
         extra_params = extra_params or {}
@@ -130,8 +134,44 @@ class GoogleOAuth(BaseGoogleOAuth):
         return True
 
 
+class GoogleOAuth2(BaseOAuth2):
+    """Google OAuth2 support"""
+    AUTH_BACKEND = GoogleOAuth2Backend
+    AUTHORIZATION_URL = 'https://accounts.google.com/o/oauth2/auth'
+    ACCESS_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'
+    SETTINGS_KEY_NAME = 'GOOGLE_OAUTH2_CLIENT_KEY'
+    SETTINGS_SECRET_NAME = 'GOOGLE_OAUTH2_CLIENT_SECRET'
+
+    def get_scope(self):
+        return GOOGLE_OAUTH_SCOPE + \
+               getattr(settings, 'GOOGLE_OAUTH_EXTRA_SCOPE', [])
+
+    def user_data(self, access_token):
+        """Return user data from Google API"""
+        return googleapis_email(urlencode({'oauth_token': access_token}))
+
+
+def googleapis_email(params):
+    """Loads user data from googleapis service, only email so far as it's
+    described in http://sites.google.com/site/oauthgoog/Home/emaildisplayscope
+
+    Parameters must be passed in queryset and Authorization header as described
+    on Google OAuth documentation at:
+        http://groups.google.com/group/oauth/browse_thread/thread/d15add9beb418ebc
+    and:
+        http://code.google.com/apis/accounts/docs/OAuth2.html#CallingAnAPI
+    """
+    request = Request(GOOGLEAPIS_EMAIL + '?alt=json&' + params,
+                      headers={'Authorization': params})
+    try:
+        return simplejson.loads(urlopen(request).read())['data']
+    except (simplejson.JSONDecodeError, KeyError, IOError):
+        return None
+
+
 # Backend definition
 BACKENDS = {
     'google': GoogleAuth,
     'google-oauth': GoogleOAuth,
+    'google-oauth2': GoogleOAuth2,
 }