How to use a refresh_token to get a new access_token (using Flask-OAuthLib)?

4.5k views Asked by At

I'm building a website + backend with the FLask Framework in which I use Flask-OAuthlib to authenticate with google. After authentication, the backend needs to regularly scan the user his Gmail. So currently users can authenticate my app and I store the access_token and the refresh_token. The access_token expires after one hour, so within that one hour I can get the userinfo like so:

google = oauthManager.remote_app(
        'google',
        consumer_key='xxxxxxxxx.apps.googleusercontent.com',
        consumer_secret='xxxxxxxxx',
        request_token_params={
            'scope': ['https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/gmail.readonly'],
            'access_type': 'offline'
        },
        base_url='https://www.googleapis.com/oauth2/v1/',
        request_token_url=None,
        access_token_method='POST',
        access_token_url='https://accounts.google.com/o/oauth2/token',
        authorize_url='https://accounts.google.com/o/oauth2/auth'
    )

token = (the_stored_access_token, '')
userinfoObj = google.get('userinfo', token=token).data
userinfoObj['id']  # Prints out my google id

Once the hour is over, I need to use the refresh_token (which I've got stored in my database) to request a new access_token. I tried replacing the_stored_access_token with the_stored_refresh_token, but this simply gives me an Invalid Credentials-error.

In this github issue I read the following:

regardless of how you obtained the access token / refresh token (whether through an authorization code grant or resource owner password credentials), you exchange them the same way, by passing the refresh token as refresh_token and grant_type set to 'refresh_token'.

From this I understood I had to create a remote app like so:

google = oauthManager.remote_app(
        'google',
        #  also the consumer_key, secret, request_token_params, etc..
        grant_type='refresh_token',
        refresh_token=u'1/xK_ZIeFn9quwvk4t5VRtE2oYe5yxkRDbP9BQ99NcJT0'
    )

But this leads to a TypeError: __init__() got an unexpected keyword argument 'refresh_token'. So from here I'm kinda lost.

Does anybody know how I can use the refresh_token to get a new access_token? All tips are welcome!

5

There are 5 answers

0
Sourav Kumar On

This is how i got my new access token.

from urllib2 import Request, urlopen, URLError
import json
import mimetools
BOUNDARY = mimetools.choose_boundary()
CRLF = '\r\n'
def EncodeMultiPart(fields, files, file_type='application/xml'):
    """Encodes list of parameters and files for HTTP multipart format.

    Args:
      fields: list of tuples containing name and value of parameters.
      files: list of tuples containing param name, filename, and file contents.
      file_type: string if file type different than application/xml.
    Returns:
      A string to be sent as data for the HTTP post request.
    """
    lines = []
    for (key, value) in fields:
      lines.append('--' + BOUNDARY)
      lines.append('Content-Disposition: form-data; name="%s"' % key)
      lines.append('')  # blank line
      lines.append(value)
    for (key, filename, value) in files:
      lines.append('--' + BOUNDARY)
      lines.append(
          'Content-Disposition: form-data; name="%s"; filename="%s"'
          % (key, filename))
      lines.append('Content-Type: %s' % file_type)
      lines.append('')  # blank line
      lines.append(value)
    lines.append('--' + BOUNDARY + '--')
    lines.append('')  # blank line
    return CRLF.join(lines)
def refresh_token():
    url = "https://oauth2.googleapis.com/token"
    headers = [
             ("grant_type",  "refresh_token"),
             ("client_id", "xxxxxx"),
             ("client_secret", "xxxxxx"),
             ("refresh_token", "xxxxx"),
             ]

    files = []
    edata = EncodeMultiPart(headers, files, file_type='text/plain')
    #print(EncodeMultiPart(headers, files, file_type='text/plain'))
    headers = {}
    request = Request(url, headers=headers)
    request.add_data(edata)

    request.add_header('Content-Length', str(len(edata)))
    request.add_header('Content-Type', 'multipart/form-data;boundary=%s' % BOUNDARY)
    response = urlopen(request).read()
    print(response)
refresh_token()
#response = json.decode(response)
#print(refresh_token())
0
tozCSS On

With your refresh_token, you can get a new access_token like:

from google.oauth2.credentials import Credentials
from google.auth.transport import requests

creds = {"refresh_token": "<goes here>",
  "token_uri": "https://accounts.google.com/o/oauth2/token",
  "client_id": "<YOUR_CLIENT_ID>.apps.googleusercontent.com",
  "client_secret": "<goes here>",
  "scopes": ["https://www.googleapis.com/auth/userinfo.email"]}
cred = Credentials.from_authorized_user_info(creds)
cred.refresh(requests.Request())
my_new_access_token = cred.token 
0
matt3o On

flask-oauthlib.contrib contains an parameter named auto_refresh_url / refresh_token_url in the remote_app which does exactely what you wanted to wanted to do. An example how to use it looks like this:

app= oauth.remote_app(
    [...]
    refresh_token_url='https://www.douban.com/service/auth2/token',
    authorization_url='https://www.douban.com/service/auth2/auth',
    [...]
)

However I did not manage to get it running this way. Nevertheless this is possible without the contrib package. My solution was to catch 401 API calls and redirect to a refresh page if a refresh_token is available. My code for the refresh endpoint looks as follows:

@app.route('/refresh/')
def refresh():
    data = {}
    data['grant_type'] = 'refresh_token'
    data['refresh_token'] = session['refresh_token'][0]
    data['client_id'] = CLIENT_ID
    data['client_secret'] = CLIENT_SECRET
    # make custom POST request to get the new token pair
    resp = remote.post(remote.access_token_url, data=data)

    # checks the response status and parses the new tokens
    # if refresh failed will redirect to login
    parse_authorized_response(resp)

    return redirect('/')

def parse_authorized_response(resp):
    if resp is None:
        return 'Access denied: reason=%s error=%s' % (
            request.args['error_reason'],
            request.args['error_description']
        )
    if isinstance(resp, dict):
            session['access_token'] = (resp['access_token'], '')
            session['refresh_token'] = (resp['refresh_token'], '')  
    elif isinstance(resp, OAuthResponse):
        print(resp.status)
        if resp.status != 200:
            session['access_token'] = None
            session['refresh_token'] = None
            return redirect(url_for('login'))
        else:
            session['access_token'] = (resp.data['access_token'], '')
            session['refresh_token'] = (resp.data['refresh_token'], '')
    else:
        raise Exception()
    return redirect('/')

Hope this will help. The code can be enhanced of course and there surely is a more elegant way than catching 401ers but it's a start ;)

One other thing: Do not store the tokens in the Flask Session Cookie. Rather use Server Side Sessions from "Flask Session" which I did in my code!

0
pech0rin On

Looking at the source code for OAuthRemoteApp. The constructor does not take a keyword argument called refresh_token. It does however take an argument called access_token_params which is an optional dictionary of parameters to forward to the access token url.

Since the url is the same, but the grant type is different. I imagine a call like this should work:

google = oauthManager.remote_app(
    'google',
    #  also the consumer_key, secret, request_token_params, etc..
    grant_type='refresh_token',
    access_token_params = {
       refresh_token=u'1/xK_ZIeFn9quwvk4t5VRtE2oYe5yxkRDbP9BQ99NcJT0'
    }
)
0
JaredM On

This is how I get a new access_token for google:

from urllib2 import Request, urlopen, URLError
from webapp2_extras import json
import mimetools
BOUNDARY = mimetools.choose_boundary()
def refresh_token()
    url = google_config['access_token_url']
    headers = [
             ("grant_type", "refresh_token"),
             ("client_id", <client_id>),
             ("client_secret", <client_secret>),
             ("refresh_token", <refresh_token>),
             ]

    files = []
    edata = EncodeMultiPart(headers, files, file_type='text/plain')
    headers = {}
    request = Request(url, headers=headers)
    request.add_data(edata)

    request.add_header('Content-Length', str(len(edata)))
    request.add_header('Content-Type', 'multipart/form-data;boundary=%s' % BOUNDARY)
    try:
        response = urlopen(request).read()
        response = json.decode(response)
    except URLError, e:
        ...

EncodeMultipart function is taken from here: https://developers.google.com/cloud-print/docs/pythonCode

Be sure to use the same BOUNDARY