Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Twitter OAuth using access_token #272

Closed
maryokhin opened this issue May 12, 2014 · 25 comments
Closed

Twitter OAuth using access_token #272

maryokhin opened this issue May 12, 2014 · 25 comments

Comments

@maryokhin
Copy link
Contributor

I am using Django REST Framework + Python-Social-Auth. I already got Facebook and Google OAuth2 working, but am having a problem using the same code to connect Twitter. I am having the same error as #107, but solution does not work because all work is done on the client and I have to work with keys in the request:

{
    "backend": "twitter",
    "access_token": "2478*************************************tdkiOW",
    "access_token_secret": "WgaYaEyHL******************************X6UpzAlTgITPa"
}
kwargs = {key: value for key, value in serializer.data.items() if key != 'backend'}
user = request.user
kwargs['user'] = user.is_authenticated() and user or None
user = strategy.backend.do_auth(**kwargs)

I also tried to do something like this:

if serializer.is_valid():
            backend = serializer.data['backend']
            oauth_token = serializer.data['access_token']
            oauth_token_secret = serializer.data['access_token_secret']
...
twitter = {'oauth_token': oauth_token, 'oauth_token_secret': oauth_token_secret}
user = strategy.backend.do_auth(twitter)

But no matter what I tried, I always got the same stacktrace

Environment:

Request Method: POST
Request URL: http://127.0.0.1:8000/social-auth/

Django Version: 1.7b3
Python Version: 3.4.0
Installed Applications:
('django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'rest_framework',
 'api',
 'rest_framework.authtoken',
 'social.apps.django_app.default')
Installed Middleware:
('django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware')

Traceback:
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/django/core/handlers/base.py" in get_response
  113.                     response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/django/views/generic/base.py" in view
  69.             return self.dispatch(request, *args, **kwargs)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/django/views/decorators/csrf.py" in wrapped_view
  57.         return view_func(*args, **kwargs)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/rest_framework/views.py" in dispatch
  400.             response = self.handle_exception(exc)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/rest_framework/views.py" in dispatch
  397.             response = handler(request, *args, **kwargs)
File "/Users/maryokhin/Workspace/backend/api/views/auth.py" in post
  54.           user = strategy.backend.do_auth(**kwargs)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/social/backends/oauth.py" in do_auth
  124.         data = self.user_data(access_token)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/social/backends/twitter.py" in user_data
  33.             auth=self.oauth_auth(access_token)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/social/backends/base.py" in get_json
  195.         return self.request(url, *args, **kwargs).json()
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/social/backends/base.py" in request
  188.             response = request(method, url, *args, **kwargs)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/requests/api.py" in request
  44.     return session.request(method=method, url=url, **kwargs)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/requests/sessions.py" in request
  349.         prep = self.prepare_request(req)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/requests/sessions.py" in prepare_request
  287.             hooks=merge_hooks(request.hooks, self.hooks),
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/requests/models.py" in prepare
  291.         self.prepare_auth(auth, url)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/requests/models.py" in prepare_auth
  470.             r = auth(self)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/requests_oauthlib/oauth1_auth.py" in __call__
  67.                 unicode(r.url), unicode(r.method), None, r.headers)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/oauthlib/oauth1/rfc5849/__init__.py" in sign
  280.         request.oauth_params.append(('oauth_signature', self.get_oauth_signature(request)))
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/oauthlib/oauth1/rfc5849/__init__.py" in get_oauth_signature
  112.         uri, headers, body = self._render(request)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/oauthlib/oauth1/rfc5849/__init__.py" in _render
  186.             headers = parameters.prepare_headers(request.oauth_params, request.headers, realm=realm)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/oauthlib/oauth1/rfc5849/utils.py" in wrapper
  32.         return target(params, *args, **kwargs)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/oauthlib/oauth1/rfc5849/parameters.py" in prepare_headers
  58.         escaped_value = utils.escape(value)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/oauthlib/oauth1/rfc5849/utils.py" in escape
  57.                          'Got %s of type %s.' % (u, type(u)))

Exception Type: ValueError at /social-auth/
Exception Value: Only unicode objects are escapable. Got None of type <class 'NoneType'>.

Any help would be appreciated, thank you.

@omab
Copy link
Owner

omab commented May 12, 2014

@maryokhin, there's any chance there's an oauth_verifier parameter around? If that's the case, try passing it also.

@maryokhin
Copy link
Contributor Author

@omab, I tried to pass it like this, but it gave me exactly the same error. Maybe I'm passing it wrong?

twitter = {'oauth_token': oauth_token, 'oauth_token_secret': oauth_token_secret, 'oauth_verifier': oauth_verifier}
user = strategy.backend.do_auth(twitter)

@omab
Copy link
Owner

omab commented May 12, 2014

@maryokhin, right now it expects the oauth_verifier as part of the request, so it's being accessed as self.data['oauth_verifier'] (self.data is the request POST/GET data). You know if it's available like that in the request?

@maryokhin
Copy link
Contributor Author

@omab, I used the debugger and at line

oauth_verifier = oauth_verifier or self.data.get('oauth_verifier')
the values are

self.data.get('oauth_verifier') = None
self.data = {'_content': '{  \r\n  "backend": "twitter",\r\n  "access_token": "2478001586-tbni3wKlT9***************8i2tdkiOW",\r\n  "oauth_token_secret": "WgaYaEyHL*****************X6UpzAlTgITPa",\r\n  "oauth_verifier": "Em5uMDvZU4G*****************NHDk6nCIYV0"\r\n}', '_content_type': 'application/json', 'csrfmiddlewaretoken': 'iOnr8epq2O*************VYBqZ'}

Could it be that self.data gets mangled because REST Framework Requests are a bit different? Would be a shame if that's the case, as this is like the only oauth plugin that is versatile enough to work for both front and back-end.

@omab
Copy link
Owner

omab commented May 12, 2014

@maryokhin try defining this strategy in some place

class DjangoRESTFrameworkStrategy(DjangoStrategy):
    def request_data(self, merge=True):
        if not self.request:
            return {}
        if merge:
            data = self.request.REQUEST
        elif self.request.method == 'POST':
            data = self.request.POST
        else:
            data = self.request.GET
        if data.get('_content'):
            data = data.copy()
            data.update(data.pop('_content'))
        return data

Then define this setting pointing to it:

SOCIAL_AUTH_STRATEGY = 'path.to.your.strategy.DjangoRESTFrameworkStrategy'

@maryokhin
Copy link
Contributor Author

@omab, I implemented the strategy, but what is request_data supposed to return, a dictionary? Because then it would be enough to just return self.request.DATA, which is equal to

request.DATA = {'oauth_token_secret': 'WgaYaEyHL*****************X6UpzAlTgITPa', 'backend': 'twitter', 'oauth_verifier': 'Em5uMDvZU4G*****************NHDk6nCIYV0', 'access_token': '2478001586-tbni3wKlT9***************8i2tdkiOW'}

On other hand I tried this, and got the same error as before.

Was not able to use data.update(data.pop('_content')) because data = self.request.REQUEST gives a MergeDict, not a dict and it's doesn't support update() and pop() along with not supporting any concise docs on what MergeDict actually is.

@omab
Copy link
Owner

omab commented May 13, 2014

request_data should return a dict, but looks like it failed since self.request.DATA looks like the final value I was looking to get tested.

Even with the .copy() you get an error whit update() and pop()?

Looks like I need to review this process for OAuth1 backends.

@maryokhin
Copy link
Contributor Author

@omab, Yes, even with copy() it doesn't work, maybe they removed the methods without notice, as MergeDict is internal and subject to change all the time.

I used a custom strategy and return self.request.DATA in request_data because it solves the verifier issue, but there's still a None returned somewhere it shouldn't (maybe with inconsistent naming, i.e. some providers require 'access_token' and some 'oauth_token', but I don't think that's the main problem).

Data from some breakpoints:
social/backends/oauth.py#L212

key = None
secret = None
oauth_verifier = Em5uMDvZU4G*****************NHDk6nCIYV0
token = {}
token.get('oauth_token') = None
token.get('oauth_token_secret') = None
decoding = None

If I modify this method:

def oauth_auth(self, token=None, oauth_verifier=None,
                   signature_type=SIGNATURE_TYPE_AUTH_HEADER):
        key, secret = self.get_key_and_secret()
        oauth_verifier = oauth_verifier or self.data.get('oauth_verifier')
        token_key = token.get('oauth_token') or self.data.get('access_token')
        token_secret = token.get('oauth_token_secret') or self.data.get('oauth_token_secret')
        # decoding='utf-8' produces errors with python-requests on Python3
        # since the final URL will be of type bytes
        decoding = None if six.PY3 else 'utf-8'
        return OAuth1(key, secret,
                      resource_owner_key=token_key,
                      resource_owner_secret=token_secret,
                      callback_uri=self.redirect_uri,
                      verifier=oauth_verifier,
                      signature_type=signature_type,
                      decoding=decoding)

Then the values of oauth_verifier, token_key, token_secret are valid, but key and secret are None because they are taken from the settings, could it be that it doesn't know how to work without them?

@omritoptix
Copy link

also have the same problem

@lburg
Copy link

lburg commented May 17, 2014

I am not sure if this is related, but I have also got a problem using the Twitter backend. I am trying to authenticate using do_auth (I am using RestFramework too) by giving it the access_token that Twitter gave me from my app API keys page and all I get is 403 Forbidden. I used Twitter's "Test OAuth" tool to generate a cURL command that would authenticate me and from there I saw two differences between the cURL command request and the Twitter backend request:

  • A header is missing from the Twitter backend request, oauth_token, which should be set to my access token value; and,
  • The cURL command has a query parameter access_token set to my access token value, whereas the Twitter backend request does not.

I set a breakpoint before the request was sent and manually added the query parameter and the missing header, and I got 401 Unauthorized instead of 403 Forbidden. Any ideas?

omab added a commit that referenced this issue May 18, 2014
@omab
Copy link
Owner

omab commented May 18, 2014

@maryokhin, @omritoptix, @Zoneur

I've tried using AJAX to authenticate an user in the django example app (no Django REST Framework, but that shouldn't matter), and everything worked OK (I've tried Facebook, Google OAuth2 and Twitter), Twitter didn't needed the oauth_verifier parameter, just access_token and access_token_secret.

I've extended the example app with a simpler form to select a backend and input the access_token and access_token_secret (for OAuth1), on submit a JS handler will submit the auth by AJAX and then update the form with the user id and username, then the page will reload (after a few seconds) and that will show that the user was logged in.

@maryokhin
Copy link
Contributor Author

@omab I wasn't able to get it to work with this code:

class SocialAuthView(APIView):

    serializer_class = SocialAuthSerializer

    def post(self, request):
        serializer = self.serializer_class(data=request.DATA, files=request.FILES)

        if serializer.is_valid():
            backend = serializer.data.get('backend')
            access_token = serializer.data.get('access_token')
            access_token_secret = serializer.data.get('access_token_secret')
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

        strategy = load_strategy(request=request, backend=backend)

        if isinstance(strategy.backend, BaseOAuth1):
            token = {
                'oauth_token': access_token,
                'oauth_token_secret': access_token_secret
            }
        elif isinstance(strategy.backend, BaseOAuth2):
            token = access_token
        else:
            raise Response(data="Wrong backend type", status=status.HTTP_400_BAD_REQUEST)

        user = strategy.backend.do_auth(token)
        login(request, user)

        data = {'id': user.id, 'username': user.username}
        return Response(data=data, status=status.HTTP_200_OK)

Maybe @omritoptix or @Zoneur got it to work and can tell me what I'm doing wrong.

P.S. @strategy decorator gave me wrapper() got multiple values for argument 'backend', so I used load_strategy() instead.

@omab
Copy link
Owner

omab commented May 18, 2014

@maryokhin, using load_strategy should be OK, but you need to pass the redirect_uri too, it should be the URL you defined in the application (in the provider), usually the value of urlresolvers.reverse('social:complete'). Call load_strategy like this: load_strategy(request=request, backend=backend, redirect_uri=...)

@maryokhin
Copy link
Contributor Author

@omab On OAuth 2 providers load_strategy(), I think worked for me without redirect_uri, on OAuth1 it doesn't work no matter with or without it. Basically the whole issue is that at oauthlib/oauth1/rfc5849/parameters.py the runtime values are

params  
[('oauth_nonce', '9345058242317761441400449282'),
 ('oauth_timestamp', '1400449282'),
 ('oauth_version', '1.0'),
 ('oauth_signature_method', 'HMAC-SHA1'),
 ('oauth_consumer_key', None),
 ('oauth_token', '2478001586-tbni3wKlT9***************8i2tdkiOW'),
 ('oauth_callback', 'http://127.0.0.1:8000/social-auth')]

so during iteration it hits the None value and throws Only unicode objects are escapable. Got None of type <class 'NoneType'>. But I just can't wrap my head around how this None managed to seep through there.

@lburg
Copy link

lburg commented May 19, 2014

Alright I got it to work. I have a function looking like this:

    @strategy()
    def _register_by_access_token(request, backend):
        backend = request.strategy.backend
        access_token = request.DATA['access_token']
        return backend.do_auth(access_token)

This works for Facebook, but failed for Twitter. I got it working by doing:

access_token = "oauth_token=" + request.DATA['access_token'] + "&oauth_token_secret=<secret>"

This seems weird because using the Twitter tool to generate cURL commands to authenticate, there is no need for the oauth_token_secret, but it fails if I do not include it.

Also, is there a way to make this work without checking whether the backend is OAuth1 or OAuth2? I understand we are not in the regular PSA flow, but it would be nice not to check the base class of the backend.

@maryokhin Your code looks rather similar to mine, try calling do_auth with a string using the url format instead of a dict maybe? Regarding the wrapper error, that's probably because your function had extra parameters instead of just request and backend. The decorator forwards those parameters to strategies.utils.get_strategy and there is a conflict between these extra parameters that are passed as positional arguments and the ones that are passed as keyword arguments (like backend).

Thanks for your help @omab !

@omab
Copy link
Owner

omab commented May 19, 2014

The access_token will be converted to a dict if it's an string https://github.com/omab/python-social-auth/blob/master/social/backends/oauth.py#L124-L125, so passing the dict should also work. I could add a do_auth() that takes a request and it takes the needed values from the request.

@omritoptix
Copy link

@maryokhin I got it to work with both twitter and facebook by using @omab Example code.
My view looks like this:

@strategy('social:complete')
    def ajax_auth(request, backend):
        post = simplejson.loads(request.body)
        backend = request.strategy.backend
        if isinstance(backend, BaseOAuth1):
            token = {
                'oauth_token': post.get('access_token'),
                'oauth_token_secret': post.get('access_token_secret'),
            }
        elif isinstance(backend, BaseOAuth2):
            token = post.get('access_token')
        else:
            raise HttpResponseBadRequest('Wrong backend type')
        user = request.strategy.backend.do_auth(token)
        login(request, user)
        data = {'username': user.username, 'api_key': user.api_key.key}
        return HttpResponse(json.dumps(data), mimetype='application/json')

and i use the following curl to invoke it (for example twitter):

curl --dump-header - -H "Content-Type: application/son" -X POST --data '{"access_token":"280665555-Dux**********tDEunLikpJq","access_token_secret":"VAvS8a8vcgPhp******nMInUCwa"}' http://localhost:8000/ajax-auth/twitter/

I don't pass callback URL, and my callback url in the twitter settings in not related to my dev server.
I used to get 401, but that apparently caused by a expired token. so regenerated the token in twitter , and it worked.

Hope it will help you in some way.

@maryokhin
Copy link
Contributor Author

@omritoptix, I don't know, even basically copying your code gives me the same error I had in the beginning. It's just driving me crazy.

@strategy()
def social_auth(request, backend):
    backend = request.strategy.backend

    if isinstance(backend, BaseOAuth1):
        token = {
            'oauth_token': request.POST.get('access_token'),
            'oauth_token_secret': request.POST.get('access_token_secret'),
        }
    elif isinstance(backend, BaseOAuth2):
        token = request.POST.get('access_token')
    else:
        raise Response('Wrong backend type', status.HTTP_400_BAD_REQUEST)

    user = request.strategy.backend.do_auth(token)
    login(request, user)

    data = {'username': user.username, 'api_key': user.api_key.key}
    return Response(data=data, status=status.HTTP_200_OK)

@omab, I guess just close the issue since I'm the only one experiencing difficulties. Thank you for all your input.

@maryokhin
Copy link
Contributor Author

@omab could it be by any chance something having to do with the setup I have or should I not even bother checking that? I communicated with @Zoneur and using the exact same code that works for him doesn't work for me. It would be valuable, if @omritoptix could share his environment.

I use python 3.4

Django==1.7b4
django-braces==1.4.0
django-cors-headers==0.12
django-grappelli==2.5.3
djangorestframework==2.3.13
oauthlib==0.6.1
psycopg2==2.5.3
python-social-auth==0.1.24
python3-openid==3.0.4
requests==2.3.0
requests-oauthlib==0.3.1
shortuuid==0.4.2
six==1.6.1

@omab
Copy link
Owner

omab commented May 21, 2014

@maryokhin, are you settings defined properly? SOCIAL_AUTH_TWITTER_KEY and SOCIAL_AUTH_TWITTER_SECRET?

@maryokhin
Copy link
Contributor Author

@omab, I get a 401 now, but that means it works! Thank you very much, I did not include the keys on the API side, because it worked without them on OAuth2, but I guess OAuth1 requires it, I didn't know about this requirement. Thank you and I now feel stupid about my mistake.
I just have a last unrelated question: access tokens are not consistent, they can expire, be revoked, so is there a best-practice when linking them to users, check user ID from service or something?

P.S. I think Django REST Framework should be included to the list of frameworks, I can contribute a simple example app in ~ week's time.

@omab
Copy link
Owner

omab commented May 21, 2014

@maryokhin, not sure what you meant with include the keys on the API side.

About Django REST Frameworks support as a built-in app, that would be great to have.

@maryokhin
Copy link
Contributor Author

@omab, I meant that for Google/Facebook I didn't define any keys/secrets in the settings and it worked.

@omab
Copy link
Owner

omab commented May 21, 2014

Cool, I'm gonna close this issue since it's finally solved, thanks!

@omab omab closed this as completed May 21, 2014
@troeger
Copy link

troeger commented Apr 15, 2015

Using SOCIAL_AUTH_TWITTER_KEY and SOCIAL_AUTH_TWITTER_SECRET as configuration parameters, as proposed, solved this for me. Here is the pull request for the documentation update:

omab/django-social-auth#821

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants