diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index a837290249e..6c1768d9855 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -211,6 +211,7 @@ class NestFlowHandler( async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Complete OAuth setup and finish pubsub or finish.""" + _LOGGER.debug("Finishing post-oauth configuration") assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" self._data.update(data) if self.source == SOURCE_REAUTH: @@ -459,6 +460,7 @@ class NestFlowHandler( async def async_step_finish(self, data: dict[str, Any] | None = None) -> FlowResult: """Create an entry for the SDM flow.""" + _LOGGER.debug("Creating/updating configuration entry") assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" # Update existing config entry when in the reauth flow. if entry := self._async_reauth_entry(): diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 4b135ae6a2f..0a6356d310d 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -41,6 +41,9 @@ MY_AUTH_CALLBACK_PATH = "https://my.home-assistant.io/redirect/oauth" CLOCK_OUT_OF_SYNC_MAX_SEC = 20 +OAUTH_AUTHORIZE_URL_TIMEOUT_SEC = 30 +OAUTH_TOKEN_TIMEOUT_SEC = 30 + class AbstractOAuth2Implementation(ABC): """Base class to abstract OAuth2 authentication.""" @@ -194,6 +197,7 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation): if self.client_secret is not None: data["client_secret"] = self.client_secret + _LOGGER.debug("Sending token request to %s", self.token_url) resp = await session.post(self.token_url, data=data) if resp.status >= 400 and _LOGGER.isEnabledFor(logging.DEBUG): body = await resp.text() @@ -283,9 +287,10 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): return self.async_external_step_done(next_step_id=next_step) try: - async with async_timeout.timeout(10): + async with async_timeout.timeout(OAUTH_AUTHORIZE_URL_TIMEOUT_SEC): url = await self.async_generate_authorize_url() - except asyncio.TimeoutError: + except asyncio.TimeoutError as err: + _LOGGER.error("Timeout generating authorize url: %s", err) return self.async_abort(reason="authorize_url_timeout") except NoURLAvailableError: return self.async_abort( @@ -303,7 +308,17 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Create config entry from external data.""" - token = await self.flow_impl.async_resolve_external_data(self.external_data) + _LOGGER.debug("Creating config entry from external data") + + try: + async with async_timeout.timeout(OAUTH_TOKEN_TIMEOUT_SEC): + token = await self.flow_impl.async_resolve_external_data( + self.external_data + ) + except asyncio.TimeoutError as err: + _LOGGER.error("Timeout resolving OAuth token: %s", err) + return self.async_abort(reason="oauth2_timeout") + # Force int for non-compliant oauth2 providers try: token["expires_in"] = int(token["expires_in"]) @@ -436,7 +451,7 @@ class OAuth2AuthorizeCallbackView(http.HomeAssistantView): await hass.config_entries.flow.async_configure( flow_id=state["flow_id"], user_input=user_input ) - + _LOGGER.debug("Resumed OAuth configuration flow") return web.Response( headers={"content-type": "text/html"}, text="", diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 1c38fb6d064..c00b51ed6cf 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -71,6 +71,7 @@ "no_devices_found": "No devices found on the network", "webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages.", "oauth2_error": "Received invalid token data.", + "oauth2_timeout": "Timeout resolving OAuth token.", "oauth2_missing_configuration": "The component is not configured. Please follow the documentation.", "oauth2_missing_credentials": "The integration requires application credentials.", "oauth2_authorize_url_timeout": "Timeout generating authorize URL.", diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 652ce69e57d..f64525ecdd3 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -134,8 +134,9 @@ async def test_abort_if_authorization_timeout( flow = flow_handler() flow.hass = hass - with patch.object( - local_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError + with patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_timeout.timeout", + side_effect=asyncio.TimeoutError, ): result = await flow.async_step_user() @@ -278,6 +279,62 @@ async def test_abort_if_oauth_rejected( assert result["description_placeholders"] == {"error": "access_denied"} +async def test_abort_on_oauth_timeout_error( + hass, + flow_handler, + local_impl, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, +): + """Check timeout during oauth token exchange.""" + flow_handler.async_register_implementation(hass, local_impl) + config_entry_oauth2_flow.async_register_implementation( + hass, TEST_DOMAIN, MockOAuth2Implementation() + ) + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "pick_implementation" + + # Pick implementation + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"implementation": TEST_DOMAIN} + ) + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["url"] == ( + f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=read+write" + ) + + 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" + + with patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_timeout.timeout", + side_effect=asyncio.TimeoutError, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "oauth2_timeout" + + async def test_step_discovery(hass, flow_handler, local_impl): """Check flow triggers from discovery.""" flow_handler.async_register_implementation(hass, local_impl)