DEV Community

Jessica Garson for TwitterDev

Posted on

Handling Refresh Tokens in the OAuth 2.0 Authorization Code Flow with PKCE with Flask

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"]
Enter fullscreen mode Exit fullscreen mode

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 requests

  • requests_oauthlib for working with OAuth 2.0

  • os for parsing environment variables and creating random strings

  • re, base64 and hashlib to create the code challenge and code verifier

  • flask 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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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("=", "")

Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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,
    )
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()

Enter fullscreen mode Exit fullscreen mode

To run the file locally you can run the following line:

python app.py
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (2)

Collapse
ahmad_butt_faa7e5cc876ea7 profile image
Ahmad

would be great if you included some diagrams, then non python devs can understand the protocol too :) but thanks for the code snippets!

Collapse
jessicagarson profile image
Jessica Garson Author

Thanks for your feedback. We'll be posting other resources and code samples in the future.