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

Implement OAuth2 user token autorefresh. #2039

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 30 additions & 4 deletions docs/authentication.rst
Expand Up @@ -117,7 +117,8 @@ You can generate an access token to authenticate as a user using
You'll need to turn on OAuth 2.0 under the User authentication settings section
of your app's Settings tab under the
`Twitter Developer Portal Projects & Apps page`_. To do this, you'll need to
provide a Callback / Redirect URI / URL.
provide a Callback / Redirect URI / URL. https://no.such.host.example.com/ will
work fine, for as long as example.com is publically controlled.

Then, you'll need to note the app's Client ID, which you can find through your
app's Keys and Tokens tab under the
Expand Down Expand Up @@ -149,13 +150,38 @@ This can be used to have a user authenticate your app. Once they've done so,
they'll be redirected to the Callback / Redirect URI / URL you provided. You'll
need to pass that authorization response URL to fetch the access token::

access_token = oauth2_user_handler.fetch_token(
oauth2_user_handler.fetch_token(
"Authorization Response URL here"
)

You can then pass the access token to :class:`Client` when initializing it::
You can then tell :class:`Client` to use the handler as its session object when
initializing it::

client = tweepy.Client(session=oauth2_user_handler)

This causes :class:`Client` to rely on the handler for all authentication.

To save the credentials, save the `oauth2_user_handler.token` dictionary. To
restore a handler from this dictionary, run::

oauth2_user_handler = OAuth2UserHandler.from_token(
old_token,
client_id="Client ID here",
redirect_uri="Callback / Redirect URI / URL here",
# Client Secret is only necessary if using a confidential client
client_secret="Client Secret here"
)

client = tweepy.Client("Access Token here")
By default, user credentials only last two hours. For them to last longer, the
`"offline.access"` scope must be used. Even in this mode, the token must be
changed, or "refreshed" every two hours using a refresh token. All of this
refreshing happens internally inside the :class:`OAuth2UserHandler`, but this
means every two hours, `OAuth2UserHandler.token` will change. So, be sure to
save oauth2_user_handler.token before destroying the handler. To handle the
automatic saving of the credentials as soon as they are changed, a function can
be passed to the `token_updater=` argument of :class:`OAuth2UserHandler` or
`OAuth2UserHandler.from_token`. This function will be called with
`OAuth2UserHandler.token` as its sole input.

3-legged OAuth
==============
Expand Down
26 changes: 25 additions & 1 deletion tweepy/auth.py
Expand Up @@ -193,13 +193,37 @@ class OAuth2UserHandler(OAuth2Session):
.. versionadded:: 4.5
"""

def __init__(self, *, client_id, redirect_uri, scope, client_secret=None):
def __init__(
self, *, client_id, redirect_uri, scope, client_secret=None,
auto_refresh=True, token_updater=None
):
super().__init__(client_id, redirect_uri=redirect_uri, scope=scope)

if client_secret is not None:
self.auth = HTTPBasicAuth(client_id, client_secret)
else:
self.auth = None

if auto_refresh:
self.token_updater = token_updater
self.auto_refresh_url = 'https://api.twitter.com/2/oauth2/token'
self.auto_refresh_kwargs = {'client_id': client_id}

@classmethod
def from_token(
cls, token, *, client_id, redirect_uri, client_secret=None,
token_updater=None
):
"""Make an OAuth2UserHandler from a token dict returned by
fetch_token() or from the .token field in a previous session."""
h = cls(
client_id=client_id, redirect_uri=redirect_uri,
scope=token['scope'], client_secret=client_secret,
token_updater=token_updater
)
h.token = token
return h

def get_authorization_url(self):
"""Get the authorization URL to redirect the user to"""
authorization_url, state = self.authorization_url(
Expand Down
25 changes: 13 additions & 12 deletions tweepy/client.py
Expand Up @@ -41,11 +41,12 @@
class BaseClient:

def __init__(
self, bearer_token=None, consumer_key=None, consumer_secret=None,
access_token=None, access_token_secret=None, *, return_type=Response,
wait_on_rate_limit=False
self, bearer_token=None, session=requests.Session(), consumer_key=None,
consumer_secret=None, access_token=None, access_token_secret=None, *,
return_type=Response, wait_on_rate_limit=False
):
self.bearer_token = bearer_token
self.session = session
self.consumer_key = consumer_key
self.consumer_secret = consumer_secret
self.access_token = access_token
Expand All @@ -54,7 +55,6 @@ def __init__(
self.return_type = return_type
self.wait_on_rate_limit = wait_on_rate_limit

self.session = requests.Session()
self.user_agent = (
f"Python/{python_version()} "
f"Requests/{requests.__version__} "
Expand All @@ -65,14 +65,15 @@ def request(self, method, route, params=None, json=None, user_auth=False):
host = "https://api.twitter.com"
headers = {"User-Agent": self.user_agent}
auth = None
if user_auth:
auth = OAuth1UserHandler(
self.consumer_key, self.consumer_secret,
self.access_token, self.access_token_secret
)
auth = auth.apply_auth()
else:
headers["Authorization"] = f"Bearer {self.bearer_token}"
if type(self.session) == requests.Session:
if user_auth:
auth = OAuth1UserHandler(
self.consumer_key, self.consumer_secret,
self.access_token, self.access_token_secret
)
auth = auth.apply_auth()
else:
headers["Authorization"] = f"Bearer {self.bearer_token}"

log.debug(
f"Making API request: {method} {host + route}\n"
Expand Down