1
votes

I'm using Django with Allauth + REST-Auth for SPA social Login and successfully set up Facebook, VK and Google authorization but faced a problem while adding Twitter. It ends up with {"code":89,"message":"Invalid or expired token."} Looks like i'm missing something 'cos standard login with Twitter works as it should

Here are my tries:

First of all, i've set Twitter login endpoint as described in doc:

class TwitterLogin(SocialLoginView):
    serializer_class = TwitterLoginSerializer
    adapter_class = CustomTwitterOAuthAdapter

It features post method, expecting access_token and token_secret So redirect view was created to receive redirect from twitter, complete login and set inner django token to browser localStorage via template render (with couple of JS lines):

class TwitterReceiveView(APIView):

    def get(self, request, *args, **kwargs):
        access_token = request.query_params.get('oauth_token')
        token_secret = request.query_params.get('oauth_verifier')
        params = {'access_token': access_token,
                  'token_secret': token_secret}
        try:
            result = requests.post(settings.DOMAIN + reverse('tw_login'), data=params).text
            result = json.loads(result)
        except (requests.HTTPError, json.decoder.JSONDecodeError):
            result = {}
        access_token = result.get('access_token')
        context = {'access_token': access_token}
        return render(request, 'account/local_storage_setter.html',
                      context, content_type='text/html')

Have to mention that I tried two methods to start process(get initial token) 1. Used standard allauth url http://0.0.0.0:8080/accounts/twitter/login 2. Created another view (using lib python oauth2) which could be used from SPA:

class TwitterGetToken(APIView):

    def get(self, request, *args, **kwargs):
        request_token_url = 'https://api.twitter.com/oauth/request_token'
        authorize_url = 'https://api.twitter.com/oauth/authorize'

        app = SocialApp.objects.filter(name='Twitter').first()
        if app and app.client_id and app.secret:
            consumer = oauth.Consumer(app.client_id, app.secret)
            client = oauth.Client(consumer)

            resp, content = client.request(request_token_url, "GET")
            if resp['status'] != '200':
                raise Exception("Invalid response {}".format(resp['status']))

            request_token = dict(urllib.parse.parse_qsl(content.decode("utf-8")))

            twitter_authorize_url = "{0}?oauth_token={1}"\
                .format(authorize_url, request_token['oauth_token'])

            return redirect(twitter_authorize_url)

        raise Exception("Twitter app is not set up")

I even tried to write get method for FacebookLoginView and pass twitter callback to it directly

class TwitterLogin(SocialLoginView):
    serializer_class = TwitterLoginSerializer
    adapter_class = TwitterOAuthAdapter

    def get(self, request, *args, **kwargs):
        data = {
            'access_token': request.query_params.get('oauth_token'),
            'token_secret': request.query_params.get('oauth_verifier')
        }
        self.request = request
        self.serializer = self.get_serializer(data=data,
                                              context={'request': request})
        self.serializer.is_valid(raise_exception=True)

        self.login()
        return self.get_response()

All methods led me to mentioned error. Could you, please, advise something in my case. Thank you in advance!

UPDATE: Here is how it's worked for me:

import json
import requests
import urllib.parse
import oauth2 as oauth
from requests_oauthlib import OAuth1Session

from django.urls import reverse
from django.conf import settings
from django.shortcuts import redirect, render
from rest_framework.views import APIView
from allauth.socialaccount.models import SocialApp
from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter, TwitterAPI
from rest_auth.social_serializers import TwitterLoginSerializer
from rest_auth.registration.views import SocialLoginView


class TwitterGetToken(APIView):
    '''
    Initiates Twitter login process
    Requests initial token and redirects user to Twitter
    '''

    def get(self, request, *args, **kwargs):
        request_token_url = 'https://api.twitter.com/oauth/request_token'
        authorize_url = 'https://api.twitter.com/oauth/authorize'

        app = SocialApp.objects.filter(name='Twitter').first()
        if app and app.client_id and app.secret:
            consumer = oauth.Consumer(app.client_id, app.secret)
            client = oauth.Client(consumer)

            resp, content = client.request(request_token_url, "GET")
            if resp['status'] != '200':
                raise Exception("Invalid response {}".format(resp['status']))

            request_token = dict(urllib.parse.parse_qsl(content.decode("utf-8")))

            twitter_authorize_url = "{0}?oauth_token={1}"\
                .format(authorize_url, request_token['oauth_token'])

            return redirect(twitter_authorize_url)

        raise Exception("Twitter app is not set up")



class TwitterLogin(SocialLoginView):
    '''
    Takes the final twitter access token, secret
    Returns inner django Token
    '''
    serializer_class = TwitterLoginSerializer
    adapter_class = TwitterOAuthAdapter


class TwitterReceiveView(APIView):
    '''
    Receives Twitter redirect, 
    Requests access token
    Uses TwitterLogin to logn and get django Token
    Renders template with JS code which sets django Token to localStorage and redirects to SPA login page
    '''

    def get(self, request, *args, **kwargs):
        access_token_url = 'https://api.twitter.com/oauth/access_token'
        callback_uri = settings.DOMAIN + '/accounts/twitter/login/callback/'

        app = SocialApp.objects.filter(name='Twitter').first()
        client_key = app.client_id
        client_secret = app.secret

        oauth_session = OAuth1Session(client_key,
                                      client_secret=client_secret,
                                      callback_uri=callback_uri)

        redirect_response = request.get_full_path()
        oauth_session.parse_authorization_response(redirect_response)
        token = oauth_session.fetch_access_token(access_token_url)

        params = {'access_token': token['oauth_token'],
                  'token_secret': token['oauth_token_secret']}
        try:
            result = requests.post(settings.DOMAIN + reverse('tw_login'),
                                   data=params).text
            result = json.loads(result)
        except (requests.HTTPError, json.decoder.JSONDecodeError):
            result = {}
        access_token = result.get('access_token')
        context = {'access_token': access_token,
                   'domain': settings.DOMAIN}
        return render(request, 'account/local_storage_setter.html',
                      context, content_type='text/html')
1

1 Answers

0
votes

Great code, Thank you for posting!

I'd like to add however that user authentication can be done directly from the front end, and given you're writing an SPA it seems logical to do so, instead of redirect your in your back-end (which kind of breaks the notion of a RESTful) to auth and then send a response.

I used the following JS helper class based of vue-authenticate. To make a popup and get info from the callback url

export default class OAuthPopup {
  constructor(url, name, redirectURI) {
    this.popup = null
    this.url = url
    this.name = name
    this.redirectURI = redirectURI
  }

  open() {
    try {
      this.popup = window.open(this.url, this.name)
      if (this.popup && this.popup.focus) {
        this.popup.focus()
      }
      return this.pooling()
    } catch(e) {
      console.log(e)
    }
  }

  pooling() {
    return new Promise((resolve, reject) => {
      let poolingInterval = setInterval(() => {
        if (!this.popup || this.popup.closed || this.popup.closed === undefined) {
          clearInterval(poolingInterval)
          poolingInterval = null
          reject(new Error('Auth popup window closed'))
        }

        try {
          var popupLocation = this.popup.location.origin + this.popup.location.pathname
          if (popupLocation == this.redirectURI) {
            if (this.popup.location.search || this.popup.location.hash ) {

              const urlParams = new URLSearchParams(this.popup.location.search);

              var params = {
                oauth_token: urlParams.get('oauth_token'),
                oauth_verifier: urlParams.get('oauth_verifier'),
                url: this.popup.location.href
              }

              if (params.error) {
                reject(new Error(params.error));
              } else {
                resolve(params);
              }

            } else {
              reject(new Error('OAuth redirect has occurred but no query or hash parameters were found.'))
            }

            clearInterval(poolingInterval)
            poolingInterval = null
            this.popup.close()
          }
        } catch(e) {
          // Ignore DOMException: Blocked a frame with origin from accessing a cross-origin frame.
        }
      }, 250)
    })
  }
}

The methodology I followed is similar to yours however:

  1. I make a GET request to TwitterGetToken and get the Twitter auth url back as a response
  2. I use the url in the response from my front end to open a popup which the allows the user to auth
  3. I make a POST request to TwitterReceiveView and attach the response url after twitter auth

everything else the just falls into place and the user is returned a access key.

In any case, Thanks I messed around with loads of libraries in js and python but this was just the best way to do things