watchforstock / evohome-client

Python client to access the Evohome web service
Apache License 2.0
88 stars 52 forks source link

Utilize (single-use) refresh tokens correctly #64

Closed zxdavb closed 5 years ago

zxdavb commented 5 years ago

OMG! I was in such a rebase hell! Anyway, I think this solves #57 and lasts longer than the first refresh_token.

I have run this against my testbed, that expects evohome-client to behave exactly as per tag 0.2.8; it doesn't utilize the new functionality.

If the Username/Password pair is invalid, you get a HTTP_BAD_REQUEST with: {'error': 'invalid_grant'}. If the refresh_token is 'bad' it falls back to the user credentials:

>>> c = EvohomeClient(username, "bad_password")
Traceback (most recent call last):
<<SNIP>>
  File "/srv/hass/lib/python3.6/site-packages/evohomeclient2/__init__.py", line 86, in _basic_login
    raise ValueError("Bad Username/Password, unable to continue.")
ValueError: Bad Username/Password, unable to continue.

>>> c = EvohomeClient(username, "bad_password", refresh_token="bad_refresh_token")
Traceback (most recent call last):
<<SNIP>>
  File "/srv/hass/lib/python3.6/site-packages/evohomeclient2/__init__.py", line 86, in _basic_login
    raise ValueError("Bad Username/Password, unable to continue.")
ValueError: Bad Username/Password, unable to continue.

>>> c = EvohomeClient(username, password, refresh_token="bad_refresh_token")
>>> print(c.access_token_expires)
2019-02-23 10:43:50.716782

You can also get HTTP_401 (unauthorised). Here the access token is 'valid', but not correct (it won't fall back to refresh token/user credentials until the access token expires):

>>> d = EvohomeClient(username, password, refresh_token=c.refresh_token, access_token="bad_access_token", access_token_expires=c.access_token_expires)
Traceback (most recent call last):
<<SNIP>>
  File "/srv/hass/lib/python3.6/site-packages/evohomeclient2/__init__.py", line 167, in user_account
    response.raise_for_status()
  File "/srv/hass/lib/python3.6/site-packages/requests/models.py", line 940, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url: https://tccna.honeywell.com/WebAPI/emea/api/v1/userAccount

You can also get a HTTP_429 (too many API calls). Here, the code falls back to the username password if the refresh token is not accepted:

>>> c = EvohomeClient(username, password, refresh_token="bad_refresh_token")
>>> print(c.access_token_expires)
2019-02-23 10:43:50.716782

>>> d = EvohomeClient(username, password, refresh_token="bad_refresh_token")
Traceback (most recent call last):
<<SNIP>>
  File "/srv/hass/lib/python3.6/site-packages/evohomeclient2/__init__.py", line 114, in _obtain_access_token
    response.raise_for_status()
  File "/srv/hass/lib/python3.6/site-packages/requests/models.py", line 940, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 429 Client Error: Too Many Requests for url: https://tccna.honeywell.com/Auth/OAuth/Token

>>> d = EvohomeClient(username, password, refresh_token=c.refresh_token)
>>> print(c.access_token_expires)
2019-02-23 10:43:50.716782
>>> print(d.access_token_expires)
2019-02-23 10:47:17.986811

This is what happens if there's a HTTP_BAD_REQUEST, which does not return {'error': 'invalid_grant'} (this test is contrived, but demonstrated the functionality):

>>> z = EvohomeClient(username, "bad_password")
Traceback (most recent call last):
<<SNIP>>
  File "/srv/hass/lib/python3.6/site-packages/evohomeclient2/__init__.py", line 112, in _obtain_access_token
    raise requests.HTTPError("Unable to obtain an Access Token: ", response_json)
requests.exceptions.HTTPError: [Errno Unable to obtain an Access Token: ] {'not_error': 'not_invalid_grant'}