From 549f779b0679b004f67aca996b107414355a6e36 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 18 Jun 2021 16:11:35 -0600 Subject: [PATCH] Force SimpliSafe to reauthenticate with a password (#51528) --- .../components/simplisafe/__init__.py | 79 +++++++------------ .../components/simplisafe/config_flow.py | 13 +-- .../components/simplisafe/strings.json | 2 +- .../simplisafe/translations/en.json | 2 +- .../components/simplisafe/test_config_flow.py | 20 +++-- 5 files changed, 54 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index c49aeb065e4..b7c2a08f093 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -6,7 +6,7 @@ from simplipy import API from simplipy.errors import EndpointUnavailable, InvalidCredentialsError, SimplipyError import voluptuous as vol -from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import CoreState, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( @@ -107,14 +107,6 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( CONFIG_SCHEMA = cv.deprecated(DOMAIN) -@callback -def _async_save_refresh_token(hass, config_entry, token): - """Save a refresh token to the config entry.""" - hass.config_entries.async_update_entry( - config_entry, data={**config_entry.data, CONF_TOKEN: token} - ) - - async def async_get_client_id(hass): """Get a client ID (based on the HASS unique ID) for the SimpliSafe API. @@ -136,16 +128,15 @@ async def async_register_base_station(hass, system, config_entry_id): ) -async def async_setup(hass, config): - """Set up the SimpliSafe component.""" - hass.data[DOMAIN] = {DATA_CLIENT: {}, DATA_LISTENER: {}} - return True - - async def async_setup_entry(hass, config_entry): # noqa: C901 """Set up SimpliSafe as config entry.""" + hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}, DATA_LISTENER: {}}) + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = [] hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = [] + if CONF_PASSWORD not in config_entry.data: + raise ConfigEntryAuthFailed("Config schema change requires re-authentication") + entry_updates = {} if not config_entry.unique_id: # If the config entry doesn't already have a unique ID, set one: @@ -167,20 +158,24 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 client_id = await async_get_client_id(hass) websession = aiohttp_client.async_get_clientsession(hass) - try: - api = await API.login_via_token( - config_entry.data[CONF_TOKEN], client_id=client_id, session=websession + async def async_get_api(): + """Define a helper to get an authenticated SimpliSafe API object.""" + return await API.login_via_credentials( + config_entry.data[CONF_USERNAME], + config_entry.data[CONF_PASSWORD], + client_id=client_id, + session=websession, ) - except InvalidCredentialsError: - LOGGER.error("Invalid credentials provided") - return False + + try: + api = await async_get_api() + except InvalidCredentialsError as err: + raise ConfigEntryAuthFailed from err except SimplipyError as err: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - _async_save_refresh_token(hass, config_entry, api.refresh_token) - - simplisafe = SimpliSafe(hass, api, config_entry) + simplisafe = SimpliSafe(hass, config_entry, api, async_get_api) try: await simplisafe.async_init() @@ -307,10 +302,10 @@ async def async_reload_entry(hass, config_entry): class SimpliSafe: """Define a SimpliSafe data object.""" - def __init__(self, hass, api, config_entry): + def __init__(self, hass, config_entry, api, async_get_api): """Initialize.""" self._api = api - self._emergency_refresh_token_used = False + self._async_get_api = async_get_api self._hass = hass self._system_notifications = {} self.config_entry = config_entry @@ -387,23 +382,17 @@ class SimpliSafe: for result in results: if isinstance(result, InvalidCredentialsError): - if self._emergency_refresh_token_used: - raise ConfigEntryAuthFailed( - "Update failed with stored refresh token" - ) - - LOGGER.warning("SimpliSafe cloud error; trying stored refresh token") - self._emergency_refresh_token_used = True - try: - await self._api.refresh_access_token( - self.config_entry.data[CONF_TOKEN] - ) + self._api = await self._async_get_api() return + except InvalidCredentialsError as err: + raise ConfigEntryAuthFailed( + "Unable to re-authenticate with SimpliSafe" + ) from err except SimplipyError as err: - raise UpdateFailed( # pylint: disable=raise-missing-from - f"Error while using stored refresh token: {err}" - ) + raise UpdateFailed( + f"SimpliSafe error while updating: {err}" + ) from err if isinstance(result, EndpointUnavailable): # In case the user attempts an action not allowed in their current plan, @@ -414,16 +403,6 @@ class SimpliSafe: if isinstance(result, SimplipyError): raise UpdateFailed(f"SimpliSafe error while updating: {result}") - if self._api.refresh_token != self.config_entry.data[CONF_TOKEN]: - _async_save_refresh_token( - self._hass, self.config_entry, self._api.refresh_token - ) - - # If we've reached this point using an emergency refresh token, we're in the - # clear and we can discard it: - if self._emergency_refresh_token_used: - self._emergency_refresh_token_used = False - class SimpliSafeEntity(CoordinatorEntity): """Define a base SimpliSafe entity.""" diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index ba51356f770..0faa07221aa 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -8,7 +8,7 @@ from simplipy.errors import ( import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -59,7 +59,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} try: - simplisafe = await self._async_get_simplisafe_api() + await self._async_get_simplisafe_api() except PendingAuthorizationError: LOGGER.info("Awaiting confirmation of MFA email click") return await self.async_step_mfa() @@ -79,7 +79,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish( { CONF_USERNAME: self._username, - CONF_TOKEN: simplisafe.refresh_token, + CONF_PASSWORD: self._password, CONF_CODE: self._code, } ) @@ -89,6 +89,9 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): existing_entry = await self.async_set_unique_id(self._username) if existing_entry: self.hass.config_entries.async_update_entry(existing_entry, data=user_input) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) return self.async_abort(reason="reauth_successful") return self.async_create_entry(title=self._username, data=user_input) @@ -98,7 +101,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="mfa") try: - simplisafe = await self._async_get_simplisafe_api() + await self._async_get_simplisafe_api() except PendingAuthorizationError: LOGGER.error("Still awaiting confirmation of MFA email click") return self.async_show_form( @@ -108,7 +111,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish( { CONF_USERNAME: self._username, - CONF_TOKEN: simplisafe.refresh_token, + CONF_PASSWORD: self._password, CONF_CODE: self._code, } ) diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index ad973261a0e..23f85495025 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -7,7 +7,7 @@ }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "Your access token has expired or been revoked. Enter your password to re-link your account.", + "description": "Your access has expired or been revoked. Enter your password to re-link your account.", "data": { "password": "[%key:common::config_flow::data::password%]" } diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index b9e274666bb..331eb65ca83 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -19,7 +19,7 @@ "data": { "password": "Password" }, - "description": "Your access token has expired or been revoked. Enter your password to re-link your account.", + "description": "Your access has expired or been revoked. Enter your password to re-link your account.", "title": "Reauthenticate Integration" }, "user": { diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index a048e4b0745..c2397e9f89e 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -10,7 +10,7 @@ from simplipy.errors import ( from homeassistant import data_entry_flow from homeassistant.components.simplisafe import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry @@ -33,7 +33,11 @@ async def test_duplicate_error(hass): MockConfigEntry( domain=DOMAIN, unique_id="user@email.com", - data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, + data={ + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_CODE: "1234", + }, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -102,7 +106,11 @@ async def test_step_reauth(hass): MockConfigEntry( domain=DOMAIN, unique_id="user@email.com", - data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, + data={ + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_CODE: "1234", + }, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -120,6 +128,8 @@ async def test_step_reauth(hass): "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch( "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) + ), patch( + "homeassistant.config_entries.ConfigEntries.async_reload" ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} @@ -151,7 +161,7 @@ async def test_step_user(hass): assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", - CONF_TOKEN: "12345abc", + CONF_PASSWORD: "password", CONF_CODE: "1234", } @@ -197,7 +207,7 @@ async def test_step_user_mfa(hass): assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", - CONF_TOKEN: "12345abc", + CONF_PASSWORD: "password", CONF_CODE: "1234", }