1 """Config flow for Google integration."""
3 from __future__
import annotations
6 from collections.abc
import Mapping
10 from gcal_sync.api
import GoogleCalendarService
11 from gcal_sync.exceptions
import ApiException, ApiForbiddenException
12 import voluptuous
as vol
31 async_create_device_flow,
36 DEFAULT_FEATURE_ACCESS,
42 _LOGGER = logging.getLogger(__name__)
46 config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
48 """Config flow to handle Google Calendars OAuth2 authentication.
50 Historically, the Google Calendar integration instructed users to use
51 Device Auth. Device Auth was considered easier to use since it did not
52 require users to configure a redirect URL. Device Auth is meant for
53 devices with limited input, such as a television.
54 https://developers.google.com/identity/protocols/oauth2/limited-input-device
56 Device Auth is limited to a small set of Google APIs (calendar is allowed)
57 and is considered less secure than Web Auth. It is not generally preferred
58 and may be limited/deprecated in the future similar to App/OOB Auth
59 https://developers.googleblog.com/2022/02/making-oauth-flows-safer.html
61 Web Auth is the preferred method by Home Assistant and Google, and a benefit
62 is that the same credentials may be used across many Google integrations in
63 Home Assistant. Web Auth is now easier for user to setup using my.home-assistant.io
66 The Application Credentials integration does not currently record which type
67 of credential the user entered (and if we ask the user, they may not know or may
68 make a mistake) so we try to determine the credential type automatically. This
69 implementation first attempts Device Auth by talking to the token API in the first
70 step of the device flow, then if that fails it will redirect using Web Auth.
71 There is not another explicit known way to check.
76 _exchange_finished_task: asyncio.Task[bool] |
None =
None
79 """Set up instance."""
88 return logging.getLogger(__name__)
92 """Extra data that needs to be appended to the authorize url."""
94 "scope": DEFAULT_FEATURE_ACCESS.scope,
96 "access_type":
"offline",
101 self, user_input: dict[str, Any] |
None =
None
102 ) -> ConfigFlowResult:
103 """Create an entry for auth."""
113 return self.async_show_progress_done(next_step_id=
"creation")
116 _LOGGER.debug(
"Creating GoogleHybridAuth flow")
117 if not isinstance(self.flow_impl, GoogleHybridAuth):
119 "Unexpected OAuth implementation does not support device auth: %s",
122 return self.async_abort(reason=
"oauth_error")
123 calendar_access = DEFAULT_FEATURE_ACCESS
124 if self.
sourcesource == SOURCE_REAUTH
and (
125 reauth_options := self._get_reauth_entry().options
127 calendar_access = FeatureAccess[reauth_options[CONF_CALENDAR_ACCESS]]
131 self.flow_impl.client_id,
132 self.flow_impl.client_secret,
135 except TimeoutError
as err:
136 _LOGGER.error(
"Timeout initializing device flow: %s",
str(err))
137 return self.async_abort(reason=
"timeout_connect")
138 except InvalidCredential:
139 _LOGGER.debug(
"Falling back to Web Auth and restarting flow")
142 except OAuthError
as err:
143 _LOGGER.error(
"Error initializing device flow: %s",
str(err))
144 return self.async_abort(reason=
"oauth_error")
147 exchange_finished_evt = asyncio.Event()
149 exchange_finished_evt.wait()
152 def _exchange_finished() -> None:
154 DEVICE_AUTH_CREDS: device_flow.creds
156 exchange_finished_evt.set()
158 device_flow.async_set_listener(_exchange_finished)
159 device_flow.async_start_exchange()
161 return self.async_show_progress(
163 description_placeholders={
167 progress_action=
"exchange",
172 self, user_input: dict[str, Any] |
None =
None
173 ) -> ConfigFlowResult:
174 """Handle external yaml configuration."""
176 return self.async_abort(reason=
"code_expired")
180 """Create an entry for the flow, or update existing entry."""
181 data[CONF_CREDENTIAL_TYPE] = (
182 CredentialType.WEB_AUTH
if self.
_web_auth_web_auth
else CredentialType.DEVICE_AUTH
184 if self.
sourcesource == SOURCE_REAUTH:
185 return self.async_update_reload_and_abort(
186 self._get_reauth_entry(), data=data
188 calendar_service = GoogleCalendarService(
194 primary_calendar = await calendar_service.async_get_calendar(
"primary")
195 except ApiForbiddenException
as err:
197 "Error reading primary calendar, make sure Google Calendar API is enabled: %s",
200 return self.async_abort(reason=
"api_disabled")
201 except ApiException
as err:
202 _LOGGER.error(
"Error reading primary calendar: %s", err)
203 return self.async_abort(reason=
"cannot_connect")
204 await self.async_set_unique_id(primary_calendar.id)
206 if found := self.hass.config_entries.async_entry_for_domain_unique_id(
207 self.handler, primary_calendar.id
209 _LOGGER.debug(
"Found existing '%s' entry: %s", primary_calendar.id, found)
211 self._abort_if_unique_id_configured()
212 return self.async_create_entry(
213 title=primary_calendar.id,
216 CONF_CALENDAR_ACCESS: DEFAULT_FEATURE_ACCESS.name,
221 self, entry_data: Mapping[str, Any]
222 ) -> ConfigFlowResult:
223 """Perform reauth upon an API authentication error."""
224 self.
_web_auth_web_auth = entry_data.get(CONF_CREDENTIAL_TYPE) == CredentialType.WEB_AUTH
228 self, user_input: dict[str, Any] |
None =
None
229 ) -> ConfigFlowResult:
230 """Confirm reauth dialog."""
231 if user_input
is None:
232 return self.async_show_form(step_id=
"reauth_confirm")
233 return await self.async_step_user()
238 config_entry: ConfigEntry,
240 """Create an options flow."""
245 """Google Calendar options flow."""
248 self, user_input: dict[str, Any] |
None =
None
249 ) -> ConfigFlowResult:
250 """Manage the options."""
251 if user_input
is not None:
256 data_schema=vol.Schema(
259 CONF_CALENDAR_ACCESS,
263 "read_write":
"Read/Write access (can create events)",
264 "read_only":
"Read-only access",
dict[str, Any] extra_authorize_data(self)
ConfigFlowResult async_step_creation(self, dict[str, Any]|None user_input=None)
OptionsFlow async_get_options_flow(ConfigEntry config_entry)
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_auth(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_oauth_create_entry(self, dict data)
logging.Logger logger(self)
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
ConfigEntry config_entry(self)
None config_entry(self, ConfigEntry value)
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
web.Response get(self, web.Request request, str config_key)
DeviceFlow async_create_device_flow(HomeAssistant hass, str client_id, str client_secret, FeatureAccess access)
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)