Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions pyhilo/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -385,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)

Expand Down
5 changes: 2 additions & 3 deletions pyhilo/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
from typing import Final

import aiohttp
import homeassistant.core

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 = "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"
Expand Down Expand Up @@ -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
Expand Down
40 changes: 9 additions & 31 deletions pyhilo/oauth2.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Custom OAuth2 implementation."""

import base64
import hashlib
import os
Expand All @@ -8,14 +9,8 @@
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]
Expand All @@ -34,8 +29,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
Expand All @@ -46,32 +41,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("=", "")
47 changes: 47 additions & 0 deletions pyhilo/oauth2helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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,
}
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ exclude = ".venv/.*"

[tool.poetry]
name = "python-hilo"
version = "2025.2.1"
version = "2025.2.2"
description = "A Python3, async interface to the Hilo API"
readme = "README.md"
authors = ["David Vallee Delisle <me@dvd.dev>"]
Expand Down
Loading