Remove default OAuth implementation from Tesla Fleet (#132431)

This commit is contained in:
Brett Adams 2024-12-07 05:43:37 +10:00 committed by GitHub
parent b30795e1f4
commit 71f5f4bcdd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 10 additions and 143 deletions

View File

@ -34,7 +34,6 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from .config_flow import OAuth2FlowHandler
from .const import DOMAIN, LOGGER, MODELS from .const import DOMAIN, LOGGER, MODELS
from .coordinator import ( from .coordinator import (
TeslaFleetEnergySiteInfoCoordinator, TeslaFleetEnergySiteInfoCoordinator,
@ -42,7 +41,6 @@ from .coordinator import (
TeslaFleetVehicleDataCoordinator, TeslaFleetVehicleDataCoordinator,
) )
from .models import TeslaFleetData, TeslaFleetEnergyData, TeslaFleetVehicleData from .models import TeslaFleetData, TeslaFleetEnergyData, TeslaFleetVehicleData
from .oauth import TeslaSystemImplementation
PLATFORMS: Final = [ PLATFORMS: Final = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
@ -73,11 +71,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -
scopes: list[Scope] = [Scope(s) for s in token["scp"]] scopes: list[Scope] = [Scope(s) for s in token["scp"]]
region: str = token["ou_code"].lower() region: str = token["ou_code"].lower()
OAuth2FlowHandler.async_register_implementation(
hass,
TeslaSystemImplementation(hass),
)
implementation = await async_get_config_entry_implementation(hass, entry) implementation = await async_get_config_entry_implementation(hass, entry)
oauth_session = OAuth2Session(hass, entry, implementation) oauth_session = OAuth2Session(hass, entry, implementation)
refresh_lock = asyncio.Lock() refresh_lock = asyncio.Lock()

View File

@ -12,7 +12,6 @@ from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers import config_entry_oauth2_flow
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER
from .oauth import TeslaSystemImplementation
class OAuth2FlowHandler( class OAuth2FlowHandler(
@ -31,11 +30,6 @@ class OAuth2FlowHandler(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle a flow start.""" """Handle a flow start."""
self.async_register_implementation(
self.hass,
TeslaSystemImplementation(self.hass),
)
return await super().async_step_user() return await super().async_step_user()
async def async_oauth_create_entry( async def async_oauth_create_entry(

View File

@ -1,8 +1,5 @@
"""Provide oauth implementations for the Tesla Fleet integration.""" """Provide oauth implementations for the Tesla Fleet integration."""
import base64
import hashlib
import secrets
from typing import Any from typing import Any
from homeassistant.components.application_credentials import ( from homeassistant.components.application_credentials import (
@ -11,59 +8,8 @@ from homeassistant.components.application_credentials import (
ClientCredential, ClientCredential,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow
from .const import AUTHORIZE_URL, CLIENT_ID, DOMAIN, SCOPES, TOKEN_URL from .const import AUTHORIZE_URL, SCOPES, TOKEN_URL
class TeslaSystemImplementation(config_entry_oauth2_flow.LocalOAuth2Implementation):
"""Tesla Fleet API open source Oauth2 implementation."""
code_verifier: str
code_challenge: str
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize open source Oauth2 implementation."""
# Setup PKCE
self.code_verifier = secrets.token_urlsafe(32)
hashed_verifier = hashlib.sha256(self.code_verifier.encode()).digest()
self.code_challenge = (
base64.urlsafe_b64encode(hashed_verifier).decode().replace("=", "")
)
super().__init__(
hass,
DOMAIN,
CLIENT_ID,
"",
AUTHORIZE_URL,
TOKEN_URL,
)
@property
def name(self) -> str:
"""Name of the implementation."""
return "Built-in open source client ID"
@property
def extra_authorize_data(self) -> dict[str, Any]:
"""Extra data that needs to be appended to the authorize url."""
return {
"prompt": "login",
"scope": " ".join(SCOPES),
"code_challenge": self.code_challenge, # PKCE
}
async def async_resolve_external_data(self, external_data: Any) -> dict:
"""Resolve the authorization code to tokens."""
return await self._token_request(
{
"grant_type": "authorization_code",
"code": external_data["code"],
"redirect_uri": external_data["state"]["redirect_uri"],
"code_verifier": self.code_verifier, # PKCE
}
)
class TeslaUserImplementation(AuthImplementation): class TeslaUserImplementation(AuthImplementation):

View File

@ -11,7 +11,6 @@ from homeassistant.components.application_credentials import (
) )
from homeassistant.components.tesla_fleet.const import ( from homeassistant.components.tesla_fleet.const import (
AUTHORIZE_URL, AUTHORIZE_URL,
CLIENT_ID,
DOMAIN, DOMAIN,
SCOPES, SCOPES,
TOKEN_URL, TOKEN_URL,
@ -52,69 +51,18 @@ async def access_token(hass: HomeAssistant) -> str:
) )
@pytest.mark.usefixtures("current_request_with_host") @pytest.fixture(autouse=True)
async def test_full_flow( async def create_credential(hass: HomeAssistant) -> None:
hass: HomeAssistant, """Create a user credential."""
hass_client_no_auth: ClientSessionGenerator, # Create user application credential
aioclient_mock: AiohttpClientMocker, assert await async_setup_component(hass, "application_credentials", {})
access_token: str, await async_import_client_credential(
) -> None:
"""Check full flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
state = config_entry_oauth2_flow._encode_jwt(
hass, hass,
{ DOMAIN,
"flow_id": result["flow_id"], ClientCredential("user_client_id", "user_client_secret"),
"redirect_uri": REDIRECT, "user_cred",
},
) )
assert result["type"] is FlowResultType.EXTERNAL_STEP
assert result["url"].startswith(AUTHORIZE_URL)
parsed_url = urlparse(result["url"])
parsed_query = parse_qs(parsed_url.query)
assert parsed_query["response_type"][0] == "code"
assert parsed_query["client_id"][0] == CLIENT_ID
assert parsed_query["redirect_uri"][0] == REDIRECT
assert parsed_query["state"][0] == state
assert parsed_query["scope"][0] == " ".join(SCOPES)
assert parsed_query["code_challenge"][0] is not None
client = await hass_client_no_auth()
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
assert resp.status == 200
assert resp.headers["content-type"] == "text/html; charset=utf-8"
aioclient_mock.clear_requests()
aioclient_mock.post(
TOKEN_URL,
json={
"refresh_token": "mock-refresh-token",
"access_token": access_token,
"type": "Bearer",
"expires_in": 60,
},
)
with patch(
"homeassistant.components.tesla_fleet.async_setup_entry", return_value=True
) as mock_setup:
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert len(mock_setup.mock_calls) == 1
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == UNIQUE_ID
assert "result" in result
assert result["result"].unique_id == UNIQUE_ID
assert "token" in result["result"].data
assert result["result"].data["token"]["access_token"] == access_token
assert result["result"].data["token"]["refresh_token"] == "mock-refresh-token"
@pytest.mark.usefixtures("current_request_with_host") @pytest.mark.usefixtures("current_request_with_host")
async def test_full_flow_user_cred( async def test_full_flow_user_cred(
@ -125,24 +73,10 @@ async def test_full_flow_user_cred(
) -> None: ) -> None:
"""Check full flow.""" """Check full flow."""
# Create user application credential
assert await async_setup_component(hass, "application_credentials", {})
await async_import_client_credential(
hass,
DOMAIN,
ClientCredential("user_client_id", "user_client_secret"),
"user_cred",
)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
assert result["type"] is FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"implementation": "user_cred"}
)
assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["type"] is FlowResultType.EXTERNAL_STEP
state = config_entry_oauth2_flow._encode_jwt( state = config_entry_oauth2_flow._encode_jwt(