diff --git a/docs/config_entries_config_flow_handler.md b/docs/config_entries_config_flow_handler.md index 42fb53ef..d9cb68ec 100644 --- a/docs/config_entries_config_flow_handler.md +++ b/docs/config_entries_config_flow_handler.md @@ -185,3 +185,95 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry): return True ``` + +## Reauthentication + +Gracefully handling authentication errors such as invalid, expired, or revoked tokens is needed to advance on the [Integration Qualily Scale](integration_quality_scale_index.md). This example of how to add reauth to the OAuth flow created by `script.scaffold` following the pattern in [Building a Python library](api_lib_auth.md#oauth2). + +This example catches an authentication exception in config entry setup in `__init__.py` and instructs the user to visit the integrations page in order to reconfigure the integration. + +```python + +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.core import HomeAssistant +from . import api + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Setup up a config entry.""" + + # TODO: Replace with actual API setup and exception + auth = api.AsyncConfigEntryAuth(...) + try: + await auth.refresh_tokens() + except TokenExpiredError: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data=entry.data, + ) + ) + return False + + # TODO: Proceed with component setup +``` + +The flow handler in `config_flow.py` also needs to have some additional steps to support reauth which include showing a confirmation, starting the reauth flow, updating the existing config entry, and reloading to invoke setup again. + +```python + +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle OAuth2 authentication.""" + + async def async_step_reauth(self, user_input=None): + """Perform reauth upon an API authentication error.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Dialog that informs the user that reauth is required.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({}), + ) + return await self.async_step_user() + + async def async_oauth_create_entry(self, data: dict) -> dict: + """Create an oauth config entry or update existing entry for reauth.""" + # TODO: This example supports only a single config entry. Consider + # any special handling needed for multiple config entries. + existing_entry = await self.async_set_unique_id(DOMAIN) + if existing_entry: + self.hass.config_entries.async_update_entry(existing_entry, data=data) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + return await super().async_oauth_create_entry(data) +``` + +Depending on the details of the integration, there may be additional considerations such as ensuring the same account is used across reauth, or handling multiple config entries. + +The reauth confirmation dialog needs additional definitions in `strings.json` for the reauth confirmation and success diaglogs: + +```json +{ + "config": { + "step": { + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + # TODO: Replace with the name of the integration + "description": "The Example integration needs to re-authenticate your account" + } + }, + "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, +} +``` + +See [Translations](#translations) local development instructions. + +Authentication failures (such as a revoked oauth token) can be a little tricky to manually test. One suggestion is to make a copy of `config/.storage/core.config_entries` and manually change the values of `access_token`, `refresh_token`, and `expires_at` depending on the scenario you want to test. You can then walk advance through the reauth flow and confirm that the values get replaced with new valid tokens. + +Automated tests should verify that the reauth flow updates the existing config entry and does not create additional entries. diff --git a/docs/integration_quality_scale_index.md b/docs/integration_quality_scale_index.md index 8421fe24..4d38cac6 100644 --- a/docs/integration_quality_scale_index.md +++ b/docs/integration_quality_scale_index.md @@ -22,7 +22,7 @@ This integration is able to cope when things go wrong. It will not print any exc - Connection/configuration is handled via a component. - Set an appropriate `SCAN_INTERVAL` (if a polling integration) - Raise `PlatformNotReady` if unable to connect during platform setup (if appropriate) -- Handles expiration of auth credentials. Refresh if possible or print correct error and fail setup. If based on a config entry, should trigger a new config entry flow to re-authorize. +- Handles expiration of auth credentials. Refresh if possible or print correct error and fail setup. If based on a config entry, should trigger a new config entry flow to re-authorize. ([docs](config_entries_config_flow_handler.md#reauthentication)) - Handles internet unavailable. Log a warning once when unavailable, log once when reconnected. - Handles device/service unavailable. Log a warning once when unavailable, log once when reconnected. - Set `available` property to `False` if appropriate ([docs](core/entity.md#generic-properties))