Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit e3c838d

Browse files
authored
Update Notion auth to store refresh tokens instead of account passwords (home-assistant#109670)
1 parent 92c3c40 commit e3c838d

File tree

12 files changed

+142
-47
lines changed

12 files changed

+142
-47
lines changed

‎.coveragerc‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,7 @@ omit =
873873
homeassistant/components/notion/__init__.py
874874
homeassistant/components/notion/binary_sensor.py
875875
homeassistant/components/notion/sensor.py
876+
homeassistant/components/notion/util.py
876877
homeassistant/components/nsw_fuel_station/sensor.py
877878
homeassistant/components/nuki/__init__.py
878879
homeassistant/components/nuki/binary_sensor.py

‎homeassistant/components/notion/__init__.py‎

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from typing import Any
88
from uuid import UUID
99

10-
from aionotion import async_get_client
1110
from aionotion.bridge.models import Bridge
1211
from aionotion.errors import InvalidCredentialsError, NotionError
1312
from aionotion.listener.models import Listener, ListenerKind
@@ -19,7 +18,6 @@
1918
from homeassistant.core import HomeAssistant, callback
2019
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
2120
from homeassistant.helpers import (
22-
aiohttp_client,
2321
config_validation as cv,
2422
device_registry as dr,
2523
entity_registry as er,
@@ -33,6 +31,8 @@
3331
)
3432

3533
from .const import (
34+
CONF_REFRESH_TOKEN,
35+
CONF_USER_UUID,
3636
DOMAIN,
3737
LOGGER,
3838
SENSOR_BATTERY,
@@ -46,6 +46,7 @@
4646
SENSOR_TEMPERATURE,
4747
SENSOR_WINDOW_HINGED,
4848
)
49+
from .util import async_get_client_with_credentials, async_get_client_with_refresh_token
4950

5051
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
5152

@@ -139,25 +140,48 @@ def asdict(self) -> dict[str, Any]:
139140

140141
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
141142
"""Set up Notion as a config entry."""
142-
if not entry.unique_id:
143-
hass.config_entries.async_update_entry(
144-
entry, unique_id=entry.data[CONF_USERNAME]
145-
)
143+
entry_updates: dict[str, Any] = {"data": {**entry.data}}
146144

147-
session = aiohttp_client.async_get_clientsession(hass)
145+
if not entry.unique_id:
146+
entry_updates["unique_id"] = entry.data[CONF_USERNAME]
148147

149148
try:
150-
client = await async_get_client(
151-
entry.data[CONF_USERNAME],
152-
entry.data[CONF_PASSWORD],
153-
session=session,
154-
use_legacy_auth=True,
155-
)
149+
if password := entry_updates["data"].pop(CONF_PASSWORD, None):
150+
# If a password exists in the config entry data, use it to get a new client
151+
# (and pop it from the new entry data):
152+
client = await async_get_client_with_credentials(
153+
hass, entry.data[CONF_USERNAME], password
154+
)
155+
else:
156+
# If a password doesn't exist in the config entry data, we can safely assume
157+
# that a refresh token and user UUID do, so we use them to get the client:
158+
client = await async_get_client_with_refresh_token(
159+
hass,
160+
entry.data[CONF_USER_UUID],
161+
entry.data[CONF_REFRESH_TOKEN],
162+
)
156163
except InvalidCredentialsError as err:
157-
raise ConfigEntryAuthFailed("Invalid username and/or password") from err
164+
raise ConfigEntryAuthFailed("Invalid credentials") from err
158165
except NotionError as err:
159166
raise ConfigEntryNotReady("Config entry failed to load") from err
160167

168+
# Always update the config entry with the latest refresh token and user UUID:
169+
entry_updates["data"][CONF_REFRESH_TOKEN] = client.refresh_token
170+
entry_updates["data"][CONF_USER_UUID] = client.user_uuid
171+
172+
@callback
173+
def async_save_refresh_token(refresh_token: str) -> None:
174+
"""Save a refresh token to the config entry data."""
175+
LOGGER.debug("Saving new refresh token to HASS storage")
176+
hass.config_entries.async_update_entry(
177+
entry, data={**entry.data, CONF_REFRESH_TOKEN: refresh_token}
178+
)
179+
180+
# Create a callback to save the refresh token when it changes:
181+
entry.async_on_unload(client.add_refresh_token_callback(async_save_refresh_token))
182+
183+
hass.config_entries.async_update_entry(entry, **entry_updates)
184+
161185
async def async_update() -> NotionData:
162186
"""Get the latest data from the Notion API."""
163187
data = NotionData(hass=hass, entry=entry)

‎homeassistant/components/notion/config_flow.py‎

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
from __future__ import annotations
33

44
from collections.abc import Mapping
5+
from dataclasses import dataclass, field
56
from typing import Any
67

7-
from aionotion import async_get_client
88
from aionotion.errors import InvalidCredentialsError, NotionError
99
import voluptuous as vol
1010

@@ -13,9 +13,9 @@
1313
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
1414
from homeassistant.core import HomeAssistant
1515
from homeassistant.data_entry_flow import FlowResult
16-
from homeassistant.helpers import aiohttp_client
1716

18-
from .const import DOMAIN, LOGGER
17+
from .const import CONF_REFRESH_TOKEN, CONF_USER_UUID, DOMAIN, LOGGER
18+
from .util import async_get_client_with_credentials
1919

2020
AUTH_SCHEMA = vol.Schema(
2121
{
@@ -30,17 +30,23 @@
3030
)
3131

3232

33+
@dataclass(frozen=True, kw_only=True)
34+
class CredentialsValidationResult:
35+
"""Define a validation result."""
36+
37+
user_uuid: str | None = None
38+
refresh_token: str | None = None
39+
errors: dict[str, Any] = field(default_factory=dict)
40+
41+
3342
async def async_validate_credentials(
3443
hass: HomeAssistant, username: str, password: str
35-
) -> dict[str, Any]:
36-
"""Validate a Notion username and password (returning any errors)."""
37-
session = aiohttp_client.async_get_clientsession(hass)
44+
) -> CredentialsValidationResult:
45+
"""Validate a Notion username and password."""
3846
errors = {}
3947

4048
try:
41-
await async_get_client(
42-
username, password, session=session, use_legacy_auth=True
43-
)
49+
client = await async_get_client_with_credentials(hass, username, password)
4450
except InvalidCredentialsError:
4551
errors["base"] = "invalid_auth"
4652
except NotionError as err:
@@ -50,7 +56,12 @@ async def async_validate_credentials(
5056
LOGGER.exception("Unknown error while validation credentials: %s", err)
5157
errors["base"] = "unknown"
5258

53-
return errors
59+
if errors:
60+
return CredentialsValidationResult(errors=errors)
61+
62+
return CredentialsValidationResult(
63+
user_uuid=client.user_uuid, refresh_token=client.refresh_token
64+
)
5465

5566

5667
class NotionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@@ -84,20 +95,24 @@ async def async_step_reauth_confirm(
8495
},
8596
)
8697

87-
iferrors:= await async_validate_credentials(
98+
credentials_validation_result= await async_validate_credentials(
8899
self.hass, self._reauth_entry.data[CONF_USERNAME], user_input[CONF_PASSWORD]
89-
):
100+
)
101+
102+
if credentials_validation_result.errors:
90103
return self.async_show_form(
91104
step_id="reauth_confirm",
92105
data_schema=REAUTH_SCHEMA,
93-
errors=errors,
106+
errors=credentials_validation_result.errors,
94107
description_placeholders={
95108
CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME]
96109
},
97110
)
98111

99112
self.hass.config_entries.async_update_entry(
100-
self._reauth_entry, data=self._reauth_entry.data | user_input
113+
self._reauth_entry,
114+
data=self._reauth_entry.data
115+
| {CONF_REFRESH_TOKEN: credentials_validation_result.refresh_token},
101116
)
102117
self.hass.async_create_task(
103118
self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
@@ -114,13 +129,22 @@ async def async_step_user(
114129
await self.async_set_unique_id(user_input[CONF_USERNAME])
115130
self._abort_if_unique_id_configured()
116131

117-
iferrors:= await async_validate_credentials(
132+
credentials_validation_result= await async_validate_credentials(
118133
self.hass, user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
119-
):
134+
)
135+
136+
if credentials_validation_result.errors:
120137
return self.async_show_form(
121138
step_id="user",
122139
data_schema=AUTH_SCHEMA,
123-
errors=errors,
140+
errors=credentials_validation_result.errors,
124141
)
125142

126-
return self.async_create_entry(title=user_input[CONF_USERNAME], data=user_input)
143+
return self.async_create_entry(
144+
title=user_input[CONF_USERNAME],
145+
data={
146+
CONF_USERNAME: user_input[CONF_USERNAME],
147+
CONF_USER_UUID: credentials_validation_result.user_uuid,
148+
CONF_REFRESH_TOKEN: credentials_validation_result.refresh_token,
149+
},
150+
)

‎homeassistant/components/notion/const.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
DOMAIN = "notion"
55
LOGGER = logging.getLogger(__package__)
66

7+
CONF_REFRESH_TOKEN = "refresh_token"
8+
CONF_USER_UUID = "user_uuid"
9+
710
SENSOR_BATTERY = "low_battery"
811
SENSOR_DOOR = "door"
912
SENSOR_GARAGE_DOOR = "garage_door"

‎homeassistant/components/notion/diagnostics.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55

66
from homeassistant.components.diagnostics import async_redact_data
77
from homeassistant.config_entries import ConfigEntry
8-
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME
8+
from homeassistant.const import CONF_EMAIL, CONF_UNIQUE_ID, CONF_USERNAME
99
from homeassistant.core import HomeAssistant
1010
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
1111

1212
from . import NotionData
13-
from .const import DOMAIN
13+
from .const import CONF_REFRESH_TOKEN, CONF_USER_UUID, DOMAIN
1414

1515
CONF_DEVICE_KEY = "device_key"
1616
CONF_HARDWARE_ID = "hardware_id"
@@ -23,12 +23,13 @@
2323
CONF_EMAIL,
2424
CONF_HARDWARE_ID,
2525
CONF_LAST_BRIDGE_HARDWARE_ID,
26-
CONF_PASSWORD,
26+
CONF_REFRESH_TOKEN,
2727
# Config entry title and unique ID may contain sensitive data:
2828
CONF_TITLE,
2929
CONF_UNIQUE_ID,
3030
CONF_USERNAME,
3131
CONF_USER_ID,
32+
CONF_USER_UUID,
3233
}
3334

3435

‎homeassistant/components/notion/manifest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"integration_type": "hub",
88
"iot_class": "cloud_polling",
99
"loggers": ["aionotion"],
10-
"requirements": ["aionotion==2024.02.0"]
10+
"requirements": ["aionotion==2024.02.1"]
1111
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Define notion utilities."""
2+
from aionotion import (
3+
async_get_client_with_credentials as cwc,
4+
async_get_client_with_refresh_token as cwrt,
5+
)
6+
from aionotion.client import Client
7+
8+
from homeassistant.core import HomeAssistant
9+
from homeassistant.helpers import aiohttp_client
10+
from homeassistant.helpers.instance_id import async_get
11+
12+
13+
async def async_get_client_with_credentials(
14+
hass: HomeAssistant, email: str, password: str
15+
) -> Client:
16+
"""Get a Notion client with credentials."""
17+
session = aiohttp_client.async_get_clientsession(hass)
18+
instance_id = await async_get(hass)
19+
return await cwc(email, password, session=session, session_name=instance_id)
20+
21+
22+
async def async_get_client_with_refresh_token(
23+
hass: HomeAssistant, user_uuid: str, refresh_token: str
24+
) -> Client:
25+
"""Get a Notion client with credentials."""
26+
session = aiohttp_client.async_get_clientsession(hass)
27+
instance_id = await async_get(hass)
28+
return await cwrt(
29+
user_uuid, refresh_token, session=session, session_name=instance_id
30+
)

‎requirements_all.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ aiomusiccast==0.14.8
315315
aionanoleaf==0.2.1
316316

317317
# homeassistant.components.notion
318-
aionotion==2024.02.0
318+
aionotion==2024.02.1
319319

320320
# homeassistant.components.oncue
321321
aiooncue==0.3.5

‎requirements_test_all.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ aiomusiccast==0.14.8
288288
aionanoleaf==0.2.1
289289

290290
# homeassistant.components.notion
291-
aionotion==2024.02.0
291+
aionotion==2024.02.1
292292

293293
# homeassistant.components.oncue
294294
aiooncue==0.3.5

‎tests/components/notion/conftest.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
TEST_USERNAME = "user@host.com"
1919
TEST_PASSWORD = "password123"
20+
TEST_REFRESH_TOKEN = "abcde12345"
21+
TEST_USER_UUID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
2022

2123

2224
@pytest.fixture
@@ -47,6 +49,7 @@ def client_fixture(data_bridge, data_listener, data_sensor, data_user_preference
4749
]
4850
)
4951
),
52+
refresh_token=TEST_REFRESH_TOKEN,
5053
sensor=Mock(
5154
async_all=AsyncMock(
5255
return_value=[
@@ -61,6 +64,7 @@ def client_fixture(data_bridge, data_listener, data_sensor, data_user_preference
6164
)
6265
)
6366
),
67+
user_uuid=TEST_USER_UUID,
6468
)
6569

6670

@@ -107,18 +111,21 @@ def data_user_preferences_fixture():
107111

108112
@pytest.fixture(name="get_client")
109113
def get_client_fixture(client):
110-
"""Define a fixture to mock the async_get_client method."""
114+
"""Define a fixture to mock the client retrieval methods."""
111115
return AsyncMock(return_value=client)
112116

113117

114118
@pytest.fixture(name="mock_aionotion")
115119
async def mock_aionotion_fixture(client):
116120
"""Define a fixture to patch aionotion."""
117121
with patch(
118-
"homeassistant.components.notion.async_get_client",
122+
"homeassistant.components.notion.async_get_client_with_credentials",
119123
AsyncMock(return_value=client),
120124
), patch(
121-
"homeassistant.components.notion.config_flow.async_get_client",
125+
"homeassistant.components.notion.async_get_client_with_refresh_token",
126+
AsyncMock(return_value=client),
127+
), patch(
128+
"homeassistant.components.notion.config_flow.async_get_client_with_credentials",
122129
AsyncMock(return_value=client),
123130
):
124131
yield

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /