singingwolfboy / flask-dance

Doing the OAuth dance with style using Flask, requests, and oauthlib.
https://pypi.python.org/pypi/Flask-Dance/
MIT License
1.01k stars 158 forks source link

getting flask-dance to auto refresh my expired tokens #391

Open lila opened 2 years ago

lila commented 2 years ago

Hi,

I'm using the fitbit flask-dance contributed module. All is good, but when my token expires, then i would like to configure flask-dance and requests-oauthlib to automatically refresh the token if expired.

To do that with fitbit oauth, i use the same token url, but need to supply it with different body:

Authorization: Basic Y2xpZW50X2lkOmNsaWVudCBzZWNyZXQ=
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=abcdef01234567890abcdef01234567890abcdef01234567890abcdef0123456

The authorization header is "Basic " + base64 encoded "client_id:client_secret". the body has grant_type and includes the refresh token.

I see that requests_oauthlib does have the mechanism to automatically refresh the token, see https://github.com/requests/requests-oauthlib/blob/master/requests_oauthlib/oauth2_session.py#L405 for example.

and it does check for expired tokens.

my question is: how can i configure the flask-dance fitbit module so that it does the right thing. All i see are two parameters, fitbit_bp.auto_refresh_url and fitbit_bp.auto_refresh_kwargs (see https://github.com/singingwolfboy/flask-dance/blob/main/flask_dance/contrib/fitbit.py )

i set fitbit_bp.auto_refresh_url to the current url for refreshing the tokens, and i tried setting fitbit_bp.auto_refresh_kwargs in a few different ways, but i'm just not getting a valid response.

any help is greatly appreciated. thanks in advance...

k

lila commented 2 years ago

testing out with my minimal flask-dance-fitbit application: https://github.com/lila/flask-dance-fitbit

added a route /fitbitexpiretoken that does the following:

@app.route("/fitbitexpiretoken")
def fitbitexpire():
    """expires the fitbit token and forces a token refresh"""

    if fitbit.authorized:
        time_past = time() - 10
        fitbit_bp.token['expires_at'] = time_past
        print("access token: " + fitbit_bp.token['access_token'])
        print("refresh_token: " + fitbit_bp.token['refresh_token'])
        print("expiration time " + str(fitbit_bp.token['expires_at']))
        print("             in " + str(fitbit_bp.token['expires_in']))

        # this will fail due to expired token
        try:
            resp = fitbit.get("/1/user/-/profile.json",
                              headers={"Authorization": "Bearer " +
                                       fitbit_bp.token["access_token"]},
                              )
            print(resp)
            print("access token: " + fitbit_bp.token['access_token'])
            print("refresh_token: " + fitbit_bp.token['refresh_token'])
            print("expiration time " + str(fitbit_bp.token['expires_at']))
            print("             in " + str(fitbit_bp.token['expires_in']))

        except Exception:
            print("exception")

        return "done"

When i hit this url, i now get:

access token: eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIyMzhDREoiLCJzdWIiOiI2M05IWkMiLCJpc3MiOiJGaXRiaXQiLCJ0eXAiOiJhY2Nlc3NfdG9rZW4iLCJzY29wZXMiOiJyYWN0IHJwcm8iLCJleHAiOjE2NTQ1NjE2MzcsImlhdCI6MTY1NDUzMjgzN30.weJYnko13djbyJ2jZ3DH9OLvJet3Ge3TrjY9GwBPboI
refresh_token: a81d83ee3c0d20dbb573e44c2b19d656526700c5181f0bbec945d1aba59c35ef
expiration time 1654543853.5550377
             in -10.001423
<Response [200]>
access token: eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIyMzhDREoiLCJzdWIiOiI2M05IWkMiLCJpc3MiOiJGaXRiaXQiLCJ0eXAiOiJhY2Nlc3NfdG9rZW4iLCJzY29wZXMiOiJyYWN0IHJwcm8iLCJleHAiOjE2NTQ1NzI2NzcsImlhdCI6MTY1NDU0Mzg3N30.fjiIYYEwNFhH1IeRN2sCwk5j82JTcvjJrsbcq--u_Mc
refresh_token: 3e924b221c8cb3500f883935615852006cd3406b2f737a461f4395c67d917a42
expiration time 1654572663.698298
             in 28799.894673
127.0.0.1 - - [06/Jun/2022 19:31:03] "GET /fitbitexpiretoken HTTP/1.1" 200 -

i manually expire the token then issue a get-profile api command. after that the token has been updated with a new refresh token and new expiration.

soo... the upshot is: flask-dance is doing the token refresh automatically.

How exactly does that happen? it feels a bit like magic to me, as i need a very specific authorization header when refreshing the token. i find it difficult to see how flask-dance or requests-oauth2lib figures that out.

Any explanation would be helpful.

Thanks in advance :-)

lila commented 2 years ago

if i turn the debugging on for requests_oauthlib using:

import logging
import sys
log = logging.getLogger('requests_oauthlib')
log.addHandler(logging.StreamHandler(sys.stdout))
log.setLevel(logging.DEBUG)

then i see the following debug statements:

...
Auto refresh is set, attempting to refresh at https://api.fitbit.com/oauth2/token.
Encoding client_id "XXXXXX" with client_secret as Basic auth credentials.
Adding auto refresh key word arguments {}.
Prepared refresh token request body grant_type=refresh_token&scope=activity+profile&refresh_token=3e924b221c8cb3500f883935615852006cd3406b2f737a461f4395c67d917a42&allow_redirects=True
Requesting url https://api.fitbit.com/oauth2/token using method POST.
Supplying headers {'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'} and data {'grant_type': 'refresh_token', 'scope': 'activity profile', 'refresh_token': '3e924b221c8cb3500f883935615852006cd3406b2f737a461f4395c67d917a42', 'allow_redirects': 'True'}
Passing through key word arguments {'json': None, 'auth': <requests.auth.HTTPBasicAuth object at 0x7f40c65cb8e0>, 'timeout': None, 'verify': True, 'proxies': None}.
Request to refresh token completed with status 200.
...

so it is definitely doing the token refresh. apparently the fitbit refresh api is more standard than i thought...

for the record, requests_oauthlib then uses requests.auth to build the authorization headers, then uses refresh_token() to make the call and update the tokens. this all happens behind the scenes of flask-dance.

All is good in the world of flask-dance. you can close the issue, but i thought i'd add all this here for completeness (and for when i forget)..

cheers and thanks for developing and maintaining flask-dance...