Browse Source

feat(social auth): replace oauth2 with requests_oauthlib (#16863)

josh 5 years ago
parent
commit
0bfe540d62

+ 0 - 1
requirements-base.txt

@@ -42,7 +42,6 @@ mistune>0.7,<0.9
 mmh3>=2.3.1,<2.4
 mock==3.0.5 ; python_version < "3.3"
 msgpack>=0.6.1,<0.7.0
-oauth2>=1.5.167
 parsimonious==0.8.0
 percy>=1.1.2
 petname>=2.6,<2.7

+ 38 - 32
src/social_auth/backends/__init__.py

@@ -16,6 +16,7 @@ import requests
 import six
 import threading
 
+from requests_oauthlib import OAuth1
 from django.contrib.auth import authenticate
 from django.utils.crypto import get_random_string, constant_time_compare
 from six.moves.urllib.error import HTTPError
@@ -29,6 +30,7 @@ from social_auth.utils import (
     clean_partial_pipeline,
     url_add_parameters,
     dsa_urlopen,
+    parse_qs,
 )
 from social_auth.exceptions import (
     StopPipeline,
@@ -41,8 +43,6 @@ from social_auth.exceptions import (
     AuthStateForbidden,
     BackendError,
 )
-from social_auth.backends.utils import build_consumer_oauth_request
-from oauth2 import Consumer as OAuthConsumer, Token, Request as OAuthRequest
 
 from sentry.utils import json
 
@@ -359,7 +359,7 @@ class BaseAuth(object):
         return uri
 
 
-class BaseOAuth(BaseAuth):
+class OAuthAuth(BaseAuth):
     """OAuth base class"""
 
     SETTINGS_KEY_NAME = ""
@@ -371,7 +371,7 @@ class BaseOAuth(BaseAuth):
 
     def __init__(self, request, redirect):
         """Init method"""
-        super(BaseOAuth, self).__init__(request, redirect)
+        super(OAuthAuth, self).__init__(request, redirect)
         self.redirect_uri = self.build_absolute_uri(self.redirect)
 
     @classmethod
@@ -405,7 +405,7 @@ class BaseOAuth(BaseAuth):
         return {}
 
 
-class ConsumerBasedOAuth(BaseOAuth):
+class BaseOAuth1(OAuthAuth):
     """Consumer based mechanism OAuth authentication, fill the needed
     parameters to communicate properly with authentication service.
 
@@ -426,7 +426,7 @@ class ConsumerBasedOAuth(BaseOAuth):
             self.request.session[name] = []
         self.request.session[name].append(token.to_string())
         self.request.session.modified = True
-        return self.oauth_authorization_request(token).to_url()
+        return self.oauth_authorization_request(token)
 
     def auth_complete(self, *args, **kwargs):
         """Return user, might be logged in"""
@@ -437,8 +437,10 @@ class ConsumerBasedOAuth(BaseOAuth):
         if not unauthed_tokens:
             raise AuthTokenError(self, "Missing unauthorized token")
         for unauthed_token in unauthed_tokens:
-            token = Token.from_string(unauthed_token)
-            if token.key == self.data.get("oauth_token", "no-token"):
+            token = unauthed_token
+            if not isinstance(unauthed_token, dict):
+                token = parse_qs(unauthed_token)
+            if token.get("oauth_token") == self.data.get("oauth_token"):
                 unauthed_tokens = list(set(unauthed_tokens) - set([unauthed_token]))
                 self.request.session[name] = unauthed_tokens
                 self.request.session.modified = True
@@ -457,9 +459,6 @@ class ConsumerBasedOAuth(BaseOAuth):
 
     def do_auth(self, access_token, *args, **kwargs):
         """Finish the auth process once the access_token was retrieved"""
-        if isinstance(access_token, six.string_types):
-            access_token = Token.from_string(access_token)
-
         data = self.user_data(access_token)
         if data is not None:
             data["access_token"] = access_token.to_string()
@@ -469,29 +468,42 @@ class ConsumerBasedOAuth(BaseOAuth):
 
     def unauthorized_token(self):
         """Return request for unauthorized token (first stage)"""
-        request = self.oauth_request(
-            token=None,
+        params = self.request_token_extra_arguments()
+        params.update(self.get_scope_argument())
+        key, secret = self.get_key_and_secret()
+        response = self.request(
             url=self.REQUEST_TOKEN_URL,
-            extra_params=self.request_token_extra_arguments(),
+            params=params,
+            auth=OAuth1(key, secret, callback_uri=self.redirect_uri),
         )
-        return Token.from_string(self.fetch_response(request))
+        return response.content
 
     def oauth_authorization_request(self, token):
         """Generate OAuth request to authorize token."""
+        if not isinstance(token, dict):
+            token = parse_qs(token)
         params = self.auth_extra_arguments() or {}
         params.update(self.get_scope_argument())
-        return OAuthRequest.from_token_and_callback(
-            token=token,
-            callback=self.redirect_uri,
-            http_url=self.AUTHORIZATION_URL,
-            parameters=params,
+        params["oauth_token"] = token.get("oauth_token")
+        params["redirect_uri"] = self.redirect_uri
+        return self.AUTHORIZATION_URL + "?" + urlencode(params)
+
+    def oauth_auth(self, token=None, oauth_verifier=None):
+        key, secret = self.get_key_and_secret()
+        oauth_verifier = oauth_verifier or self.data.get("oauth_verifier")
+        token = token or {}
+        return OAuth1(
+            key,
+            secret,
+            resource_owner_key=token.get("oauth_token"),
+            resource_owner_secret=token.get("oauth_token_secret"),
+            callback_uri=self.redirect_uri,
+            verifier=oauth_verifier,
         )
 
-    def oauth_request(self, token, url, extra_params=None):
+    def oauth_request(self, token, url, extra_params=None, method="GET"):
         """Generate OAuth request, setups callback url"""
-        return build_consumer_oauth_request(
-            self, token, url, self.redirect_uri, self.data.get("oauth_verifier"), extra_params
-        )
+        return self.request(url, auth=self.oauth_auth(token))
 
     def fetch_response(self, request):
         """Executes request and fetchs service response"""
@@ -500,16 +512,10 @@ class ConsumerBasedOAuth(BaseOAuth):
 
     def access_token(self, token):
         """Return request for access token value"""
-        request = self.oauth_request(token, self.ACCESS_TOKEN_URL)
-        return Token.from_string(self.fetch_response(request))
-
-    @property
-    def consumer(self):
-        """Setups consumer"""
-        return OAuthConsumer(*self.get_key_and_secret())
+        return self.get_querystring(self.ACCESS_TOKEN_URL, auth=self.oauth_auth(token))
 
 
-class BaseOAuth2(BaseOAuth):
+class BaseOAuth2(OAuthAuth):
     """Base class for OAuth2 providers.
 
     OAuth2 draft details at:

+ 2 - 2
src/social_auth/backends/bitbucket.py

@@ -14,7 +14,7 @@ from __future__ import absolute_import
 
 import simplejson
 
-from social_auth.backends import ConsumerBasedOAuth, OAuthBackend
+from social_auth.backends import BaseOAuth1, OAuthBackend
 from social_auth.utils import dsa_urlopen
 
 # Bitbucket configuration
@@ -67,7 +67,7 @@ class BitbucketBackend(OAuthBackend):
         return token
 
 
-class BitbucketAuth(ConsumerBasedOAuth):
+class BitbucketAuth(BaseOAuth1):
     """Bitbucket OAuth authentication mechanism"""
 
     AUTHORIZATION_URL = BITBUCKET_AUTHORIZATION_URL

+ 0 - 52
src/social_auth/backends/utils.py

@@ -1,52 +0,0 @@
-from __future__ import absolute_import
-
-from oauth2 import (
-    Consumer as OAuthConsumer,
-    Token,
-    Request as OAuthRequest,
-    SignatureMethod_HMAC_SHA1,
-    HTTP_METHOD,
-)
-
-import simplejson
-
-from social_auth.models import UserSocialAuth
-from social_auth.utils import dsa_urlopen
-
-
-def consumer_oauth_url_request(backend, url, user_or_id, redirect_uri="/", json=True):
-    """Builds and retrieves an OAuth signed response."""
-    user = UserSocialAuth.resolve_user_or_id(user_or_id)
-    oauth_info = user.social_auth.filter(provider=backend.AUTH_BACKEND.name)[0]
-    token = Token.from_string(oauth_info.tokens["access_token"])
-    request = build_consumer_oauth_request(backend, token, url, redirect_uri)
-    response = "\n".join(dsa_urlopen(request.to_url()).readlines())
-
-    if json:
-        response = simplejson.loads(response)
-    return response
-
-
-def build_consumer_oauth_request(
-    backend,
-    token,
-    url,
-    redirect_uri="/",
-    oauth_verifier=None,
-    extra_params=None,
-    method=HTTP_METHOD,
-):
-    """Builds a Consumer OAuth request."""
-    params = {"oauth_callback": redirect_uri}
-    if extra_params:
-        params.update(extra_params)
-
-    if oauth_verifier:
-        params["oauth_verifier"] = oauth_verifier
-
-    consumer = OAuthConsumer(*backend.get_key_and_secret())
-    request = OAuthRequest.from_consumer_and_token(
-        consumer, token=token, http_method=method, http_url=url, parameters=params
-    )
-    request.sign_request(SignatureMethod_HMAC_SHA1(), consumer, token)
-    return request

+ 11 - 0
src/social_auth/utils.py

@@ -3,6 +3,8 @@ from __future__ import absolute_import
 import random
 import logging
 from importlib import import_module
+import six
+from six.moves.urllib.parse import parse_qs as urlparse_parse_qs
 
 from cgi import parse_qsl
 from django.conf import settings
@@ -131,6 +133,15 @@ def module_member(name):
     return getattr(module, member)
 
 
+def parse_qs(value):
+    """Like urlparse.parse_qs but transform list values to single items"""
+    return drop_lists(urlparse_parse_qs(value))
+
+
+def drop_lists(value):
+    return dict((key, val[0]) for key, val in six.iteritems(value))
+
+
 if __name__ == "__main__":
     import doctest