Skip to content

Commit f97d96e

Browse files
rikroefrenck
andcommitted
Add captcha to BMW ConfigFlow (#131351)
Co-authored-by: Franck Nijhof <[email protected]>
1 parent ee96093 commit f97d96e

File tree

7 files changed

+153
-74
lines changed

7 files changed

+153
-74
lines changed

homeassistant/components/bmw_connected_drive/config_flow.py

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,18 @@
2727
from homeassistant.core import HomeAssistant, callback
2828
from homeassistant.exceptions import HomeAssistantError
2929
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
30+
from homeassistant.util.ssl import get_default_context
3031

3132
from . import DOMAIN
32-
from .const import CONF_ALLOWED_REGIONS, CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN
33+
from .const import (
34+
CONF_ALLOWED_REGIONS,
35+
CONF_CAPTCHA_REGIONS,
36+
CONF_CAPTCHA_TOKEN,
37+
CONF_CAPTCHA_URL,
38+
CONF_GCID,
39+
CONF_READ_ONLY,
40+
CONF_REFRESH_TOKEN,
41+
)
3342

3443
DATA_SCHEMA = vol.Schema(
3544
{
@@ -41,7 +50,14 @@
4150
translation_key="regions",
4251
)
4352
),
44-
}
53+
},
54+
extra=vol.REMOVE_EXTRA,
55+
)
56+
CAPTCHA_SCHEMA = vol.Schema(
57+
{
58+
vol.Required(CONF_CAPTCHA_TOKEN): str,
59+
},
60+
extra=vol.REMOVE_EXTRA,
4561
)
4662

4763

@@ -54,6 +70,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
5470
data[CONF_USERNAME],
5571
data[CONF_PASSWORD],
5672
get_region_from_name(data[CONF_REGION]),
73+
hcaptcha_token=data.get(CONF_CAPTCHA_TOKEN),
74+
verify=get_default_context(),
5775
)
5876

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

8098
VERSION = 1
8199

100+
data: dict[str, Any] = {}
101+
82102
_existing_entry_data: Mapping[str, Any] | None = None
83103

84104
async def async_step_user(
85105
self, user_input: dict[str, Any] | None = None
86106
) -> ConfigFlowResult:
87107
"""Handle the initial step."""
88-
errors: dict[str, str] = {}
108+
errors: dict[str, str] = self.data.pop("errors", {})
89109

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

@@ -96,22 +116,35 @@ async def async_step_user(
96116
else:
97117
self._abort_if_unique_id_configured()
98118

119+
# Store user input for later use
120+
self.data.update(user_input)
121+
122+
# North America and Rest of World require captcha token
123+
if (
124+
self.data.get(CONF_REGION) in CONF_CAPTCHA_REGIONS
125+
and CONF_CAPTCHA_TOKEN not in self.data
126+
):
127+
return await self.async_step_captcha()
128+
99129
info = None
100130
try:
101-
info = await validate_input(self.hass, user_input)
102-
entry_data = {
103-
**user_input,
104-
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
105-
CONF_GCID: info.get(CONF_GCID),
106-
}
131+
info = await validate_input(self.hass, self.data)
107132
except MissingCaptcha:
108133
errors["base"] = "missing_captcha"
109134
except CannotConnect:
110135
errors["base"] = "cannot_connect"
111136
except InvalidAuth:
112137
errors["base"] = "invalid_auth"
138+
finally:
139+
self.data.pop(CONF_CAPTCHA_TOKEN, None)
113140

114141
if info:
142+
entry_data = {
143+
**self.data,
144+
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
145+
CONF_GCID: info.get(CONF_GCID),
146+
}
147+
115148
if self.source == SOURCE_REAUTH:
116149
return self.async_update_reload_and_abort(
117150
self._get_reauth_entry(), data=entry_data
@@ -128,7 +161,7 @@ async def async_step_user(
128161

129162
schema = self.add_suggested_values_to_schema(
130163
DATA_SCHEMA,
131-
self._existing_entry_data,
164+
self._existing_entry_data or self.data,
132165
)
133166

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

183+
async def async_step_captcha(
184+
self, user_input: dict[str, Any] | None = None
185+
) -> ConfigFlowResult:
186+
"""Show captcha form."""
187+
if user_input and user_input.get(CONF_CAPTCHA_TOKEN):
188+
self.data[CONF_CAPTCHA_TOKEN] = user_input[CONF_CAPTCHA_TOKEN].strip()
189+
return await self.async_step_user(self.data)
190+
191+
return self.async_show_form(
192+
step_id="captcha",
193+
data_schema=CAPTCHA_SCHEMA,
194+
description_placeholders={
195+
"captcha_url": CONF_CAPTCHA_URL.format(region=self.data[CONF_REGION])
196+
},
197+
)
198+
150199
@staticmethod
151200
@callback
152201
def async_get_options_flow(

homeassistant/components/bmw_connected_drive/const.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@
88
ATTR_VIN = "vin"
99

1010
CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"]
11+
CONF_CAPTCHA_REGIONS = ["north_america", "rest_of_world"]
1112
CONF_READ_ONLY = "read_only"
1213
CONF_ACCOUNT = "account"
1314
CONF_REFRESH_TOKEN = "refresh_token"
1415
CONF_GCID = "gcid"
16+
CONF_CAPTCHA_TOKEN = "captcha_token"
17+
CONF_CAPTCHA_URL = (
18+
"https://bimmer-connected.readthedocs.io/en/stable/captcha/{region}.html"
19+
)
1520

1621
DATA_HASS_CONFIG = "hass_config"
1722

homeassistant/components/bmw_connected_drive/coordinator.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,6 @@ async def _async_update_data(self) -> None:
8484

8585
if self.account.refresh_token != old_refresh_token:
8686
self._update_config_entry_refresh_token(self.account.refresh_token)
87-
_LOGGER.debug(
88-
"bimmer_connected: refresh token %s > %s",
89-
old_refresh_token,
90-
self.account.refresh_token,
91-
)
9287

9388
def _update_config_entry_refresh_token(self, refresh_token: str | None) -> None:
9489
"""Update or delete the refresh_token in the Config Entry."""

homeassistant/components/bmw_connected_drive/strings.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77
"password": "[%key:common::config_flow::data::password%]",
88
"region": "ConnectedDrive Region"
99
}
10+
},
11+
"captcha": {
12+
"title": "Are you a robot?",
13+
"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.",
14+
"data": {
15+
"captcha_token": "Captcha token"
16+
},
17+
"data_description": {
18+
"captcha_token": "One-time token retrieved from the captcha challenge."
19+
}
1020
}
1121
},
1222
"error": {

tests/components/bmw_connected_drive/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from homeassistant import config_entries
1111
from homeassistant.components.bmw_connected_drive.const import (
12+
CONF_CAPTCHA_TOKEN,
1213
CONF_GCID,
1314
CONF_READ_ONLY,
1415
CONF_REFRESH_TOKEN,
@@ -24,8 +25,12 @@
2425
CONF_PASSWORD: "p4ssw0rd",
2526
CONF_REGION: "rest_of_world",
2627
}
27-
FIXTURE_REFRESH_TOKEN = "SOME_REFRESH_TOKEN"
28-
FIXTURE_GCID = "SOME_GCID"
28+
FIXTURE_CAPTCHA_INPUT = {
29+
CONF_CAPTCHA_TOKEN: "captcha_token",
30+
}
31+
FIXTURE_USER_INPUT_W_CAPTCHA = FIXTURE_USER_INPUT | FIXTURE_CAPTCHA_INPUT
32+
FIXTURE_REFRESH_TOKEN = "another_token_string"
33+
FIXTURE_GCID = "DUMMY"
2934

3035
FIXTURE_CONFIG_ENTRY = {
3136
"entry_id": "1",

tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4833,7 +4833,7 @@
48334833
}),
48344834
]),
48354835
'info': dict({
4836-
'gcid': 'SOME_GCID',
4836+
'gcid': 'DUMMY',
48374837
'password': '**REDACTED**',
48384838
'refresh_token': '**REDACTED**',
48394839
'region': 'rest_of_world',
@@ -7202,7 +7202,7 @@
72027202
}),
72037203
]),
72047204
'info': dict({
7205-
'gcid': 'SOME_GCID',
7205+
'gcid': 'DUMMY',
72067206
'password': '**REDACTED**',
72077207
'refresh_token': '**REDACTED**',
72087208
'region': 'rest_of_world',
@@ -8925,7 +8925,7 @@
89258925
}),
89268926
]),
89278927
'info': dict({
8928-
'gcid': 'SOME_GCID',
8928+
'gcid': 'DUMMY',
89298929
'password': '**REDACTED**',
89308930
'refresh_token': '**REDACTED**',
89318931
'region': 'rest_of_world',

0 commit comments

Comments
 (0)