Refreshing OAuth 2 Tokens

This guide assumes you've already built an OAuth2 app

If you haven’t, please refer to this guide, and return here once you’ve captured the access_token and refresh_token from a successful OAuth 2 credentials grant.

Basics of token refresh

Assuming you’ve included the offline scope in your OAuth2.0 credentials request, successful authentication via Frame.io’s Accounts application will return a payload that looks like the following:

1{
2 "access_token":"BEARER_TOKEN",
3 "expires_in":3600,
4 "refresh_token":"REFRESH_TOKEN",
5 "scope":"account.read offline",
6 "token_type":"bearer"
7}

The access_token is a bearer token that can be used to act on behalf of the authenticated user; it will expire after 3600 seconds (one hour); and after that, the refresh_token can be used to to fetch a new access_token. The refresh token will then expire after 30 days, at which point you will need the user to login from scratch, producing a new access/refresh token pair; and so on.

If you do not request the offline scope explicitly, you will not receive a refresh_token, and therefore after an hour will have to fully re-authenticate the user.

Capturing the refresh token on successful authentication

Needless to say, you can’t use a refresh_token you don’t have, so be sure in your app to:

  • Request the offline scope
  • Capture the refresh_token that’s returned in a successful callback.

For convenience, the callback from our OAuth 2 App Guides is reproduced here, with an os call to stash the Refresh token. Please note that two examples are provided: one with PKCE configured (does not include basic auth header), and one without (includes basic auth header).

Without PKCE

Python
1def callback():
2 # Where `request` refers to our initial call to the auth URL
3 state = request.args.get('state')
4 scope = request.args.get('scope')
5 code = request.args.get('code')
6 error = request.args.get('error')
7
8 if error:
9 return "Error: " + error
10
11 # Set up for client authorization and set up the data you need to send.
12 client_auth = requests.auth.HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET)
13
14 post_data = {
15 "grant_type": "authorization_code",
16 "code": code,
17 "redirect_uri": REDIRECT_URI,
18 "state": state,
19 "scope": SCOPE
20 }
21
22 # Send a POST request with the data you need to receive an access token.
23 response = requests.post(TOKEN, auth=client_auth, data=post_data)
24 # Stash the refresh token for later
25 os.environ['REFRESH_TOKEN'] = response.json()["refresh_token"]
26
27 return response.text

With PKCE

Python
1def callback():
2 # Where `request` refers to our initial call to the auth URL
3 state = request.args.get('state')
4 scope = request.args.get('scope')
5 code = request.args.get('code')
6 error = request.args.get('error')
7
8 if error:
9 return "Error: " + error
10
11 # If using PKCE, you must include the CLIENT_ID in your request body
12 post_data = {
13 "grant_type": "authorization_code",
14 "code": code,
15 "redirect_uri": REDIRECT_URI,
16 "state": state,
17 "scope": SCOPE
18 "client_id": CLIENT_ID
19 }
20
21 # Send a POST request with the data you need to receive an access token.
22 # If using PKCE, use the below request with no auth
23 response = requests.post(TOKEN_URL, data=post_data)
24 # Stash the refresh token for later
25 os.environ['REFRESH_TOKEN'] = response.json()["refresh_token"]
26
27 return response.text

Executing a refresh

The refresh itself is a single call to Frame.io’s token URL:

A refresh will always include at least the following three attributes in its form data:

  • grant_type: refresh_token
  • scope: <SCOPES>
  • refresh_token: <REFRESH_TOKEN>

If you’re using PKCE, you’ll need to include your app’s client_id in this form data; if not, you’ll need to include a Basic authentication header with your app’s client_id and client_secret as the Username and Password, respectively.

Without PKCE

Similar to making the initial authentication callback without PKCE, this standard refresh will require supplying your client_id and client_secret as the Username and Password in a Basic Authentication header.

Python
1def refresh():
2 # Fetch the refresh token, assuming we have it
3 REFRESH_TOKEN = os.environ.get('REFRESH_TOKEN')
4
5 client_auth = requests.auth.HTTPBasicAuth(CLIENT_ID,CLIENT_SECRET)
6 post_data = {
7 "grant_type": "refresh_token",
8 "scope": SCOPE,
9 "refresh_token": REFRESH_TOKEN
10 # if using PKCE, you will need to include your client_id as below
11 # "client_id": CLIENT_ID
12 }
13
14 response = requests.post(TOKEN_URL, auth=client_auth, data=post_data)
15 # Catch + stash a new Refresh Token
16 os.environ['REFRESH_TOKEN'] = response.json()["refresh_token"]
17
18 return response.text

With PKCE

Again, we’re mimicking the rules of our initial /callback cycle:

  • We don’t include an Authorization header
  • We must include the client_id in our payload
Python
1def refresh():
2 # Fetch the refresh token, assuming we have it
3 REFRESH_TOKEN = os.environ.get('REFRESH_TOKEN')
4
5 post_data = {
6 "grant_type": "refresh_token",
7 "scope": SCOPE,
8 "refresh_token": REFRESH_TOKEN
9 "client_id": CLIENT_ID
10 }
11
12 response = requests.post(TOKEN_URL, data=post_data)
13 # Catch + stash a new Refresh Token
14 os.environ['REFRESH_TOKEN'] = response.json()["refresh_token"]
15
16 return response.text

Congratulations! You can now handle the entire token lifecycle of an OAuth2.0 client application.