With OAuth2UserHandler included in the tweepy package, if you generate an authorization URL and later want to retrieve an OAuth2 bearer token, it only works if you reuse the exact OAuth2UserHandler() in memory.
Given an OAuth2UserHandler
like this:
from tweepy import OAuth2UserHandler
def _oauth2_handler(callback_url: str) -> OAuth2UserHandler:
return OAuth2UserHandler(
client_id=MY_TWITTER_KEY,
redirect_uri=callback_url,
scope=["offline.access", "users.read", "tweet.read"],
consumer_secret=MY_TWITTER_SECRET,
)
This works:
handler = _oauth2_handler(callback_url, None)
authorize_url = handler.get_authorization_url()
# .. user does authorization flow and we have this in memory still somehow ..
token_data = handler.fetch_token(current_url_after_callback)
This does not work:
handler = _oauth2_handler(callback_url)
authorize_url = handler.get_authorization_url()
# .. user does authorization flow and we talk to a new instance later ..
handler = _oauth2_handler(callback_url)
token_data = handler.fetch_token(current_url_after_callback)
This is because the internal state of OAuth2UserHandler creates a code_verifier
, which is not possible to pass into the class.
How can I resolve?
My solution was to reimplement OAuth2UserHandler
, exposing code_verifier
and allowing the caller to store it and provide it back to the handler later.
Example implementation (fork of tweepy's implementation):
import tweepy
from oauthlib.oauth2 import OAuth2Error
from requests.auth import HTTPBasicAuth
from requests_oauthlib import OAuth2Session
class NotTweepyOAuth2UserHandler(OAuth2Session):
def __init__(
self, client_id: str, client_secret: str, redirect_uri: str, scope: list[str], code_verifier: str | None = None
):
super().__init__(client_id, redirect_uri=redirect_uri, scope=scope)
self.auth = HTTPBasicAuth(client_id, client_secret)
self.code_verifier = code_verifier or str(self._client.create_code_verifier(128))
def get_authorization_url(self) -> str:
url, state_seems_unnecessary = self.authorization_url(
"https://twitter.com/i/oauth2/authorize",
code_challenge=self._client.create_code_challenge(self.code_verifier, "S256"),
code_challenge_method="S256",
)
return url
def fetch_token(self, authorization_response):
return super().fetch_token(
"https://api.twitter.com/2/oauth2/token",
authorization_response=authorization_response,
auth=self.auth,
include_client_id=True,
code_verifier=self.code_verifier,
)
def _oauth2_handler(callback_url: str, code_verifier: str | None) -> OAuth2UserHandler:
return NotTweepyOAuth2UserHandler(
MY_TWITTER_KEY,
MY_TWITTER_SECRET,
callback_url,
["offline.access", "users.read", "tweet.read"],
code_verifier=code_verifier,
)
def get_twitter_authorize_url_and_verifier(callback_url: str) -> tuple[str, str]:
handler = _oauth2_handler(callback_url, None)
authorize_url = handler.get_authorization_url()
# the caller can now store code_verifier somehow
return authorize_url, handler.code_verifier
def get_twitter_token(callback_url: str, current_url: str, twitter_verifier: str) -> dict: # then pass twitter_verifier back in here handler = _oauth2_handler(callback_url, twitter_verifier)
try:
return handler.fetch_token(current_url)
except OAuth2Error as e:
raise TwitterAuthError(e.description) from e
Now we can store code_verifier
somewhere and complete the authorization loop to get our twitter oauth2 bearer token.