Search code examples
pythonoauth-2.0tweepytwitter-oauth

How to recreate tweepy OAuth2UserHandler across web requests


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?


Solution

  • 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.