Repository: home-assistant/core
File: homeassistant/helpers/config_entry_oauth2_flow.py (Python)

Code before:

    async def async_step_creation(
        self, user_input: dict[str, Any] | None = None
    ) -> config_entries.ConfigFlowResult:
        """Create config entry from external data."""
        _LOGGER.debug("Creating config entry from external data")

        try:
            async with asyncio.timeout(OAUTH_TOKEN_TIMEOUT_SEC):
                token = await self.flow_impl.async_resolve_external_data(
                    self.external_data
                )
        except TimeoutError as err:
            _LOGGER.error("Timeout resolving OAuth token: %s", err)
            return self.async_abort(reason="oauth_timeout")
        except (
            OAuth2TokenRequestReauthError,
            OAuth2TokenRequestTransientError,
            OAuth2TokenRequestError,
            ClientError,
        ) as err:
            _LOGGER.error("Error resolving OAuth token: %s", err)
            # Only the unauthorized error is non-recoverable
            if (
                isinstance(err, OAuth2TokenRequestReauthError)
                and err.status == HTTPStatus.UNAUTHORIZED
            ):
                return self.async_abort(reason="oauth_unauthorized")
            return self.async_abort(reason="oauth_failed")

        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"])
        except ValueError as err:
            _LOGGER.warning("Error converting expires_in to int: %s", err)
            return self.async_abort(reason="oauth_error")
        token["expires_at"] = time.time() + token["expires_in"]

        self.logger.info("Successfully authenticated")

        return await self.async_oauth_create_entry(
            {"auth_implementation": self.flow_impl.domain, "token": token}
        )

    async def async_step_authorize_rejected(
        self, data: None = None
    ) -> config_entries.ConfigFlowResult:

Review comment (security):
I think we should consider removing the `HTTPStatus.UNAUTHORIZED` condition since we now classify all 400 errors as unauthorized. The status attribute also comes from the aiohttp parent exception, which we may remove after the deprecation period.

Code after:
    ) -> config_entries.ConfigFlowResult:
        """Create config entry from external data."""
        _LOGGER.debug("Creating config entry from external data")

        try:
            async with asyncio.timeout(OAUTH_TOKEN_TIMEOUT_SEC):
                token = await self.flow_impl.async_resolve_external_data(
                    self.external_data
                )
        except TimeoutError as err:
            _LOGGER.error("Timeout resolving OAuth token: %s", err)
            return self.async_abort(reason="oauth_timeout")
        except (
            OAuth2TokenRequestError,
            ClientError,
        ) as err:
            _LOGGER.error("Error resolving OAuth token: %s", err)
            if isinstance(err, OAuth2TokenRequestReauthError):
                return self.async_abort(reason="oauth_unauthorized")
            return self.async_abort(reason="oauth_failed")

        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"])
        except ValueError as err:
            _LOGGER.warning("Error converting expires_in to int: %s", err)
            return self.async_abort(reason="oauth_error")
        token["expires_at"] = time.time() + token["expires_in"]

        self.logger.info("Successfully authenticated")

        return await self.async_oauth_create_entry(
            {"auth_implementation": self.flow_impl.domain, "token": token}
        )

    async def async_step_authorize_rejected(
        self, data: None = None
    ) -> config_entries.ConfigFlowResult:
        """Step to handle flow rejection."""
        return self.async_abort(
            reason="user_rejected_authorize",
            description_placeholders={"error": self.external_data["error"]},
        )

    async def async_oauth_create_entry(
        self, data: dict
    ) -> config_entries.ConfigFlowResult: