diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index df948a2b593..a9ba69ad045 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -16,7 +16,8 @@ "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "already_configured": "Configuration updated for profile.", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]" }, "create_entry": { "default": "Successfully authenticated with Withings." diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 6538c7fe891..1a106364566 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -319,6 +319,10 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): _LOGGER.error("Timeout resolving OAuth token: %s", err) return self.async_abort(reason="oauth2_timeout") + if "expires_in" not in token: + _LOGGER.warning("Invalid token: %s", token) + return self.async_abort(reason="oauth_error") + # Force int for non-compliant oauth2 providers try: token["expires_in"] = int(token["expires_in"]) diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 36edffcc346..f8f8f62becf 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -253,3 +253,54 @@ async def test_config_reauth_wrong_account( assert result assert result["type"] == FlowResultType.ABORT assert result["reason"] == "wrong_account" + + +async def test_config_flow_with_invalid_credentials( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + polling_config_entry: MockConfigEntry, + withings: AsyncMock, + current_request_with_host, +) -> None: + """Test flow with invalid credentials.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["url"] == ( + "https://account.withings.com/oauth2_user/authorize2?" + f"response_type=code&client_id={CLIENT_ID}&" + "redirect_uri=https://example.com/auth/external/callback&" + f"state={state}" + "&scope=user.info,user.metrics,user.activity,user.sleepevents" + ) + + 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( + "https://wbsapi.withings.net/v2/oauth2", + json={ + "body": { + "status": 503, + "error": "Invalid Params: invalid client id/secret", + }, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "oauth_error" diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 94cdf34cba3..c36b62f66c0 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -167,6 +167,7 @@ async def test_abort_if_no_url_available( assert result["reason"] == "no_url_available" +@pytest.mark.parametrize("expires_in_dict", [{}, {"expires_in": "badnumber"}]) async def test_abort_if_oauth_error( hass: HomeAssistant, flow_handler, @@ -174,6 +175,7 @@ async def test_abort_if_oauth_error( hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, current_request_with_host: None, + expires_in_dict: dict[str, str], ) -> None: """Check bad oauth token.""" flow_handler.async_register_implementation(hass, local_impl) @@ -219,8 +221,8 @@ async def test_abort_if_oauth_error( "refresh_token": REFRESH_TOKEN, "access_token": ACCESS_TOKEN_1, "type": "bearer", - "expires_in": "badnumber", - }, + } + | expires_in_dict, ) result = await hass.config_entries.flow.async_configure(result["flow_id"])