Handling Refresh Tokens in the OAuth 2.0 Authorization Code Flow with PKCE with Flask
Jessica Garson
Posted on June 17, 2022
We recently released OAuth 2.0 Authorization Code Flow with PKCE for use with most of our v2 endpoints. As a backend-focused Python developer, I struggled with the flow since much of the code I like to write focuses on automation tasks.
This tutorial will walk you through critical concepts related to refresh tokens using v2 of the Twitter API. If you don’t already have access to the Twitter API, you can sign up for an account. You will also need to have OAuth 2.0 turned on in your App’s settings.
Tokens are only valid for two hours
I was used to working with OAuth 1.0a, where keys and tokens are available for use until they are revoked. This is not the case with OAuth 2.0 Authorization Code Flow with PKCE. Access Tokens generated using this flow are only valid for 2 hours.
Refresh tokens
Inside the token object generated from the consent flow is a refresh token. A refresh token allows an application to obtain a new access token without prompting the user to log in again.
To generate a refresh token, you must set a scope for offline access. If I was using the manage Tweets endpoint and I wanted to Tweet on behalf of a user every six months, I’d use the following line of Python code:
scopes = ["tweet.read", "users.read", "tweet.write", "offline.access"]
How long are refresh tokens valid for?
Refresh tokens stay valid for six months, so you will want to refresh them at least that often or more regularly.
Generating tokens
To generate a token using OAuth 2.0 Authorization Code Flow with PKCE you can use a method similar to this example that allows you to look up an authenticated user’s bookmarks. Since the authenticated user will need to log in directly, and you will need to parse the response, I found creating a Flask application allowed for a more automated process.
The following code will generate a token and save the token into a dictionary.
You will be working with the following libraries:
requests
for making HTTP requestsrequests_oauthlib
for working with OAuth 2.0os
for parsing environment variables and creating random stringsre
,base64
andhashlib
to create the code challenge and code verifierflask
for creating a web framework
If you don’t already have flask, requests_oauthlib and requests installed you will need to install these libraries.
You will first need to import the following libraries:
import base64
import hashlib
import os
import re
import requests
from requests_oauthlib import OAuth2Session
from flask import Flask, request, redirect, session, url_for
First, you will need to set a variable for your app
, which is the start of every Flask app. Additionally, you will need to set a secret key for your app to be a random string.
app = Flask(__name__)
app.secret_key = os.urandom(50)
The two main credentials you will need to authenticate with OAuth 2.0 Authorization Code Flow with PKCE are Client ID and Client Secret. You can set these as environment variables to ensure security.
client_id = os.environ.get("CLIENT_ID")
client_secret = os.environ.get("CLIENT_SECRET")
You will also need to set a variable for your redirect URL, this should be the same value as your callback URL in your App’s settings in the Developer Portal.
redirect_uri = os.environ.get("REDIRECT_URI")
auth_url = "https://twitter.com/i/oauth2/authorize"
token_url = "https://api.twitter.com/2/oauth2/token"
To define the permissions of your App, you will need to set the scopes of your application. Check out the authentication mapping guide to determine what scopes you will need based on the endpoints you are using. The scope offline.access
allows you to generate refresh tokens. The scopes for this demo are tweet.read
and users.read
, which gives you access to read Tweets and obtain information about users.
scopes = ["tweet.read", "users.read", "offline.access"]
You will need to set a code verifier which is a secure random string. The code verifier is used to create the code challenge.
code_verifier = base64.urlsafe_b64encode(os.urandom(30)).decode("utf-8")
code_verifier = re.sub("[^a-zA-Z0-9]+", "", code_verifier
With PKCE the code challenge is a base64 encoded string of the SHA256 hash of the code verifier.
code_challenge = hashlib.sha256(code_verifier.encode("utf-8")).digest()
code_challenge = base64.urlsafe_b64encode(code_challenge).decode("utf-8")
code_challenge = code_challenge.replace("=", "")
Now you can start with creating a login page that will be the first page you will visit when you run your application. @app.route("/")
indicates it’s the first landing page. This page will be the page that an authenticated user logs into.
@app.route("/")
def demo():
global twitter
twitter = OAuth2Session(client_id, redirect_uri=redirect_uri, scope=scopes)
authorization_url, state = twitter.authorization_url(
auth_url, code_challenge=code_challenge, code_challenge_method="S256"
)
session["oauth_state"] = state
return redirect(authorization_url)
After the user logs in, they will be directed to the callback. In the callback you will generate an object called token
that contains a series of tokens that includes an access token and a refresh token.
@app.route("/oauth/callback", methods=["GET"])
def callback():
code = request.args.get("code")
token = twitter.fetch_token(
token_url=token_url,
client_secret=client_secret,
code_verifier=code_verifier,
code=code,
)
Now that you’ve generated a token object, you can now make a request to the authenticated user lookup, to obtain a user ID that can be used to access many of our v2 Users endpoints such as manage Tweets or manage blocks.
The access_token
variable inside of the token object is the bearer token you’d use to connect to any of the endpoints that support OAuth 2.0 Authorization Code Flow with PKCE.
user_me = requests.request(
"GET",
"https://api.twitter.com/2/users/me",
headers={"Authorization": "Bearer {}".format(token["access_token"])},
).json()
print(user_me)
user_id = user_me["data"]["id"]
Now that you’ve created a token and saved into a dictionary you can now access the token dictionary and create a refreshed token through this process. This code should be added to your callback method. This refreshed_token
will contain a new access token as well.
tokens = {"new_token": token}
t = tokens["new_token"]
refreshed_token = twitter.refresh_token(
client_id=client_id,
client_secret=client_secret,
token_url=token_url,
refresh_token=t["refresh_token"],
)
tokens.update({"new_token": refreshed_token})
return "You should now have a refreshed token"
if __name__ == "__main__":
app.run()
Full code
Here is the full code that you can save as app.py
import base64
import hashlib
import os
import re
import requests
from requests_oauthlib import OAuth2Session
from flask import Flask, request, redirect, session, url_for
app = Flask(__name__)
app.secret_key = os.urandom(50)
client_id = os.environ.get("CLIENT_ID")
client_secret = os.environ.get("CLIENT_SECRET")
redirect_uri = os.environ.get("REDIRECT_URI")
auth_url = "https://twitter.com/i/oauth2/authorize"
token_url = "https://api.twitter.com/2/oauth2/token"
scopes = ["tweet.read", "users.read", "offline.access"]
code_verifier = base64.urlsafe_b64encode(os.urandom(30)).decode("utf-8")
code_verifier = re.sub("[^a-zA-Z0-9]+", "", code_verifier)
code_challenge = hashlib.sha256(code_verifier.encode("utf-8")).digest()
code_challenge = base64.urlsafe_b64encode(code_challenge).decode("utf-8")
code_challenge = code_challenge.replace("=", "")
@app.route("/")
def demo():
global twitter
twitter = OAuth2Session(client_id, redirect_uri=redirect_uri, scope=scopes)
authorization_url, state = twitter.authorization_url(
auth_url, code_challenge=code_challenge, code_challenge_method="S256"
)
session["oauth_state"] = state
return redirect(authorization_url)
@app.route("/oauth/callback", methods=["GET"])
def callback():
code = request.args.get("code")
token = twitter.fetch_token(
token_url=token_url,
client_secret=client_secret,
code_verifier=code_verifier,
code=code,
)
user_me = requests.request(
"GET",
"https://api.twitter.com/2/users/me",
headers={"Authorization": "Bearer {}".format(token["access_token"])},
).json()
print(user_me)
user_id = user_me["data"]["id"]
tokens = {"new_token": token}
t = tokens["new_token"]
refreshed_token = twitter.refresh_token(
client_id=client_id,
client_secret=client_secret,
token_url=token_url,
refresh_token=t["refresh_token"],
)
tokens.update({"new_token": refreshed_token})
return "You should now have a refreshed token"
if __name__ == "__main__":
app.run()
To run the file locally you can run the following line:
python app.py
Next steps
Hopefully, this can be a starting point for you to get started with generating refresh tokens. As a next step, if you are using Flask you may want to consider using a schedular to update your refresh tokens regularly in an automated fashion. Additionally you may want to consider saving your tokens to a database in a secure fashion. This code sample can also be extended to allow you to connect to any of the endpoints that support v2 and can be deployed to a server as part of a more complete application.
Be sure to let us know on the forums if you run into any troubles along the way, or Tweet us at @TwitterDev if this tutorial inspires you to create anything.
Posted on June 17, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.