Validate scopes in SmartThings config flow (#139569)

pull/139571/head
Joost Lekkerkerker 2025-03-01 12:47:58 +01:00 committed by GitHub
parent 042e4d20c5
commit fe5cd5c55c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 115 additions and 1 deletions

View File

@ -34,6 +34,8 @@ class SmartThingsConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
"""Create an entry for SmartThings."""
if data[CONF_TOKEN]["scope"].split() != SCOPES:
return self.async_abort(reason="missing_scopes")
client = SmartThings(session=async_get_clientsession(self.hass))
client.authenticate(data[CONF_TOKEN][CONF_ACCESS_TOKEN])
locations = await client.get_locations()

View File

@ -23,7 +23,8 @@
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reauth_account_mismatch": "Authenticated account does not match the account to be reauthenticated. Please log in with the correct account and pick the right location.",
"reauth_location_mismatch": "Authenticated location does not match the location to be reauthenticated. Please log in with the correct account and pick the right location."
"reauth_location_mismatch": "Authenticated location does not match the location to be reauthenticated. Please log in with the correct account and pick the right location.",
"missing_scopes": "Authentication failed. Please make sure you have granted all required permissions."
}
},
"entity": {

View File

@ -101,6 +101,66 @@ async def test_full_flow(
assert result["result"].unique_id == "397678e5-9995-4a39-9d9f-ae6ba310236c"
@pytest.mark.usefixtures("current_request_with_host")
async def test_not_enough_scopes(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
mock_smartthings: AsyncMock,
mock_setup_entry: AsyncMock,
) -> None:
"""Test we abort if we don't have enough scopes."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
state = config_entry_oauth2_flow._encode_jwt(
hass,
{
"flow_id": result["flow_id"],
"redirect_uri": "https://example.com/auth/external/callback",
},
)
assert result["type"] is FlowResultType.EXTERNAL_STEP
assert result["url"] == (
"https://api.smartthings.com/oauth/authorize"
"?response_type=code&client_id=CLIENT_ID"
"&redirect_uri=https://example.com/auth/external/callback"
f"&state={state}"
"&scope=r:devices:*+w:devices:*+x:devices:*+r:hubs:*+"
"r:locations:*+w:locations:*+x:locations:*+r:scenes:*+"
"x:scenes:*+r:rules:*+w:rules:*+r:installedapps+w:installedapps+sse"
)
client = await hass_client_no_auth()
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
assert resp.status == HTTPStatus.OK
assert resp.headers["content-type"] == "text/html; charset=utf-8"
aioclient_mock.clear_requests()
aioclient_mock.post(
"https://auth-global.api.smartthings.com/oauth/token",
json={
"refresh_token": "mock-refresh-token",
"access_token": "mock-access-token",
"token_type": "Bearer",
"expires_in": 82806,
"scope": "r:devices:* w:devices:* x:devices:* r:hubs:* "
"r:locations:* w:locations:* x:locations:* "
"r:scenes:* x:scenes:* r:rules:* w:rules:* "
"r:installedapps w:installedapps",
"access_tier": 0,
"installed_app_id": "5aaaa925-2be1-4e40-b257-e4ef59083324",
},
)
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "missing_scopes"
@pytest.mark.usefixtures("current_request_with_host")
async def test_duplicate_entry(
hass: HomeAssistant,
@ -227,6 +287,57 @@ async def test_reauthentication(
}
@pytest.mark.usefixtures("current_request_with_host")
async def test_reauthentication_wrong_scopes(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
mock_smartthings: AsyncMock,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test SmartThings reauthentication with wrong scopes."""
mock_config_entry.add_to_hass(hass)
result = await mock_config_entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
state = config_entry_oauth2_flow._encode_jwt(
hass,
{
"flow_id": result["flow_id"],
"redirect_uri": "https://example.com/auth/external/callback",
},
)
client = await hass_client_no_auth()
await client.get(f"/auth/external/callback?code=abcd&state={state}")
aioclient_mock.post(
"https://auth-global.api.smartthings.com/oauth/token",
json={
"refresh_token": "new-refresh-token",
"access_token": "new-access-token",
"token_type": "Bearer",
"expires_in": 82806,
"scope": "r:devices:* w:devices:* x:devices:* r:hubs:* "
"r:locations:* w:locations:* x:locations:* "
"r:scenes:* x:scenes:* r:rules:* w:rules:* "
"r:installedapps w:installedapps",
"access_tier": 0,
"installed_app_id": "5aaaa925-2be1-4e40-b257-e4ef59083324",
},
)
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "missing_scopes"
@pytest.mark.usefixtures("current_request_with_host")
async def test_reauth_account_mismatch(
hass: HomeAssistant,