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

Code before:
            .update_query(self.extra_authorize_data)
        )

    async def async_resolve_external_data(self, external_data: Any) -> dict:
        """Resolve the authorization code to tokens."""
        request_data: dict = {
            "grant_type": "authorization_code",
            "code": external_data["code"],
            "redirect_uri": external_data["state"]["redirect_uri"],
        }
        request_data.update(self.extra_token_resolve_data)
        return await self._token_request(request_data)

    async def _async_refresh_token(self, token: dict) -> dict:
        """Refresh tokens."""
        try:
            new_token = await self._token_request(
                {
                    "grant_type": "refresh_token",
                    "client_id": self.client_id,
                    "refresh_token": token["refresh_token"],
                }
            )
        except ClientResponseError as err:
            # I have doubts if this is right exception to raise here. Let's find out in a review. :)
            raise ConfigEntryAuthFailed("Refresh token failed") from err
        return {**token, **new_token}

    async def _token_request(self, data: dict) -> dict:
        """Make a token request.

        Retries only HTTP 429 responses (respecting exponential backoff).
        Any other HTTP status >= 400 is not treated as they were.
        """
        session = async_get_clientsession(self.hass)

        data["client_id"] = self.client_id
        if self.client_secret:
            data["client_secret"] = self.client_secret

        _LOGGER.debug("Sending token request to %s", self.token_url)

        retry_opts = ExponentialRetry(
            attempts=5,
            factor=1.0,
            max_timeout=30,
            statuses={429},
            exceptions={ClientError, asyncio.TimeoutError},
            retry_all_server_errors=False,
        )


Review comment (bug):
I suggest we raise a new exception when token refresh fails, that integration can catch and handle appropriately in each case. We don't know that we can raise a `ConfigEntryAuthFailed`.

This will be a breaking change for integrations that we need to handle in a non breaking way during a deprecation period. A dev blog needs to be released etc.

Code after:
        return str(
            URL(self.authorize_url)
            .with_query(
                {
                    "response_type": "code",
                    "client_id": self.client_id,
                    "redirect_uri": redirect_uri,
                    "state": _encode_jwt(
                        self.hass, {"flow_id": flow_id, "redirect_uri": redirect_uri}
                    ),
                }
            )
            .update_query(self.extra_authorize_data)
        )

    async def async_resolve_external_data(self, external_data: Any) -> dict:
        """Resolve the authorization code to tokens."""
        request_data: dict = {
            "grant_type": "authorization_code",
            "code": external_data["code"],
            "redirect_uri": external_data["state"]["redirect_uri"],
        }
        request_data.update(self.extra_token_resolve_data)
        return await self._token_request(request_data)

    async def _async_refresh_token(self, token: dict) -> dict:
        """Refresh a token."""

        new_token = await self._token_request(
            {
                "grant_type": "refresh_token",
                "client_id": self.client_id,
                "refresh_token": token["refresh_token"],
            }
        )

        return {**token, **new_token}

    async def _token_request(self, data: dict) -> dict:
        """Make a token request.

        Raises OAuth2TokenRequestError on token request failure.
        """
        session = async_get_clientsession(self.hass)

        data["client_id"] = self.client_id
        if self.client_secret:
            data["client_secret"] = self.client_secret

        _LOGGER.debug("Sending token request to %s", self.token_url)