diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index f5605886628..b22f93f11f1 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -2,7 +2,8 @@ import base64 from collections import OrderedDict import logging -from typing import Any, Dict, List, Optional, cast + +from typing import Any, Dict, List, Optional, Set, cast # noqa: F401 import bcrypt import voluptuous as vol @@ -52,6 +53,9 @@ class Data: self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, private=True) self._data = None # type: Optional[Dict[str, Any]] + # Legacy mode will allow usernames to start/end with whitespace + # and will compare usernames case-insensitive. + # Remove in 2020 or when we launch 1.0. self.is_legacy = False @callback @@ -60,7 +64,7 @@ class Data: if self.is_legacy: return username - return username.strip() + return username.strip().casefold() async def async_load(self) -> None: """Load stored data.""" @@ -71,9 +75,26 @@ class Data: 'users': [] } + seen = set() # type: Set[str] + for user in data['users']: username = user['username'] + # check if we have duplicates + folded = username.casefold() + + if folded in seen: + self.is_legacy = True + + logging.getLogger(__name__).warning( + "Home Assistant auth provider is running in legacy mode " + "because we detected usernames that are case-insensitive" + "equivalent. Please change the username: '%s'.", username) + + break + + seen.add(folded) + # check if we have unstripped usernames if username != username.strip(): self.is_legacy = True @@ -81,7 +102,7 @@ class Data: logging.getLogger(__name__).warning( "Home Assistant auth provider is running in legacy mode " "because we detected usernames that start or end in a " - "space. Please change the username.") + "space. Please change the username: '%s'.", username) break @@ -103,7 +124,7 @@ class Data: # Compare all users to avoid timing attacks. for user in self.users: - if username == user['username']: + if self.normalize_username(user['username']) == username: found = user if found is None: diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index b654b42fb35..ffc4d67f21d 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -73,7 +73,6 @@ async def test_changing_password_raises_invalid_user(data, hass): async def test_adding_user(data, hass): """Test adding a user.""" data.add_auth('test-user', 'test-pass') - data.validate_login('test-user', 'test-pass') data.validate_login(' test-user ', 'test-pass') @@ -81,7 +80,7 @@ async def test_adding_user_duplicate_username(data, hass): """Test adding a user with duplicate username.""" data.add_auth('test-user', 'test-pass') with pytest.raises(hass_auth.InvalidUser): - data.add_auth('test-user ', 'other-pass') + data.add_auth('TEST-user ', 'other-pass') async def test_validating_password_invalid_password(data, hass): @@ -91,16 +90,22 @@ async def test_validating_password_invalid_password(data, hass): with pytest.raises(hass_auth.InvalidAuth): data.validate_login(' test-user ', 'invalid-pass') + with pytest.raises(hass_auth.InvalidAuth): + data.validate_login('test-user', 'test-pass ') + + with pytest.raises(hass_auth.InvalidAuth): + data.validate_login('test-user', 'Test-pass') + async def test_changing_password(data, hass): """Test adding a user.""" data.add_auth('test-user', 'test-pass') - data.change_password('test-user ', 'new-pass') + data.change_password('TEST-USER ', 'new-pass') with pytest.raises(hass_auth.InvalidAuth): data.validate_login('test-user', 'test-pass') - data.validate_login('test-user', 'new-pass') + data.validate_login('test-UsEr', 'new-pass') async def test_login_flow_validates(data, hass): @@ -122,18 +127,18 @@ async def test_login_flow_validates(data, hass): assert result['errors']['base'] == 'invalid_auth' result = await flow.async_step_init({ - 'username': 'test-user ', + 'username': 'TEST-user ', 'password': 'incorrect-pass', }) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM assert result['errors']['base'] == 'invalid_auth' result = await flow.async_step_init({ - 'username': 'test-user', + 'username': 'test-USER', 'password': 'test-pass', }) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result['data']['username'] == 'test-user' + assert result['data']['username'] == 'test-USER' async def test_saving_loading(data, hass): @@ -179,6 +184,9 @@ async def test_legacy_adding_user_duplicate_username(legacy_data, hass): legacy_data.add_auth('test-user', 'test-pass') with pytest.raises(hass_auth.InvalidUser): legacy_data.add_auth('test-user', 'other-pass') + # Not considered duplicate + legacy_data.add_auth('test-user ', 'test-pass') + legacy_data.add_auth('Test-user', 'test-pass') async def test_legacy_validating_password_invalid_password(legacy_data, hass):