singingwolfboy / flask-dance

Doing the OAuth dance with style using Flask, requests, and oauthlib.
https://pypi.python.org/pypi/Flask-Dance/
MIT License
1.01k stars 158 forks source link

Twitter: "ValueError: Cannot get OAuth token without an associated user" #120

Closed chrisroat closed 6 years ago

chrisroat commented 6 years ago

I was able to run the github-oauth based example correctly.

I then created this gist which is the same except switching to twitter. It gives "ValueError: Cannot get OAuth token without an associated user".

abprojects commented 6 years ago

It seems this error is only thrown when using Twitter provider. I had issues and commented on it in a previous thread on getting this error. I ran the same code but switched the providers. I've had success with multiple other providers but get this error thrown with Twitter.

chrisroat commented 6 years ago

Agree. I traced it to the fact that the OAuth1 flow makes a request before needing a token, when OAuth1 gets a "request token". Flask-dance attempts to load the token at this point, before one exists, leading to the failure.

I have found two possible workarounds:

  1. When creating the SQLAlchemyBackend object, pass in user_required=False
  2. Remove the "self.load_token()" statement from the OAuth1Session.request method, here

I don't think either is necessarily ideal, and each has drawbacks. I've chosen to go with the 1st option, which has the drawback of not checking user existence later in the flow when storing the token. I have been very careful about being sure to store the user when by intercepting the oauth_authorized signal.

The second option has the drawback of not loading tokens for later requests. This would be OK for my app, but I choose not to go this way because of the need to hack the package.

abprojects commented 6 years ago

That makes sense. I was hoping to avoid these scenarios, but it seems to be the only workaround. Thanks for your help!

singingwolfboy commented 6 years ago

Alright, I finally had time to look into this problem in depth, and it's a bit complex. I found the source of the problem pretty quickly: as I thought, it's due to the fact that Twitter is using OAuth 1 while just about every other OAuth provider in the world has switched to OAuth 2. Since OAuth 1 and OAuth 2 are different protocols, the implementations within Flask-Dance are different, as well. Since OAuth 2 gets much more use, it's reasonable to expect that bugs in Flask-Dance that are related to OAuth 2 are found more quickly than bugs in Flask-Dance for OAuth 1.

However, while finding the problem was easy, trying to figure out the right way to fix it is harder. It doesn't help that I haven't had to dive into the details of these protocols in awhile. 😄 Flask-Dance is a layered system: it's built on top of requests-oauthlib, which is built on top of requests and oauthlib. To fix the problem, I had to make two pull requests: one for requests-oauthlib, and one for flask-dance. I'm pretty confident that the requests-oauthlib fix is the right way to fix the problem, but I'm less confident about the flask-dance fix. I need to think about it more, and see if there's a cleaner way to solve it.

@chrisroat and @abprojects: would you mind testing this fix for me, to verify that it works the way I expect? In order to do so, you'll need to install versions of these Python modules that have the fix applied. You can use pip requirements URLs to do so, like this:

pip install git+https://github.com/singingwolfboy/flask-dance.git@oauth1-should-load-token
pip install git+https://github.com/singingwolfboy/requests-oauthlib.git@oauth1session-token-property

Alternatively, you can install these packages normally, and then manually apply the changes from these pull requests to your local copies.

Can you give this a try, and let me know if it works with these changes?

chrisroat commented 6 years ago

Yes, I can confirm this works for me. With those two patches, I was able to remove the 'user_required = False' parameter when creating the backend, and my app worked as I expected it to.

abprojects commented 6 years ago

I installed the patches and was not able to get the twitter oauth to work (without user_required parameter set to False). Was still able to get access with other providers. I don't know if it could be a problem with installation or something else on my end in the setup, but I was not able to get it to work as expected after installing both patches

singingwolfboy commented 6 years ago

@abprojects Can you share your code, so I can test it out myself? If there's an edge case that my pull requests don't cover, I definitely want to know about it so I can fix it!

abprojects commented 6 years ago

No problem, I sent it to you in an email (I have trouble formatting on here)

k13w commented 6 years ago

Hello, I was with the same error, thanks to chrisroat I was able to solve using user_required = False well, I'll try to install the two packages to see if the problem solved definitively

k13w commented 6 years ago

it did not work here

singingwolfboy commented 6 years ago

@abprojects: Is there a repository you can share with me? I tried to take your email and reconstruct a working application, but you only gave me part of the files, and they depend on other files that you didn't give me. A repository would be easiest for me to test.

@HeavenH: Can you also share your code?

I want to fix this problem, but I can't fix it if I can't reproduce it!

k13w commented 6 years ago

@singingwolfboy https://github.com/HeavenH/flask_oauth

singingwolfboy commented 6 years ago

@HeavenH I tried your repo using the two patches I mentioned earlier, and everything worked fine for me. Maybe you didn't apply both patches correctly?

k13w commented 6 years ago

https://imgur.com/a/mh1dhwW

Look if I installed it correctly

singingwolfboy commented 6 years ago

Update: I've merged the pull request for Flask-Dance to make it work properly with Twitter. However, I've had no response from the maintainer of requests-oauthlib, so that pull request hasn't been merged yet, and I don't know when it will be. :(

wgwz commented 6 years ago

Hey @davidism! Are both PRs required to fix the issue? I’m just wanting to confirm. I’m going to try to test this.

singingwolfboy commented 6 years ago

In the process of chasing down this issue, I've become the maintainer for requests-oauthlib, as well! So, a few updates:

Woo, we finally reached 1.0! No more 0ver for this project. At any rate, this should resolve the Twitter issue, so I'm closing this GitHub issue. If anyone is still seeing this issue with Flask-Dance 1.0, please reopen the issue to let me know!

chrisroat commented 6 years ago

Thanks for the update. The new package versions work great for my app.

VortexH commented 5 years ago

I'm still having this issue...I followed this tutorial to the T (except replacing SQLAlchemyBackend to SQLAlchemyStorage).https://www.youtube.com/watch?time_continue=141&v=G44Tpi58dcc

I'm hooking into the oauth_authorized connect signal and creating a user if it can't find it.

Here's my code if someone wants to take a look at it.

#!/usr/bin/python3
from flask import Flask
from flask_dance.consumer.storage.sqla import OAuthConsumerMixin, SQLAlchemyStorage
from flask_dance.contrib.twitter import make_twitter_blueprint
from flask_login import UserMixin, current_user, LoginManager
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__, instance_relative_config=True)
app.config.from_pyfile('config.py')
app.url_map.strict_slashes = False

db = SQLAlchemy(app)
login_manager = LoginManager(app)

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key = True)
    username = db.Column(db.String(250), unique = True)

class OAuth(OAuthConsumerMixin, db.Model):
    user_id = db.Column(db.Integer, db.ForeignKey(User.id))
    user = db.relationship(User)

db.create_all()

twitter_bp = make_twitter_blueprint(redirect_to='index')
twitter_bp.storage = SQLAlchemyStorage(OAuth, db.session, user=current_user, user_required=False)

app.register_blueprint(twitter_bp, url_prefix='/login')

import detweet_app.views
#!/usr/bin/python3
""" Script to start a Flask web application """

from detweet_app import app, twitter_bp, login_manager, db, User
from flask import jsonify, redirect, render_template, request, url_for, session
from flask_cors import CORS
from flask_dance.contrib.twitter import twitter
from flask_dance.consumer import oauth_authorized
from sqlalchemy.orm.exc import NoResultFound
from flask_login import current_user
from .deTweet import get_all_tweets, delete_tweets
import re

CORS(app, resources={r"*": {"origins": "*"}})

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

@oauth_authorized.connect_via(twitter_bp)
def twitter_logged_in(twitter_bp, token):
    resp = twitter_bp.session.get("account/verify_credentials.json")

    if resp.ok:
        resp_json = resp.json()
        username = resp_json['screen_name']
        query = User.query.filter_by(username = username)

        try:
            query.one()
        except NoResultFound:
            user = User(username = username)
            db.session.add(user)
            db.session.commit()

        login_user(user)

    return False

@app.route('/')
def serve_login_page():
    return render_template('login.html')

@app.route('/login')
def index():
    print("hello")
    if not twitter.authorized:
        return redirect(url_for('twitter.login'))
    resp = twitter.get("account/verify_credentials.json")
    session['img'] = resp.json()['profile_image_url_https']
    screen_name = resp.json()['screen_name']
    print(current_user)
    return redirect(url_for('tweet_page', username=screen_name))

I think I'm almost there with the setup but this is annoying.

singingwolfboy commented 5 years ago

@VortexH I think you'll find the Flask-Dance Twitter SQLAlchemy quickstart project to be a very useful reference. That video tutorial is out of date; please don't use it.

VortexH commented 5 years ago

Thanks for the link, I got it working. I'm running into another issue however. Now that I'm adding local users and storing their OAuth token information, I have another view where I want to make a GET request to a twitter API endpoint, but it seems that I get ValueError: Cannot get OAuth token without an associated user even though both the User and Token are in the database.

twitter.get('statuses/user_timeline.json', params=payload).json()

Doesn't the twitter object get access to the database and pull the token out?

I'm lost right now

singingwolfboy commented 5 years ago

@VortexH In general, please do not comment on closed GitHub issues -- it makes it more difficult for other people to understand the history of this project. If you're having a problem, I suggest that you open a new issue for your problem, so that we can close that issue when your problem is resolved. You can include links to any other issues that seem relevant if you want to, including this one.

fabge commented 5 years ago

@VortexH Can you explain what you did to make it work? I originally used the video as reference, too. But I continue to have the problem of ValueError: Cannot get OAuth token without an associated user despite using the newer reference.

singingwolfboy commented 5 years ago

As I said before, please do not comment on closed GitHub issues -- it makes it more difficult for other people to understand the history of this project. If you're having a problem, I suggest that you open a new issue for your problem, so that we can close that issue when your problem is resolved. You can include links to any other issues that seem relevant if you want to, including this one.

I am locking this GitHub issue. If you continue to have this problem, open a new issue.