orcasgit / python-fitbit

Fitbit API Python Client Implementation
Other
623 stars 330 forks source link

Unclear how to stay authed for longer than a day #119

Open stevenirby opened 7 years ago

stevenirby commented 7 years ago

I'm not sure how to keep my app authed for longer than one day.

Here is what I'm doing:

    authd_client = fitbit.Fitbit(USER_ID, CONSUMER_SECRET,
                             access_token=ACCESS_TOKEN, refresh_token=REFRESH_TOKEN)

How do I auth the app for much longer time?

brad commented 7 years ago

Sorry @stevenirby the decumentation is a bit lacking on this (non-existent). What you need to do is supply an extra kwarg in the Fitbit constructor called refresh_cb. The value should be a function that accepts one argument: a token. The value of that argument will be a dictionary with the keys ['access_token', 'refresh_token', 'expires_at'].

When python fitbit makes a call and discovers the access token is expired, it will automatically refresh the token and pass the new token to the function specified by refresh_cb. You will need to implement this function to store your new token somewhere persistent. The current fitbit client will automatically use the new token so there is no need to update anything there, but the next time you create a fitbit client, you will need to use the updated token that you saved. Does that make sense?

See the function we use in django-fitbit for an example: https://github.com/orcasgit/django-fitbit/blob/3e696d45d58dac1a097d1ed7f82339895418705e/fitapp/models.py#L24

stevenirby commented 7 years ago

Oh wow, thanks for the quick reply! I figured it was close to that. I think I'm good now.

Do you need some help updating docs or the README?

brad commented 7 years ago

@stevenirby we would totally appreciate help with the docs!

toothie96 commented 7 years ago

For those of us with little experience with code, could you possibly write out what lines need to be added and where? It'd be a massive help! thanks!

lrpina commented 7 years ago

Hello!

First, thanks for putting together this api and all the help provided a long the way. I'm still having trouble with refreshing the token and setting up refresh_cb correctly. I've approached setting up refresh_cb in several ways. Including:

lambda x: None

or

lambda x: {}

I also added more specific fields to the token argument to the refresh_cb constructor. I actually replicated the function shown in: https://github.com/orcasgit/django-fitbit/blob/3e696d45d58dac1a097d1ed7f82339895418705e/fitapp/models.py#L24

which makes a lot of sense! And so I replicated the function:

def refresh_cb(self, token):
     """ Called when the OAuth token has been refreshed """
     self.access_token = token['access_token']
     self.refresh_token = token['refresh_token']
     self.expires_at = token['expires_at']
     self.save

For background, this is how initialize the fitbit object

authd = fitbit.Fitbit(consumer_key, consumer_secret, access_token=access_token, refresh_token=refresh_token, redirect_uri='http://localhost:8080', expires_at=expires_at, refresh_cb=refresh_cb)

But I still get the error. I've tried to add print statements the api.py to trace the error and the error is triggered in the call to _request specifically whereresponse = self.session.request(method, url, **kwargs)

From what concerns me here is that code breaks before token_updater in_request is called. Help would be appreciated!

in make request no url
in make_request with url
in _request
printing **kwargs
headers -> {'Accept-Language': 'en_US'}
data -> {}
client_id -> _client_id_ #not shown here for privacy
client_secret -> _client_secret_ #not shown here for privacy
Traceback (most recent call last):
  File "simple_request.py", line 104, in <module>
    sleep_data = authd.get_sleep(date)
  File "/Users/lpina/Repositories/python-fitbit/fitbit/api.py", line 833, in get_sleep
    return self.make_request(url)
  File "/Users/lpina/Repositories/python-fitbit/fitbit/api.py", line 279, in make_request
    response = self.client.make_request(*args, **kwargs)
  File "/Users/lpina/Repositories/python-fitbit/fitbit/api.py", line 109, in make_request
    **kwargs
  File "/Users/lpina/Repositories/python-fitbit/fitbit/api.py", line 80, in _request
    response = self.session.request(method, url, **kwargs)
  File "/Library/Python/2.7/site-packages/requests_oauthlib/oauth2_session.py", line 341, in request
    self.auto_refresh_url, auth=auth, **kwargs
  File "/Library/Python/2.7/site-packages/requests_oauthlib/oauth2_session.py", line 309, in refresh_token
    self.token = self._client.parse_request_body_response(r.text, scope=self.scope)
  File "/Library/Python/2.7/site-packages/oauthlib/oauth2/rfc6749/clients/base.py", line 409, in parse_request_body_response
    self.token = parse_token_response(body, scope=scope)
  File "/Library/Python/2.7/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 376, in parse_token_response
    validate_token_parameters(params)
  File "/Library/Python/2.7/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 383, in validate_token_parameters
    raise_from_error(params.get('error'), params)
  File "/Library/Python/2.7/site-packages/oauthlib/oauth2/rfc6749/errors.py", line 325, in raise_from_error
    raise cls(**kwargs)
oauthlib.oauth2.rfc6749.errors.InvalidGrantError: (invalid_grant) 

For background here's my version of the python-fitbit repository:

https://github.com/lrpina/python-fitbit

stevenirby commented 7 years ago

I haven't had time to come back to this, but I too had problems making this work.

The call back function I passed along never fired. So I'm not sure how to make this work.

lrpina commented 7 years ago

@stevenirby, thanks for the quick reply. Hopefully others can chime in.

lrpina commented 7 years ago

Update! I think it I got it work. Here's how I got mine to work:

def r_cb(token):
     """ Called when the OAuth token has been refreshed """
     access_token = token['access_token']
     refresh_token = token['refresh_token']
     expires_at = token['expires_at']

where the variables on the left ( access_token, refresh_token, expires_at) are local variables that get updated when r_cb is called.

brad commented 7 years ago

@lrpina That could work, but you need to make sure to save those values somewhere permanent (like a database) and use them the next time you create a Fitbit object for the user. Does that make sense?

lrpina commented 7 years ago

@brad, yes! I was just showing an example for others to view what the callback function looks like. Many of use couldn't figure out how to write the refresh_cb function properly. For example, it was unclear what the token dictionary needed to include.

But yes, thanks for clarification. Callback function (in my case r_cb) needs to save the access_token, refresh_token, expires_at to permanent place, such as a database.

tchellomello commented 7 years ago

Thanks for the notes on this issue. I was able to fix a problem this problem https://github.com/home-assistant/home-assistant/pull/9183 for Home Assistant.

stuboo commented 6 years ago

This feature is incredibly valuable and, with the help of this thread, I'm able to successfully execute the callback. My issue is this: because the callback function can only take one variable, I'm at a loss as to how to store the tokens dictionary in my database. I have multiple users and need to be able to insert the credentials with the correct user_id.

I am fully aware that my [in]ability do this is directly proportional to my [lack of] skill as a python programmer so I'm not asking for a change in the code (or even the documentation), but for your advice on how you would solve this problem. Thanks!

For example:

def refresh_callback(tokens):
    """ Called when the OAuth token has been refreshed """
    fbm.store_tokens(study_id, tokens) # a db call in my model

if __name__ == "__main__":
    result = []
    for a in active_user_tokens:
        study_id = a['study_id']
        authd_client = fitbit.Fitbit(config.CLIENT_ID,
                                     config.CLIENT_SECRET,
                                     access_token=a['credentials']['access_token'],
                                     refresh_token=a['credentials']['refresh_token'],
                                     expires_at =a['credentials']['expires_in'],
                                     refresh_cb=refresh_callback)
        result.append(authd_client.sleep())
    print(result)

This code gives me the sleep data (and I can print the new tokens inside the refresh_callback function, but I need access to those variables inside the loop so that I can update the database by a['user_id'].

::facepalm:: global variable code updated to reflect what actually works

brad commented 6 years ago

@stuboo Take a look at how we did it in django-fitbit. The refresh callback is actually a method on the DB model class so it has access to everything it needs:

    def refresh_cb(self, token):
        """ Called when the OAuth token has been refreshed """
        self.access_token = token['access_token']
        self.refresh_token = token['refresh_token']
        self.expires_at = token['expires_at']
        self.save()

Will something like that work for your case?

pete7863 commented 5 years ago

Has anyone had any success getting the refresh callback function to fire? My code is as follows

import datetime
import fitbit
import json

def refresh_callback(token):
    f_data = {
        'access_token': token['access_token'],
        'refresh_token': token['refresh_token'],
        'expires_at': token['expires_at']
    }

    with open('access_token_cache_file.json', 'w') as outfile:
        json.dump(f_data, outfile)

with open('fitbit_config.json') as json_file:
    data = json.load(json_file)
    fitbit_user_id = data['fitbit']['user_id']
    fitbit_client_id = data['fitbit']['client_id']

with open('access_token_cache_file.json') as json_file:
    data = json.load(json_file)
    access_token = data['access_token']
    refresh_token = data['refresh_token']
    expires_at = data['expires_at']

client = fitbit.Fitbit(fitbit_user_id, fitbit_client_id, access_token=access_token, refresh_token=refresh_token,
                       expires_at=expires_at, refresh_cb=refresh_callback)

date = datetime.datetime.today()
print(client.activities(date))

After my token expires, I get the following exceptions (traceback below):

Traceback (most recent call last):
  File "/home/pete7863/.local/lib/python3.5/site-packages/requests_oauthlib/oauth2_session.py", line 395, in request
    http_method=method, body=data, headers=headers)
  File "/home/pete7863/.local/lib/python3.5/site-packages/oauthlib/oauth2/rfc6749/clients/base.py", line 198, in add_token
    raise TokenExpiredError()
oauthlib.oauth2.rfc6749.errors.TokenExpiredError: (token_expired) 

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/pete7863/influxdbmylife/fitbit/main.py", line 102, in <module>
    write_day(date, client, fb.activities(date))
  File "/home/pete7863/.local/lib/python3.5/site-packages/fitbit/utils.py", line 38, in _curried
    return _curried_func(*(args+moreargs), **dict(kwargs, **morekwargs))
  File "/home/pete7863/.local/lib/python3.5/site-packages/fitbit/api.py", line 348, in _COLLECTION_RESOURCE
    return self.make_request(url, data)
  File "/home/pete7863/.local/lib/python3.5/site-packages/fitbit/api.py", line 256, in make_request
    response = self.client.make_request(*args, **kwargs)
  File "/home/pete7863/.local/lib/python3.5/site-packages/fitbit/api.py", line 96, in make_request
    **kwargs
  File "/home/pete7863/.local/lib/python3.5/site-packages/fitbit/api.py", line 68, in _request
    response = self.session.request(method, url, **kwargs)
  File "/home/pete7863/.local/lib/python3.5/site-packages/requests_oauthlib/oauth2_session.py", line 408, in request
    self.auto_refresh_url, auth=auth, **kwargs
  File "/home/pete7863/.local/lib/python3.5/site-packages/requests_oauthlib/oauth2_session.py", line 374, in refresh_token
    self.token = self._client.parse_request_body_response(r.text, scope=self.scope)
  File "/home/pete7863/.local/lib/python3.5/site-packages/oauthlib/oauth2/rfc6749/clients/base.py", line 415, in parse_request_body_response
    self.token = parse_token_response(body, scope=scope)
  File "/home/pete7863/.local/lib/python3.5/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 425, in parse_token_response
    validate_token_parameters(params)
  File "/home/pete7863/.local/lib/python3.5/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 432, in validate_token_parameters
    raise_from_error(params.get('error'), params)
  File "/home/pete7863/.local/lib/python3.5/site-packages/oauthlib/oauth2/rfc6749/errors.py", line 405, in raise_from_error
    raise cls(**kwargs)
oauthlib.oauth2.rfc6749.errors.InvalidClientError: (invalid_client)

Is there something simple I am missing? I've tried to strip down my code to the barest form. Any help is greatly appreciated!

pete7863 commented 5 years ago

I think I figured my issue out. For some reason I was initializing with the user_id and the client_id as the first two parameters when initializing the Fitbit client. Once I fixed this issue, my callback started working properly.

MasonV commented 5 years ago

My issue with refresh_token is that it is worded as something that is automatic, but it is never called within the code itself. The token_updater also doesn't exist or at least I couldn't figure out how to make it exist through passing some sort of function originally.

So my solution was to modify the source code and call it myself.

` def refresh_token(self):

token = {}

token = self.session.refresh_token(

    self.refresh_token_url,

    auth=HTTPBasicAuth(self.client_id, self.client_secret)

return token

`

In my main code I create the fitbit object with stored credentials and try to access data. If that doesn't work then I directly call the refresh_token() function. And if THAT doesn't work then I go through the entire reauthorization process.