Validate scopes in SmartThings config flow (#139569)
parent
042e4d20c5
commit
fe5cd5c55c
|
@ -34,6 +34,8 @@ class SmartThingsConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||||
|
|
||||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||||
"""Create an entry for SmartThings."""
|
"""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 = SmartThings(session=async_get_clientsession(self.hass))
|
||||||
client.authenticate(data[CONF_TOKEN][CONF_ACCESS_TOKEN])
|
client.authenticate(data[CONF_TOKEN][CONF_ACCESS_TOKEN])
|
||||||
locations = await client.get_locations()
|
locations = await client.get_locations()
|
||||||
|
|
|
@ -23,7 +23,8 @@
|
||||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
"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_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": {
|
"entity": {
|
||||||
|
|
|
@ -101,6 +101,66 @@ async def test_full_flow(
|
||||||
assert result["result"].unique_id == "397678e5-9995-4a39-9d9f-ae6ba310236c"
|
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")
|
@pytest.mark.usefixtures("current_request_with_host")
|
||||||
async def test_duplicate_entry(
|
async def test_duplicate_entry(
|
||||||
hass: HomeAssistant,
|
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")
|
@pytest.mark.usefixtures("current_request_with_host")
|
||||||
async def test_reauth_account_mismatch(
|
async def test_reauth_account_mismatch(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|
Loading…
Reference in New Issue