Skip to content

Commit

Permalink
Add captcha to BMW ConfigFlow (#131351)
Browse files Browse the repository at this point in the history
Co-authored-by: Franck Nijhof <[email protected]>
  • Loading branch information
rikroe and frenck committed Nov 28, 2024
1 parent ee96093 commit f97d96e
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 74 deletions.
71 changes: 60 additions & 11 deletions homeassistant/components/bmw_connected_drive/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,18 @@
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
from homeassistant.util.ssl import get_default_context

from . import DOMAIN
from .const import CONF_ALLOWED_REGIONS, CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN
from .const import (
CONF_ALLOWED_REGIONS,
CONF_CAPTCHA_REGIONS,
CONF_CAPTCHA_TOKEN,
CONF_CAPTCHA_URL,
CONF_GCID,
CONF_READ_ONLY,
CONF_REFRESH_TOKEN,
)

DATA_SCHEMA = vol.Schema(
{
Expand All @@ -41,7 +50,14 @@
translation_key="regions",
)
),
}
},
extra=vol.REMOVE_EXTRA,
)
CAPTCHA_SCHEMA = vol.Schema(
{
vol.Required(CONF_CAPTCHA_TOKEN): str,
},
extra=vol.REMOVE_EXTRA,
)


Expand All @@ -54,6 +70,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
data[CONF_USERNAME],
data[CONF_PASSWORD],
get_region_from_name(data[CONF_REGION]),
hcaptcha_token=data.get(CONF_CAPTCHA_TOKEN),
verify=get_default_context(),
)

try:
Expand All @@ -79,15 +97,17 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):

VERSION = 1

data: dict[str, Any] = {}

_existing_entry_data: Mapping[str, Any] | None = None

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
errors: dict[str, str] = self.data.pop("errors", {})

if user_input is not None:
if user_input is not None and not errors:
unique_id = f"{user_input[CONF_REGION]}-{user_input[CONF_USERNAME]}"
await self.async_set_unique_id(unique_id)

Expand All @@ -96,22 +116,35 @@ async def async_step_user(
else:
self._abort_if_unique_id_configured()

# Store user input for later use
self.data.update(user_input)

# North America and Rest of World require captcha token
if (
self.data.get(CONF_REGION) in CONF_CAPTCHA_REGIONS
and CONF_CAPTCHA_TOKEN not in self.data
):
return await self.async_step_captcha()

info = None
try:
info = await validate_input(self.hass, user_input)
entry_data = {
**user_input,
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
CONF_GCID: info.get(CONF_GCID),
}
info = await validate_input(self.hass, self.data)
except MissingCaptcha:
errors["base"] = "missing_captcha"
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
finally:
self.data.pop(CONF_CAPTCHA_TOKEN, None)

if info:
entry_data = {
**self.data,
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
CONF_GCID: info.get(CONF_GCID),
}

if self.source == SOURCE_REAUTH:
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data=entry_data
Expand All @@ -128,7 +161,7 @@ async def async_step_user(

schema = self.add_suggested_values_to_schema(
DATA_SCHEMA,
self._existing_entry_data,
self._existing_entry_data or self.data,
)

return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
Expand All @@ -147,6 +180,22 @@ async def async_step_reconfigure(
self._existing_entry_data = self._get_reconfigure_entry().data
return await self.async_step_user()

async def async_step_captcha(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Show captcha form."""
if user_input and user_input.get(CONF_CAPTCHA_TOKEN):
self.data[CONF_CAPTCHA_TOKEN] = user_input[CONF_CAPTCHA_TOKEN].strip()
return await self.async_step_user(self.data)

return self.async_show_form(
step_id="captcha",
data_schema=CAPTCHA_SCHEMA,
description_placeholders={
"captcha_url": CONF_CAPTCHA_URL.format(region=self.data[CONF_REGION])
},
)

@staticmethod
@callback
def async_get_options_flow(
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/bmw_connected_drive/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@
ATTR_VIN = "vin"

CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"]
CONF_CAPTCHA_REGIONS = ["north_america", "rest_of_world"]
CONF_READ_ONLY = "read_only"
CONF_ACCOUNT = "account"
CONF_REFRESH_TOKEN = "refresh_token"
CONF_GCID = "gcid"
CONF_CAPTCHA_TOKEN = "captcha_token"
CONF_CAPTCHA_URL = (
"https://bimmer-connected.readthedocs.io/en/stable/captcha/{region}.html"
)

DATA_HASS_CONFIG = "hass_config"

Expand Down
5 changes: 0 additions & 5 deletions homeassistant/components/bmw_connected_drive/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,6 @@ async def _async_update_data(self) -> None:

if self.account.refresh_token != old_refresh_token:
self._update_config_entry_refresh_token(self.account.refresh_token)
_LOGGER.debug(
"bimmer_connected: refresh token %s > %s",
old_refresh_token,
self.account.refresh_token,
)

def _update_config_entry_refresh_token(self, refresh_token: str | None) -> None:
"""Update or delete the refresh_token in the Config Entry."""
Expand Down
10 changes: 10 additions & 0 deletions homeassistant/components/bmw_connected_drive/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
"password": "[%key:common::config_flow::data::password%]",
"region": "ConnectedDrive Region"
}
},
"captcha": {
"title": "Are you a robot?",
"description": "A captcha is required for BMW login. Visit the external website to complete the challenge and submit the form. Copy the resulting token into the field below.\n\n{captcha_url}\n\nNo data will be exposed outside of your Home Assistant instance.",
"data": {
"captcha_token": "Captcha token"
},
"data_description": {
"captcha_token": "One-time token retrieved from the captcha challenge."
}
}
},
"error": {
Expand Down
9 changes: 7 additions & 2 deletions tests/components/bmw_connected_drive/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from homeassistant import config_entries
from homeassistant.components.bmw_connected_drive.const import (
CONF_CAPTCHA_TOKEN,
CONF_GCID,
CONF_READ_ONLY,
CONF_REFRESH_TOKEN,
Expand All @@ -24,8 +25,12 @@
CONF_PASSWORD: "p4ssw0rd",
CONF_REGION: "rest_of_world",
}
FIXTURE_REFRESH_TOKEN = "SOME_REFRESH_TOKEN"
FIXTURE_GCID = "SOME_GCID"
FIXTURE_CAPTCHA_INPUT = {
CONF_CAPTCHA_TOKEN: "captcha_token",
}
FIXTURE_USER_INPUT_W_CAPTCHA = FIXTURE_USER_INPUT | FIXTURE_CAPTCHA_INPUT
FIXTURE_REFRESH_TOKEN = "another_token_string"
FIXTURE_GCID = "DUMMY"

FIXTURE_CONFIG_ENTRY = {
"entry_id": "1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4833,7 +4833,7 @@
}),
]),
'info': dict({
'gcid': 'SOME_GCID',
'gcid': 'DUMMY',
'password': '**REDACTED**',
'refresh_token': '**REDACTED**',
'region': 'rest_of_world',
Expand Down Expand Up @@ -7202,7 +7202,7 @@
}),
]),
'info': dict({
'gcid': 'SOME_GCID',
'gcid': 'DUMMY',
'password': '**REDACTED**',
'refresh_token': '**REDACTED**',
'region': 'rest_of_world',
Expand Down Expand Up @@ -8925,7 +8925,7 @@
}),
]),
'info': dict({
'gcid': 'SOME_GCID',
'gcid': 'DUMMY',
'password': '**REDACTED**',
'refresh_token': '**REDACTED**',
'region': 'rest_of_world',
Expand Down
Loading

0 comments on commit f97d96e

Please sign in to comment.