Skip to content

Commit

Permalink
Add refresh token after API auth change
Browse files Browse the repository at this point in the history
* fix: handle access token expiration

Fixes #1

* fix: correct requirement pyjwt name

* fix: check atributes before refresh/login

* Apply suggestions from code review

Co-authored-by: Milan Meulemans <[email protected]>

* fix: token expiration offset name

* fix: refresh method name

* fix: use access token only for selected requests

* fix: don't rely on access_token presence

* fix: updte to v2 of the API

---------

Co-authored-by: hopkins-tk <[email protected]>
Co-authored-by: Milan Meulemans <[email protected]>
  • Loading branch information
3 people authored Aug 25, 2023
1 parent 35b72bd commit cbb1d0e
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 13 deletions.
83 changes: 72 additions & 11 deletions aioaseko/mobile.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,42 +25,49 @@
from .exceptions import APIUnavailable, InvalidAuthCredentials
from .unit import Unit

from jwt import decode, get_unverified_header
from time import time


if TYPE_CHECKING:
from aiohttp import ClientResponse, ClientSession

TOKEN_EXPIRATION_OFFSET = 60

class MobileAccount:
"""Aseko account."""

_access_token: str | None = None
_access_token_expiration: int | None = None

def __init__(
self,
session: ClientSession,
username: str | None = None,
password: str | None = None,
access_token: str | None = None,
refresh_token: str | None = None,
) -> None:
"""Init Aseko account."""
self._session = session
self._username = username
self._password = password
self._access_token = access_token
self._refresh_token = refresh_token

@property
def access_token(self) -> str | None:
"""Return access token."""
return self._access_token
def refresh_token(self) -> str | None:
"""Return refresh token."""
return self._refresh_token

async def _request(
self, method: str, path: str, data: dict | None = None
self, method: str, path: str, data: dict | None = None, auth: bool = True
) -> ClientResponse:
"""Make a request to the Aseko mobile API."""
resp = await self._session.request(
method,
f"https://pool.aseko.com/api/v1/{path}",
f"https://pool.aseko.com/api/v2/{path}",
data=data,
headers=None
if self._access_token is None
else {"access-token": self.access_token},
headers=None if not auth
else {"access-token": await self._get_valid_access_token()},
)
if resp.status == 401:
raise InvalidAuthCredentials
Expand All @@ -80,13 +87,53 @@ async def login(self) -> None:
"password": self._password,
"firebaseId": "",
},
False,
)
data = await resp.json()
self._access_token = data["accessToken"]
self._retrieve_tokens(data)

async def _get_valid_access_token(self) -> str:
"""Return a valid access token."""
if self._access_token is not None:
assert self._access_token_expiration is not None
if self._access_token_expiration >= time() + TOKEN_EXPIRATION_OFFSET:
return self._access_token
else:
self._access_token = None
if self._refresh_token is not None:
try:
await self._refresh()
except InvalidAuthCredentials:
self._refresh_token = None
else:
assert self._access_token is not None
return self._access_token
if self._username is not None and self._password is not None:
await self.login()
assert self._access_token is not None
return self._access_token
raise InvalidAuthCredentials("No valid refresh token or username and password available.")

async def _refresh(self) -> None:
"""Refresh access token for Aseko Pool Live with refresh token."""
resp = await self._request(
"post",
"refresh",
{
"refreshToken": self._refresh_token,
"firebaseId": "",
},
False,
)
data = await resp.json()
self._retrieve_tokens(data)

async def logout(self) -> None:
"""Logout Aseko Pool Live account."""
await self._request("post", "logout")
self._access_token = None
self._access_token_expiration = None
self._refresh_token = None

async def get_units(self) -> list[Unit]:
"""Get units."""
Expand All @@ -106,3 +153,17 @@ async def get_units(self) -> list[Unit]:
)
for item in data["items"]
]

def _retrieve_tokens(self, data: dict) -> None:
algorithm = get_unverified_header(data["accessToken"]).get('alg')
token = decode(
jwt=data["accessToken"],
key="",
algorithms=algorithm,
options={
"verify_signature": False
},
)
self._access_token = data["accessToken"]
self._access_token_expiration = token["exp"]
self._refresh_token = data["refreshToken"]
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setup(
name="aioaseko",
version="0.0.2",
version="0.1.0",
author="Milan Meulemans",
author_email="[email protected]",
description="Async Python package for the Aseko Pool Live API",
Expand Down Expand Up @@ -33,5 +33,5 @@
python_requires=">=3.8",
packages=["aioaseko"],
package_data={"aioaseko": ["py.typed"]},
install_requires=["aiohttp"]
install_requires=["aiohttp", "pyjwt", "time"]
)

0 comments on commit cbb1d0e

Please sign in to comment.