Validate username during onboarding before creating a new user (#182320)

This commit is contained in:
Robert Resch
2026-09-18 10:44:21 +00:00
committed by Franck Nijhof
parent 100c365aa9
commit 571c756524
2 changed files with 61 additions and 2 deletions
+10 -2
View File
@@ -10,7 +10,7 @@ from aiohttp.web_exceptions import HTTPUnauthorized
import voluptuous as vol
from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.auth.providers.homeassistant import HassAuthProvider
from homeassistant.auth.providers.homeassistant import HassAuthProvider, InvalidUsername
from homeassistant.components import person
from homeassistant.components.auth import indieauth
from homeassistant.components.http import KEY_HASS, KEY_HASS_REFRESH_TOKEN_ID
@@ -199,10 +199,18 @@ class UserOnboardingView(_BaseOnboardingStepView):
provider = _async_get_hass_provider(hass)
await provider.async_initialize()
# Add the auth before creating the user, as it validates the
# username, to avoid leaving an orphaned user behind on failure.
try:
await provider.async_add_auth(data["username"], data["password"])
except InvalidUsername as err:
return self.json_message(
str(err), HTTPStatus.BAD_REQUEST, err.translation_key
)
user = await hass.auth.async_create_user(
data["name"], group_ids=[GROUP_ID_ADMIN]
)
await provider.async_add_auth(data["username"], data["password"])
credentials = await provider.async_get_or_create_credentials(
{"username": data["username"]}
)
+51
View File
@@ -256,6 +256,57 @@ async def test_onboarding_user_invalid_name(
assert resp.status == 400
@pytest.mark.parametrize(
("username", "expected_code"),
[
pytest.param("Test-User", "username_not_normalized", id="uppercase"),
pytest.param("test-user ", "username_not_normalized", id="whitespace"),
pytest.param("existing-user", "username_already_exists", id="duplicate"),
],
)
async def test_onboarding_user_invalid_username(
hass: HomeAssistant,
hass_storage: dict[str, Any],
hass_client_no_auth: ClientSessionGenerator,
username: str,
expected_code: str,
) -> None:
"""Test a rejected username does not leave an orphaned user behind."""
mock_storage(hass_storage, {"done": []})
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
provider = views._async_get_hass_provider(hass)
await provider.async_initialize()
await provider.async_add_auth("existing-user", "test-pass")
cur_users = len(await hass.auth.async_get_users())
client = await hass_client_no_auth()
resp = await client.post(
"/api/onboarding/users",
json={
"client_id": CLIENT_ID,
"name": "Test Name",
"username": username,
"password": "test-pass",
"language": "en",
},
)
assert resp.status == HTTPStatus.BAD_REQUEST
body = await resp.json()
assert body["code"] == expected_code
# The rejected username is echoed back so the frontend can explain the failure
assert username in body["message"]
# The step stays open so onboarding can be retried with another username,
# and no user may be left behind from the rejected attempt.
assert const.STEP_USER not in hass_storage[const.DOMAIN]["data"]["done"]
assert len(await hass.auth.async_get_users()) == cur_users
async def test_onboarding_user_race(
hass: HomeAssistant,
hass_storage: dict[str, Any],