From 438e1ea52576191ebc4564d01705791b5e08aa38 Mon Sep 17 00:00:00 2001 From: Boris Fersing Date: Tue, 4 Feb 2025 14:41:40 -0500 Subject: [PATCH 1/6] Move oauth2 flow components from pyhilo to hilo. PyHilo had dependencies to HA, this commit allows a transition to the removal of those dependencies. Once the new HA integration is release we'll be able to remove oauth2.py (AuthCodeWithPKCEImplementation) --- pyhilo/api.py | 5 ++--- pyhilo/const.py | 3 +-- pyhilo/oauth2.py | 41 +++++++++------------------------- pyhilo/oauth2helper.py | 50 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 36 deletions(-) create mode 100644 pyhilo/oauth2helper.py diff --git a/pyhilo/api.py b/pyhilo/api.py index 2d111e2..bfe5295 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -12,7 +12,6 @@ from aiohttp import ClientSession from aiohttp.client_exceptions import ClientResponseError import backoff -from homeassistant.helpers import config_entry_oauth2_flow from pyhilo.const import ( ANDROID_CLIENT_ENDPOINT, @@ -67,7 +66,7 @@ def __init__( self, *, session: ClientSession, - oauth_session: config_entry_oauth2_flow.OAuth2Session, + oauth_session, request_retries: int = REQUEST_RETRY, log_traces: bool = False, ) -> None: @@ -98,7 +97,7 @@ async def async_create( cls, *, session: ClientSession, - oauth_session: config_entry_oauth2_flow.OAuth2Session, + oauth_session, request_retries: int = REQUEST_RETRY, log_traces: bool = False, ) -> API: diff --git a/pyhilo/const.py b/pyhilo/const.py index a53edb1..5082873 100755 --- a/pyhilo/const.py +++ b/pyhilo/const.py @@ -3,7 +3,6 @@ from typing import Final import aiohttp -import homeassistant.core LOG: Final = logging.getLogger(__package__) DEFAULT_STATE_FILE: Final = "hilo_state.yaml" @@ -46,7 +45,7 @@ # Request constants -DEFAULT_USER_AGENT: Final = f"PyHilo/{PYHILO_VERSION} HomeAssistant/{homeassistant.core.__version__} aiohttp/{aiohttp.__version__} Python/{platform.python_version()}" +DEFAULT_USER_AGENT: Final = f"PyHilo/{PYHILO_VERSION} aiohttp/{aiohttp.__version__} Python/{platform.python_version()}" # NOTE(dvd): Not sure how to get new ones so I'm using the ones from my emulator diff --git a/pyhilo/oauth2.py b/pyhilo/oauth2.py index 33d4d63..e9a30c0 100644 --- a/pyhilo/oauth2.py +++ b/pyhilo/oauth2.py @@ -1,4 +1,5 @@ """Custom OAuth2 implementation.""" + import base64 import hashlib import os @@ -8,14 +9,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_oauth2_flow import LocalOAuth2Implementation -from pyhilo.const import ( - AUTH_AUTHORIZE, - AUTH_CHALLENGE_METHOD, - AUTH_CLIENT_ID, - AUTH_SCOPE, - AUTH_TOKEN, - DOMAIN, -) +from pyhilo.const import AUTH_AUTHORIZE, AUTH_CLIENT_ID, AUTH_TOKEN, DOMAIN + +from pyhilo.oauth2helper import OAuth2Helper class AuthCodeWithPKCEImplementation(LocalOAuth2Implementation): # type: ignore[misc] @@ -34,8 +30,8 @@ def __init__( AUTH_AUTHORIZE, AUTH_TOKEN, ) - self._code_verifier = self._get_code_verifier() - self._code_challenge = self._get_code_challange(self._code_verifier) + + self.oauth_helper = OAuth2Helper() # ... Override AbstractOAuth2Implementation details @property @@ -46,32 +42,15 @@ def name(self) -> str: @property def extra_authorize_data(self) -> dict: """Extra data that needs to be appended to the authorize url.""" - return { - "scope": AUTH_SCOPE, - "code_challenge": self._code_challenge, - "code_challenge_method": AUTH_CHALLENGE_METHOD, - } + return self.oauth_helper.get_authorize_parameters() async def async_resolve_external_data(self, external_data: Any) -> dict: """Resolve the authorization code to tokens.""" return cast( dict, await self._token_request( - { - "grant_type": "authorization_code", - "code": external_data["code"], - "redirect_uri": external_data["state"]["redirect_uri"], - "code_verifier": self._code_verifier, - }, + self.oauth_helper.get_token_request_parameters( + external_data["code"], external_data["state"]["redirect_uri"] + ) ), ) - - # Ref : https://blog.sanghviharshit.com/reverse-engineering-private-api-oauth-code-flow-with-pkce/ - def _get_code_verifier(self) -> str: - code = base64.urlsafe_b64encode(os.urandom(40)).decode("utf-8") - return re.sub("[^a-zA-Z0-9]+", "", code) - - def _get_code_challange(self, verifier: str) -> str: - sha_verifier = hashlib.sha256(verifier.encode("utf-8")).digest() - code = base64.urlsafe_b64encode(sha_verifier).decode("utf-8") - return code.replace("=", "") diff --git a/pyhilo/oauth2helper.py b/pyhilo/oauth2helper.py new file mode 100644 index 0000000..9d44d75 --- /dev/null +++ b/pyhilo/oauth2helper.py @@ -0,0 +1,50 @@ +import base64 +import hashlib +import os +import re +from typing import Any, cast + + +from pyhilo.const import ( + AUTH_AUTHORIZE, + AUTH_CHALLENGE_METHOD, + AUTH_CLIENT_ID, + AUTH_SCOPE, + AUTH_TOKEN, +) + +class OAuth2Helper: + """Custom OAuth2 implementation.""" + + def __init__(self) -> None: + self._code_verifier = self._get_code_verifier() + self._code_challenge = self._get_code_challenge(self._code_verifier) + + # Ref : https://blog.sanghviharshit.com/reverse-engineering-private-api-oauth-code-flow-with-pkce/ + def _get_code_verifier(self) -> str: + code = base64.urlsafe_b64encode(os.urandom(40)).decode("utf-8") + return re.sub("[^a-zA-Z0-9]+", "", code) + + def _get_code_challenge(self, verifier: str) -> str: + sha_verifier = hashlib.sha256(verifier.encode("utf-8")).digest() + code = base64.urlsafe_b64encode(sha_verifier).decode("utf-8") + return code.replace("=", "") + + def get_authorize_parameters(self): + return { + "scope": AUTH_SCOPE, + "code_challenge": self._code_challenge, + "code_challenge_method": AUTH_CHALLENGE_METHOD, + "response_type": "code", + "client_id": AUTH_CLIENT_ID + } + + def get_token_request_parameters(self, code, redirect_uri): + return { + "grant_type": "authorization_code", + "code": code, + "redirect_uri": redirect_uri, + "code_verifier": self._code_verifier, + } + + From 20ef99c913d1d131d5c7005575ebe3705b7d2827 Mon Sep 17 00:00:00 2001 From: Boris Fersing Date: Tue, 4 Feb 2025 14:41:40 -0500 Subject: [PATCH 2/6] Move oauth2 flow components from pyhilo to hilo. PyHilo had dependencies to HA, this commit allows a transition to the removal of those dependencies. Once the new HA integration is release we'll be able to remove oauth2.py (AuthCodeWithPKCEImplementation) --- pyhilo/api.py | 5 ++--- pyhilo/const.py | 3 +-- pyhilo/oauth2.py | 41 +++++++++------------------------- pyhilo/oauth2helper.py | 50 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 36 deletions(-) create mode 100644 pyhilo/oauth2helper.py diff --git a/pyhilo/api.py b/pyhilo/api.py index 2d111e2..bfe5295 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -12,7 +12,6 @@ from aiohttp import ClientSession from aiohttp.client_exceptions import ClientResponseError import backoff -from homeassistant.helpers import config_entry_oauth2_flow from pyhilo.const import ( ANDROID_CLIENT_ENDPOINT, @@ -67,7 +66,7 @@ def __init__( self, *, session: ClientSession, - oauth_session: config_entry_oauth2_flow.OAuth2Session, + oauth_session, request_retries: int = REQUEST_RETRY, log_traces: bool = False, ) -> None: @@ -98,7 +97,7 @@ async def async_create( cls, *, session: ClientSession, - oauth_session: config_entry_oauth2_flow.OAuth2Session, + oauth_session, request_retries: int = REQUEST_RETRY, log_traces: bool = False, ) -> API: diff --git a/pyhilo/const.py b/pyhilo/const.py index 27e8c0c..c1f0adb 100755 --- a/pyhilo/const.py +++ b/pyhilo/const.py @@ -3,7 +3,6 @@ from typing import Final import aiohttp -import homeassistant.core LOG: Final = logging.getLogger(__package__) DEFAULT_STATE_FILE: Final = "hilo_state.yaml" @@ -46,7 +45,7 @@ # Request constants -DEFAULT_USER_AGENT: Final = f"PyHilo/{PYHILO_VERSION} HomeAssistant/{homeassistant.core.__version__} aiohttp/{aiohttp.__version__} Python/{platform.python_version()}" +DEFAULT_USER_AGENT: Final = f"PyHilo/{PYHILO_VERSION} aiohttp/{aiohttp.__version__} Python/{platform.python_version()}" # NOTE(dvd): Not sure how to get new ones so I'm using the ones from my emulator diff --git a/pyhilo/oauth2.py b/pyhilo/oauth2.py index 33d4d63..e9a30c0 100644 --- a/pyhilo/oauth2.py +++ b/pyhilo/oauth2.py @@ -1,4 +1,5 @@ """Custom OAuth2 implementation.""" + import base64 import hashlib import os @@ -8,14 +9,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_oauth2_flow import LocalOAuth2Implementation -from pyhilo.const import ( - AUTH_AUTHORIZE, - AUTH_CHALLENGE_METHOD, - AUTH_CLIENT_ID, - AUTH_SCOPE, - AUTH_TOKEN, - DOMAIN, -) +from pyhilo.const import AUTH_AUTHORIZE, AUTH_CLIENT_ID, AUTH_TOKEN, DOMAIN + +from pyhilo.oauth2helper import OAuth2Helper class AuthCodeWithPKCEImplementation(LocalOAuth2Implementation): # type: ignore[misc] @@ -34,8 +30,8 @@ def __init__( AUTH_AUTHORIZE, AUTH_TOKEN, ) - self._code_verifier = self._get_code_verifier() - self._code_challenge = self._get_code_challange(self._code_verifier) + + self.oauth_helper = OAuth2Helper() # ... Override AbstractOAuth2Implementation details @property @@ -46,32 +42,15 @@ def name(self) -> str: @property def extra_authorize_data(self) -> dict: """Extra data that needs to be appended to the authorize url.""" - return { - "scope": AUTH_SCOPE, - "code_challenge": self._code_challenge, - "code_challenge_method": AUTH_CHALLENGE_METHOD, - } + return self.oauth_helper.get_authorize_parameters() async def async_resolve_external_data(self, external_data: Any) -> dict: """Resolve the authorization code to tokens.""" return cast( dict, await self._token_request( - { - "grant_type": "authorization_code", - "code": external_data["code"], - "redirect_uri": external_data["state"]["redirect_uri"], - "code_verifier": self._code_verifier, - }, + self.oauth_helper.get_token_request_parameters( + external_data["code"], external_data["state"]["redirect_uri"] + ) ), ) - - # Ref : https://blog.sanghviharshit.com/reverse-engineering-private-api-oauth-code-flow-with-pkce/ - def _get_code_verifier(self) -> str: - code = base64.urlsafe_b64encode(os.urandom(40)).decode("utf-8") - return re.sub("[^a-zA-Z0-9]+", "", code) - - def _get_code_challange(self, verifier: str) -> str: - sha_verifier = hashlib.sha256(verifier.encode("utf-8")).digest() - code = base64.urlsafe_b64encode(sha_verifier).decode("utf-8") - return code.replace("=", "") diff --git a/pyhilo/oauth2helper.py b/pyhilo/oauth2helper.py new file mode 100644 index 0000000..9d44d75 --- /dev/null +++ b/pyhilo/oauth2helper.py @@ -0,0 +1,50 @@ +import base64 +import hashlib +import os +import re +from typing import Any, cast + + +from pyhilo.const import ( + AUTH_AUTHORIZE, + AUTH_CHALLENGE_METHOD, + AUTH_CLIENT_ID, + AUTH_SCOPE, + AUTH_TOKEN, +) + +class OAuth2Helper: + """Custom OAuth2 implementation.""" + + def __init__(self) -> None: + self._code_verifier = self._get_code_verifier() + self._code_challenge = self._get_code_challenge(self._code_verifier) + + # Ref : https://blog.sanghviharshit.com/reverse-engineering-private-api-oauth-code-flow-with-pkce/ + def _get_code_verifier(self) -> str: + code = base64.urlsafe_b64encode(os.urandom(40)).decode("utf-8") + return re.sub("[^a-zA-Z0-9]+", "", code) + + def _get_code_challenge(self, verifier: str) -> str: + sha_verifier = hashlib.sha256(verifier.encode("utf-8")).digest() + code = base64.urlsafe_b64encode(sha_verifier).decode("utf-8") + return code.replace("=", "") + + def get_authorize_parameters(self): + return { + "scope": AUTH_SCOPE, + "code_challenge": self._code_challenge, + "code_challenge_method": AUTH_CHALLENGE_METHOD, + "response_type": "code", + "client_id": AUTH_CLIENT_ID + } + + def get_token_request_parameters(self, code, redirect_uri): + return { + "grant_type": "authorization_code", + "code": code, + "redirect_uri": redirect_uri, + "code_verifier": self._code_verifier, + } + + From 426b9286faa591c53c5b4ed88e04848a82502fcf Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Sun, 9 Feb 2025 20:35:09 -0500 Subject: [PATCH 3/6] Linting --- pyhilo/api.py | 2 +- pyhilo/oauth2.py | 1 - pyhilo/oauth2helper.py | 6 ++---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pyhilo/api.py b/pyhilo/api.py index bfe5295..531569f 100755 --- a/pyhilo/api.py +++ b/pyhilo/api.py @@ -384,7 +384,7 @@ async def _async_post_init(self) -> None: # instantiate differently self.websocket_devices = WebsocketClient(self.websocket_manager.devicehub) - # For backward compatibility during the transition to challengehub websocket + # For backward compatibility during the transition to challengehub websocket self.websocket = self.websocket_devices self.websocket_challenges = WebsocketClient(self.websocket_manager.challengehub) diff --git a/pyhilo/oauth2.py b/pyhilo/oauth2.py index e9a30c0..a7c9816 100644 --- a/pyhilo/oauth2.py +++ b/pyhilo/oauth2.py @@ -10,7 +10,6 @@ from homeassistant.helpers.config_entry_oauth2_flow import LocalOAuth2Implementation from pyhilo.const import AUTH_AUTHORIZE, AUTH_CLIENT_ID, AUTH_TOKEN, DOMAIN - from pyhilo.oauth2helper import OAuth2Helper diff --git a/pyhilo/oauth2helper.py b/pyhilo/oauth2helper.py index 9d44d75..697ffba 100644 --- a/pyhilo/oauth2helper.py +++ b/pyhilo/oauth2helper.py @@ -4,7 +4,6 @@ import re from typing import Any, cast - from pyhilo.const import ( AUTH_AUTHORIZE, AUTH_CHALLENGE_METHOD, @@ -13,6 +12,7 @@ AUTH_TOKEN, ) + class OAuth2Helper: """Custom OAuth2 implementation.""" @@ -36,7 +36,7 @@ def get_authorize_parameters(self): "code_challenge": self._code_challenge, "code_challenge_method": AUTH_CHALLENGE_METHOD, "response_type": "code", - "client_id": AUTH_CLIENT_ID + "client_id": AUTH_CLIENT_ID, } def get_token_request_parameters(self, code, redirect_uri): @@ -46,5 +46,3 @@ def get_token_request_parameters(self, code, redirect_uri): "redirect_uri": redirect_uri, "code_verifier": self._code_verifier, } - - From 095ec8b0996c7fd2f3faf4cd54f2c3f902eb813a Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Wed, 12 Feb 2025 20:59:05 -0500 Subject: [PATCH 4/6] Revert "Merge branch 'main' into pr/237" This reverts commit c21167370c9d16ef1883a9cf49e74828d489789b, reversing changes made to b60f14c2d19107da0caa69a59dec24fc2c384555. --- pyhilo/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyhilo/const.py b/pyhilo/const.py index c1f0adb..5082873 100755 --- a/pyhilo/const.py +++ b/pyhilo/const.py @@ -7,7 +7,7 @@ LOG: Final = logging.getLogger(__package__) DEFAULT_STATE_FILE: Final = "hilo_state.yaml" REQUEST_RETRY: Final = 9 -PYHILO_VERSION: Final = "2025.2.01" +PYHILO_VERSION: Final = "2024.10.02" # TODO: Find a way to keep previous line in sync with pyproject.toml automatically CONTENT_TYPE_FORM: Final = "application/x-www-form-urlencoded" diff --git a/pyproject.toml b/pyproject.toml index 0c8de27..7fa0310 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ exclude = ".venv/.*" [tool.poetry] name = "python-hilo" -version = "2025.2.1" +version = "2024.10.2" description = "A Python3, async interface to the Hilo API" readme = "README.md" authors = ["David Vallee Delisle "] From 59f2063b13b0e0d9ea5b81dd68b4781238d5d374 Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Wed, 12 Feb 2025 21:01:40 -0500 Subject: [PATCH 5/6] Bump up version --- pyhilo/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyhilo/const.py b/pyhilo/const.py index 5082873..6ea3751 100755 --- a/pyhilo/const.py +++ b/pyhilo/const.py @@ -7,7 +7,7 @@ LOG: Final = logging.getLogger(__package__) DEFAULT_STATE_FILE: Final = "hilo_state.yaml" REQUEST_RETRY: Final = 9 -PYHILO_VERSION: Final = "2024.10.02" +PYHILO_VERSION: Final = "2025.2.02" # TODO: Find a way to keep previous line in sync with pyproject.toml automatically CONTENT_TYPE_FORM: Final = "application/x-www-form-urlencoded" diff --git a/pyproject.toml b/pyproject.toml index 7fa0310..0af7cf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ exclude = ".venv/.*" [tool.poetry] name = "python-hilo" -version = "2024.10.2" +version = "2025.2.02" description = "A Python3, async interface to the Hilo API" readme = "README.md" authors = ["David Vallee Delisle "] From f4441c553bb2fa81295bff29d7a2e8b06d60ecc8 Mon Sep 17 00:00:00 2001 From: "Ian C." <108159253+ic-dev21@users.noreply.github.com> Date: Wed, 12 Feb 2025 21:03:54 -0500 Subject: [PATCH 6/6] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0af7cf3..4af4255 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ exclude = ".venv/.*" [tool.poetry] name = "python-hilo" -version = "2025.2.02" +version = "2025.2.2" description = "A Python3, async interface to the Hilo API" readme = "README.md" authors = ["David Vallee Delisle "]