vangorra / python_withings_api

Library for the Withings Health API
MIT License
101 stars 34 forks source link

refresh_cb returning Error code 401 #61

Open ethanopp opened 3 years ago

ethanopp commented 3 years ago

Code has been working now for over a year, stopped working all of the sudden...wondering if something has changed

Code is pretty straightforward, user clicks 'Allow', I parse query params for the code and then pass to the .get_credentials() function.

The code is being parsed correctly so it seems something with the function is off...

query_params = urlparse.urlparse(search)
creds = withings_auth_client.get_credentials(parse_qs(query_params.query)['code'][0])

Here is the error I'm getting

Traceback (most recent call last):
  File "/Users/ethan/PycharmProjects/fitly/src/fitly/pages/settings.py", line 1234, in update_tokens
    creds = withings_auth_client.get_credentials(parse_qs(query_params.query)['code'][0])
  File "/Users/ethan/PycharmProjects/fitly/venv/lib/python3.7/site-packages/withings_api/__init__.py", line 376, in get_credentials
    include_client_id=True,
  File "/Users/ethan/PycharmProjects/fitly/venv/lib/python3.7/site-packages/requests_oauthlib/oauth2_session.py", line 360, in fetch_token
    self._client.parse_request_body_response(r.text, scope=self.scope)
  File "/Users/ethan/PycharmProjects/fitly/venv/lib/python3.7/site-packages/oauthlib/oauth2/rfc6749/clients/base.py", line 421, in parse_request_body_response
    self.token = parse_token_response(body, scope=scope)
  File "/Users/ethan/PycharmProjects/fitly/venv/lib/python3.7/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 431, in parse_token_response
    validate_token_parameters(params)
  File "/Users/ethan/PycharmProjects/fitly/venv/lib/python3.7/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 441, in validate_token_parameters
    raise MissingTokenError(description="Missing access token parameter.")
oauthlib.oauth2.rfc6749.errors.MissingTokenError: (missing_token) Missing access token parameter.

Also, when trying to print(creds) I get invalid syntax (<unknown>, line 1)

ethanopp commented 3 years ago

It seems like the credentials are saving correctly, but when I pass them to the WithingsApi() I get this: unsupported operand type(s) for -: 'int' and 'method'

I am a little confused because the common.py shows token_expiry as int https://github.com/vangorra/python_withings_api/blob/7ac2327249e95a106da0634c2ed87f27bfc3842e/withings_api/common.py#L585

Could you please let me know what I am doing wrong here?

Save the tokens to db table:

# Function for auto saving withings token_dict to db
def save_withings_token(tokens):
    app.server.logger.debug('***** ATTEMPTING TO SAVE TOKENS *****')

    token_dict = tokens.dict()
    # Can't save arrow method to sqlite, so save it as timestamp
    token_dict['created'] = token_dict['created'].timestamp()
    token_dict['token_expiry'] = int(round(token_dict['created'])) + int(token_dict['expires_in'])

    # Delete current tokens
    app.session.execute(delete(apiTokens).where(apiTokens.service == 'Withings'))
    # Insert new tokens
    app.session.add(apiTokens(date_utc=datetime.utcnow(), service='Withings', tokens=str(token_dict)))
    app.session.commit()

    app.session.remove()
    app.server.logger.debug('***** SAVED TOKENS *****')

Query the db table and create the Credentials() object

def current_token_dict():
    try:
        token_dict = app.session.query(apiTokens.tokens).filter(apiTokens.service == 'Withings').first()
        token_dict = ast.literal_eval(token_dict[0]) if token_dict else {}
        app.session.remove()
    except BaseException as e:
        app.server.logger.error(e)
        token_dict = {}

    return token_dict

def withings_creds(token_dict):
    '''
    :param token_dict:
    :return: Withings Credentials Object
    '''
    return Credentials(client_id=client_id,
                       consumer_secret=client_secret,
                       access_token=token_dict['access_token'],
                       token_expiry=token_dict['token_expiry'],
                       token_type=token_dict['token_type'],
                       userid=token_dict['userid'],
                       refresh_token=token_dict['refresh_token'])

Pass the Credentials() object to WithingsAPI to test if tokens are working

def withings_connected():
    token_dict = current_token_dict()
    try:
        if token_dict:
            creds = withings_creds(token_dict)
            client = WithingsApi(credentials=creds, refresh_cb=save_withings_token)
            measures = client.measure_get_meas()
            app.server.logger.debug('Withings Connected')
            return True
    except BaseException as e:
        app.server.logger.error('Withings not connected')
        app.server.logger.error(e)
        return False
ethanopp commented 3 years ago

Alright think I'm pretty close... Got it working now by using Credentials2 instead of Credentials... Only last issue now is that it doesn't seem like the refresh_cb is working if I go into my db and manually update the 'expires_in' to 10... Thanks again in advance

def current_token_dict():
    try:
        token_dict = app.session.query(apiTokens.tokens).filter(apiTokens.service == 'Withings').first()
        token_dict = ast.literal_eval(token_dict[0]) if token_dict else {}
        app.session.remove()
    except BaseException as e:
        app.server.logger.error(e)
        token_dict = {}

    return token_dict

# Function for auto saving withings token_dict to db
def save_withings_token(tokens):
    app.server.logger.debug('***** ATTEMPTING TO SAVE TOKENS *****')

    token_dict = tokens.dict()
    # Can't save arrow method to sqlite, so save it as timestamp
    token_dict['created'] = round(int(token_dict['created'].timestamp()))

    # Delete current tokens
    app.session.execute(delete(apiTokens).where(apiTokens.service == 'Withings'))
    # Insert new tokens
    app.session.add(apiTokens(date_utc=datetime.utcnow(), service='Withings', tokens=str(token_dict)))
    app.session.commit()

    app.session.remove()
    app.server.logger.debug('***** SAVED TOKENS *****')

def withings_creds(token_dict):
    '''
    :param token_dict:
    :return: Withings Credentials Object
    '''

    return Credentials2(client_id=client_id,
                        consumer_secret=client_secret,
                        access_token=token_dict['access_token'],
                        expires_in=token_dict['expires_in'],
                        created=token_dict['created'],
                        token_type=token_dict['token_type'],
                        userid=token_dict['userid'],
                        refresh_token=token_dict['refresh_token'])

def withings_connected():
    token_dict = current_token_dict()
    try:
        if token_dict:
            creds = withings_creds(token_dict)
            client = WithingsApi(credentials=creds, refresh_cb=save_withings_token)
            measures = client.measure_get_meas()
            app.server.logger.debug('Withings Connected')
            return True
    except BaseException as e:
        app.server.logger.error('Withings not connected')
        app.server.logger.error(e)
        return False
ethanopp commented 3 years ago

Put some more time into this... Updated my functions to use pickle as per the integration test example:

def save_withings_token(credentials: CredentialsType) -> None:
    app.server.logger.debug('***** ATTEMPTING TO SAVE TOKENS *****')
    # Delete current tokens
    app.session.execute(delete(apiTokens).where(apiTokens.service == 'Withings'))
    # Insert new tokens
    app.session.add(apiTokens(date_utc=datetime.utcnow(), service='Withings', tokens=pickle.dumps(credentials)))
    app.session.commit()

    app.session.remove()
    app.server.logger.debug('***** SAVED TOKENS *****')

def load_credentials() -> CredentialsType:
    try:
        token_pickle = app.session.query(apiTokens.tokens).filter(apiTokens.service == 'Withings').first().tokens
        creds = cast(CredentialsType, pickle.loads(token_pickle))
        app.session.remove()
    except BaseException as e:
        app.server.logger.error(e)
        creds = None

    return creds

def withings_connected():
    try:
        client = WithingsApi(credentials=load_credentials(), refresh_cb=save_withings_token)
        measures = client.measure_get_meas()
        app.server.logger.debug('Withings Connected')
        return True
    except BaseException as e:
        app.server.logger.error('Withings not connected')
        app.server.logger.error(e)
        return False

# Redirect function to save tokens the first time - ** WORKING **
def update_tokens(search):
    query_params = urlparse.urlparse(search)
    creds = withings_auth_client.get_credentials(parse_qs(query_params.query)['code'][0])
    save_withings_token(creds)

The above all works when I do the initial connection via the authorize_url, and continues to work until the the token expires... When the refresh_db tries to run though, I keep getting Error code 401

@vangorra please help!

ethanopp commented 3 years ago

@vangorra wanted to check in once more to see if you could please help out here.

Thanks in advance...

knarayen commented 3 years ago

I have essentially the same issue as ethanopp. The .credentials file is not being created and it is failing in the token generation also with the following screen.

This site can’t be reached184.187.79.5 took too long to respond. Try:

Checking the connection Checking the proxy and the firewall Running Windows Network Diagnostics ERR_CONNECTION_TIMED_OUT

The app authentication (step 1) works where it asks the user to press the authorize app button. It fails in the second step with the error shown above. I tried the same invocation with localhost:5000 as my uri and it fails with the same error message. Please help. I am running scripts/integration_test.py with mode=None