From fe1840f9016c87288840f307620484e8683885cc Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sun, 10 Mar 2019 19:55:36 -0700 Subject: [PATCH] Deprecate http.api_password (#21884) * Deprecated http.api_password * Deprecated ApiConfig.api_password GitHub Drafted PR would trigger CI after changed it to normal PR. I have to commit a comment change to trigger it * Trigger CI * Adjust if- elif chain in auth middleware --- homeassistant/auth/__init__.py | 19 +- .../auth/providers/legacy_api_password.py | 61 ++-- homeassistant/bootstrap.py | 4 +- homeassistant/components/__init__.py | 1 + homeassistant/components/api/__init__.py | 4 +- homeassistant/components/camera/proxy.py | 9 +- homeassistant/components/frontend/__init__.py | 2 +- homeassistant/components/hassio/auth.py | 6 +- homeassistant/components/hassio/handler.py | 7 +- homeassistant/components/http/__init__.py | 58 ++-- homeassistant/components/http/auth.py | 261 +++++++++--------- homeassistant/components/http/const.py | 2 + homeassistant/components/http/view.py | 4 +- homeassistant/components/mqtt/__init__.py | 8 - .../components/websocket_api/auth.py | 21 +- homeassistant/components/zeroconf/__init__.py | 4 +- homeassistant/config.py | 9 +- homeassistant/core.py | 2 +- .../providers/test_legacy_api_password.py | 24 +- tests/components/api/test_init.py | 8 +- tests/components/hassio/test_init.py | 6 - tests/components/http/test_auth.py | 72 ++--- tests/components/http/test_init.py | 6 +- tests/components/mqtt/test_server.py | 18 -- tests/components/websocket_api/__init__.py | 2 +- tests/conftest.py | 5 +- tests/test_config.py | 5 +- 27 files changed, 304 insertions(+), 324 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index bb90f296468..9e4b9d09d78 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -100,9 +100,21 @@ class AuthManager: """Return a list of available auth modules.""" return list(self._mfa_modules.values()) + def get_auth_provider(self, provider_type: str, provider_id: str) \ + -> Optional[AuthProvider]: + """Return an auth provider, None if not found.""" + return self._providers.get((provider_type, provider_id)) + + def get_auth_providers(self, provider_type: str) \ + -> List[AuthProvider]: + """Return a List of auth provider of one type, Empty if not found.""" + return [provider + for (p_type, _), provider in self._providers.items() + if p_type == provider_type] + def get_auth_mfa_module(self, module_id: str) \ -> Optional[MultiFactorAuthModule]: - """Return an multi-factor auth module, None if not found.""" + """Return a multi-factor auth module, None if not found.""" return self._mfa_modules.get(module_id) async def async_get_users(self) -> List[models.User]: @@ -113,6 +125,11 @@ class AuthManager: """Retrieve a user.""" return await self._store.async_get_user(user_id) + async def async_get_owner(self) -> Optional[models.User]: + """Retrieve the owner.""" + users = await self.async_get_users() + return next((user for user in users if user.is_owner), None) + async def async_get_group(self, group_id: str) -> Optional[models.Group]: """Retrieve all groups.""" return await self._store.async_get_group(group_id) diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index 6cdb12b7157..e85d831a325 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -4,27 +4,23 @@ Support Legacy API password auth provider. It will be removed when auth system production ready """ import hmac -from typing import Any, Dict, Optional, cast, TYPE_CHECKING +from typing import Any, Dict, Optional, cast import voluptuous as vol from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow from .. import AuthManager from ..models import Credentials, UserMeta, User -if TYPE_CHECKING: - from homeassistant.components.http import HomeAssistantHTTP # noqa: F401 - - -USER_SCHEMA = vol.Schema({ - vol.Required('username'): str, -}) - +AUTH_PROVIDER_TYPE = 'legacy_api_password' +CONF_API_PASSWORD = 'api_password' CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({ + vol.Required(CONF_API_PASSWORD): cv.string, }, extra=vol.PREVENT_EXTRA) LEGACY_USER_NAME = 'Legacy API password user' @@ -34,40 +30,45 @@ class InvalidAuthError(HomeAssistantError): """Raised when submitting invalid authentication.""" -async def async_get_user(hass: HomeAssistant) -> User: - """Return the legacy API password user.""" +async def async_validate_password(hass: HomeAssistant, password: str)\ + -> Optional[User]: + """Return a user if password is valid. None if not.""" auth = cast(AuthManager, hass.auth) # type: ignore - found = None - - for prv in auth.auth_providers: - if prv.type == 'legacy_api_password': - found = prv - break - - if found is None: + providers = auth.get_auth_providers(AUTH_PROVIDER_TYPE) + if not providers: raise ValueError('Legacy API password provider not found') - return await auth.async_get_or_create_user( - await found.async_get_or_create_credentials({}) - ) + try: + provider = cast(LegacyApiPasswordAuthProvider, providers[0]) + provider.async_validate_login(password) + return await auth.async_get_or_create_user( + await provider.async_get_or_create_credentials({}) + ) + except InvalidAuthError: + return None -@AUTH_PROVIDERS.register('legacy_api_password') +@AUTH_PROVIDERS.register(AUTH_PROVIDER_TYPE) class LegacyApiPasswordAuthProvider(AuthProvider): - """Example auth provider based on hardcoded usernames and passwords.""" + """An auth provider support legacy api_password.""" DEFAULT_TITLE = 'Legacy API Password' + @property + def api_password(self) -> str: + """Return api_password.""" + return str(self.config[CONF_API_PASSWORD]) + async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow: """Return a flow to login.""" return LegacyLoginFlow(self) @callback def async_validate_login(self, password: str) -> None: - """Validate a username and password.""" - hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP + """Validate password.""" + api_password = str(self.config[CONF_API_PASSWORD]) - if not hmac.compare_digest(hass_http.api_password.encode('utf-8'), + if not hmac.compare_digest(api_password.encode('utf-8'), password.encode('utf-8')): raise InvalidAuthError @@ -99,12 +100,6 @@ class LegacyLoginFlow(LoginFlow): """Handle the step of the form.""" errors = {} - hass_http = getattr(self.hass, 'http', None) - if hass_http is None or not hass_http.api_password: - return self.async_abort( - reason='no_api_password_set' - ) - if user_input is not None: try: cast(LegacyApiPasswordAuthProvider, self._auth_provider)\ diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 06f4fdd8788..444b4a9f855 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -99,12 +99,12 @@ async def async_from_config_dict(config: Dict[str, Any], "This may cause issues") core_config = config.get(core.DOMAIN, {}) - has_api_password = bool(config.get('http', {}).get('api_password')) + api_password = config.get('http', {}).get('api_password') trusted_networks = config.get('http', {}).get('trusted_networks') try: await conf_util.async_process_ha_core_config( - hass, core_config, has_api_password, trusted_networks) + hass, core_config, api_password, trusted_networks) except vol.Invalid as config_err: conf_util.async_log_exception( config_err, 'homeassistant', core_config, hass) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index f3045df6a12..533811e275d 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -166,6 +166,7 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: _LOGGER.error(err) return + # auth only processed during startup await conf_util.async_process_ha_core_config( hass, conf.get(ha.DOMAIN) or {}) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 7639ac621fe..beba17ee2ea 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -168,11 +168,11 @@ class APIDiscoveryView(HomeAssistantView): def get(self, request): """Get discovery information.""" hass = request.app['hass'] - needs_auth = hass.config.api.api_password is not None return self.json({ ATTR_BASE_URL: hass.config.api.base_url, ATTR_LOCATION_NAME: hass.config.location_name, - ATTR_REQUIRES_API_PASSWORD: needs_auth, + # always needs authentication + ATTR_REQUIRES_API_PASSWORD: True, ATTR_VERSION: __version__, }) diff --git a/homeassistant/components/camera/proxy.py b/homeassistant/components/camera/proxy.py index 8afd71abc26..3e6e4911d27 100644 --- a/homeassistant/components/camera/proxy.py +++ b/homeassistant/components/camera/proxy.py @@ -11,13 +11,11 @@ from datetime import timedelta import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE, \ - HTTP_HEADER_HA_AUTH +from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util -from homeassistant.components.camera import async_get_still_stream REQUIREMENTS = ['pillow==5.4.1'] @@ -209,9 +207,6 @@ class ProxyCamera(Camera): or config.get(CONF_CACHE_IMAGES)) self._last_image_time = dt_util.utc_from_timestamp(0) self._last_image = None - self._headers = ( - {HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password} - if self.hass.config.api.api_password is not None else None) self._mode = config.get(CONF_MODE) def camera_image(self): @@ -252,7 +247,7 @@ class ProxyCamera(Camera): return await self.hass.components.camera.async_get_mjpeg_stream( request, self._proxied_camera) - return await async_get_still_stream( + return await self.hass.components.camera.async_get_still_stream( request, self._async_stream_image, self.content_type, self.frame_interval) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8f1e563b782..3fa5791b7bc 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -407,7 +407,7 @@ class IndexView(HomeAssistantView): }) no_auth = '1' - if hass.config.api.api_password and not request[KEY_AUTHENTICATED]: + if not request[KEY_AUTHENTICATED]: # do not try to auto connect on load no_auth = '0' diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index b104d53aff9..05c183ccd60 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -57,9 +57,9 @@ class HassIOAuth(HomeAssistantView): def _get_provider(self): """Return Homeassistant auth provider.""" - for prv in self.hass.auth.auth_providers: - if prv.type == 'homeassistant': - return prv + prv = self.hass.auth.get_auth_provider('homeassistant', None) + if prv is not None: + return prv _LOGGER.error("Can't find Home Assistant auth.") raise HTTPNotFound() diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 640ed29e578..46e32c9f7c3 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -7,8 +7,10 @@ import aiohttp import async_timeout from homeassistant.components.http import ( - CONF_API_PASSWORD, CONF_SERVER_HOST, CONF_SERVER_PORT, - CONF_SSL_CERTIFICATE) + CONF_SERVER_HOST, + CONF_SERVER_PORT, + CONF_SSL_CERTIFICATE, +) from homeassistant.const import CONF_TIME_ZONE, SERVER_PORT from .const import X_HASSIO @@ -125,7 +127,6 @@ class HassIO: options = { 'ssl': CONF_SSL_CERTIFICATE in http_config, 'port': port, - 'password': http_config.get(CONF_API_PASSWORD), 'watchdog': True, 'refresh_token': refresh_token, } diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 93afbc04396..0bcf3f85ff7 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -18,7 +18,12 @@ from homeassistant.util.logging import HideSensitiveDataFilter from .auth import setup_auth from .ban import setup_bans -from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa +from .const import ( # noqa + KEY_AUTHENTICATED, + KEY_HASS, + KEY_HASS_USER, + KEY_REAL_IP, +) from .cors import setup_cors from .real_ip import setup_real_ip from .static import CACHE_HEADERS, CachingStaticResource @@ -66,8 +71,22 @@ def trusted_networks_deprecated(value): return value +def api_password_deprecated(value): + """Warn user api_password config is deprecated.""" + if not value: + return value + + _LOGGER.warning( + "Configuring api_password via the http component has been" + " deprecated. Use the legacy api password auth provider instead." + " For instructions, see https://www.home-assistant.io/docs/" + "authentication/providers/#legacy-api-password") + return value + + HTTP_SCHEMA = vol.Schema({ - vol.Optional(CONF_API_PASSWORD): cv.string, + vol.Optional(CONF_API_PASSWORD): + vol.All(cv.string, api_password_deprecated), vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string, vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, vol.Optional(CONF_BASE_URL): cv.string, @@ -98,12 +117,10 @@ class ApiConfig: """Configuration settings for API server.""" def __init__(self, host: str, port: Optional[int] = SERVER_PORT, - use_ssl: bool = False, - api_password: Optional[str] = None) -> None: + use_ssl: bool = False) -> None: """Initialize a new API config object.""" self.host = host self.port = port - self.api_password = api_password host = host.rstrip('/') if host.startswith(("http://", "https://")): @@ -133,7 +150,6 @@ async def async_setup(hass, config): cors_origins = conf[CONF_CORS_ORIGINS] use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False) trusted_proxies = conf.get(CONF_TRUSTED_PROXIES, []) - trusted_networks = conf[CONF_TRUSTED_NETWORKS] is_ban_enabled = conf[CONF_IP_BAN_ENABLED] login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] ssl_profile = conf[CONF_SSL_PROFILE] @@ -146,14 +162,12 @@ async def async_setup(hass, config): hass, server_host=server_host, server_port=server_port, - api_password=api_password, ssl_certificate=ssl_certificate, ssl_peer_certificate=ssl_peer_certificate, ssl_key=ssl_key, cors_origins=cors_origins, use_x_forwarded_for=use_x_forwarded_for, trusted_proxies=trusted_proxies, - trusted_networks=trusted_networks, login_threshold=login_threshold, is_ban_enabled=is_ban_enabled, ssl_profile=ssl_profile, @@ -183,8 +197,7 @@ async def async_setup(hass, config): host = hass_util.get_local_ip() port = server_port - hass.config.api = ApiConfig(host, port, ssl_certificate is not None, - api_password) + hass.config.api = ApiConfig(host, port, ssl_certificate is not None) return True @@ -192,13 +205,14 @@ async def async_setup(hass, config): class HomeAssistantHTTP: """HTTP server for Home Assistant.""" - def __init__(self, hass, api_password, + def __init__(self, hass, ssl_certificate, ssl_peer_certificate, ssl_key, server_host, server_port, cors_origins, - use_x_forwarded_for, trusted_proxies, trusted_networks, + use_x_forwarded_for, trusted_proxies, login_threshold, is_ban_enabled, ssl_profile): """Initialize the HTTP Home Assistant server.""" app = self.app = web.Application(middlewares=[]) + app[KEY_HASS] = hass # This order matters setup_real_ip(app, use_x_forwarded_for, trusted_proxies) @@ -206,34 +220,16 @@ class HomeAssistantHTTP: if is_ban_enabled: setup_bans(hass, app, login_threshold) - if hass.auth.support_legacy: - _LOGGER.warning( - "legacy_api_password support has been enabled. If you don't " - "require it, remove the 'api_password' from your http config.") - - for prv in hass.auth.auth_providers: - if prv.type == 'trusted_networks': - # auth_provider.trusted_networks will override - # http.trusted_networks, http.trusted_networks will be - # removed from future release - trusted_networks = prv.trusted_networks - break - - setup_auth(app, trusted_networks, - api_password if hass.auth.support_legacy else None) + setup_auth(hass, app) setup_cors(app, cors_origins) - app['hass'] = hass - self.hass = hass - self.api_password = api_password self.ssl_certificate = ssl_certificate self.ssl_peer_certificate = ssl_peer_certificate self.ssl_key = ssl_key self.server_host = server_host self.server_port = server_port - self.trusted_networks = trusted_networks self.is_ban_enabled = is_ban_enabled self.ssl_profile = ssl_profile self._handler = None diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 312fc2164c3..4736ef12391 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -1,6 +1,5 @@ """Authentication for HTTP component.""" import base64 -import hmac import logging from aiohttp import hdrs @@ -13,7 +12,11 @@ from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.core import callback from homeassistant.util import dt as dt_util -from .const import KEY_AUTHENTICATED, KEY_REAL_IP +from .const import ( + KEY_AUTHENTICATED, + KEY_HASS_USER, + KEY_REAL_IP, +) _LOGGER = logging.getLogger(__name__) @@ -40,10 +43,125 @@ def async_sign_path(hass, refresh_token_id, path, expiration): @callback -def setup_auth(app, trusted_networks, api_password): +def setup_auth(hass, app): """Create auth middleware for the app.""" old_auth_warning = set() + support_legacy = hass.auth.support_legacy + if support_legacy: + _LOGGER.warning("legacy_api_password support has been enabled.") + + trusted_networks = [] + for prv in hass.auth.auth_providers: + if prv.type == 'trusted_networks': + trusted_networks += prv.trusted_networks + + async def async_validate_auth_header(request): + """ + Test authorization header against access token. + + Basic auth_type is legacy code, should be removed with api_password. + """ + try: + auth_type, auth_val = \ + request.headers.get(hdrs.AUTHORIZATION).split(' ', 1) + except ValueError: + # If no space in authorization header + return False + + if auth_type == 'Bearer': + refresh_token = await hass.auth.async_validate_access_token( + auth_val) + if refresh_token is None: + return False + + request[KEY_HASS_USER] = refresh_token.user + return True + + if auth_type == 'Basic' and support_legacy: + decoded = base64.b64decode(auth_val).decode('utf-8') + try: + username, password = decoded.split(':', 1) + except ValueError: + # If no ':' in decoded + return False + + if username != 'homeassistant': + return False + + user = await legacy_api_password.async_validate_password( + hass, password) + if user is None: + return False + + request[KEY_HASS_USER] = user + _LOGGER.info( + 'Basic auth with api_password is going to deprecate,' + ' please use a bearer token to access %s from %s', + request.path, request[KEY_REAL_IP]) + old_auth_warning.add(request.path) + return True + + return False + + async def async_validate_signed_request(request): + """Validate a signed request.""" + secret = hass.data.get(DATA_SIGN_SECRET) + + if secret is None: + return False + + signature = request.query.get(SIGN_QUERY_PARAM) + + if signature is None: + return False + + try: + claims = jwt.decode( + signature, + secret, + algorithms=['HS256'], + options={'verify_iss': False} + ) + except jwt.InvalidTokenError: + return False + + if claims['path'] != request.path: + return False + + refresh_token = await hass.auth.async_get_refresh_token(claims['iss']) + + if refresh_token is None: + return False + + request[KEY_HASS_USER] = refresh_token.user + return True + + async def async_validate_trusted_networks(request): + """Test if request is from a trusted ip.""" + ip_addr = request[KEY_REAL_IP] + + if not any(ip_addr in trusted_network + for trusted_network in trusted_networks): + return False + + user = await hass.auth.async_get_owner() + if user is None: + return False + + request[KEY_HASS_USER] = user + return True + + async def async_validate_legacy_api_password(request, password): + """Validate api_password.""" + user = await legacy_api_password.async_validate_password( + hass, password) + if user is None: + return False + + request[KEY_HASS_USER] = user + return True + @middleware async def auth_middleware(request, handler): """Authenticate as middleware.""" @@ -53,13 +171,14 @@ def setup_auth(app, trusted_networks, api_password): DATA_API_PASSWORD in request.query): if request.path not in old_auth_warning: _LOGGER.log( - logging.INFO if api_password else logging.WARNING, - 'You need to use a bearer token to access %s from %s', + logging.INFO if support_legacy else logging.WARNING, + 'api_password is going to deprecate. You need to use a' + ' bearer token to access %s from %s', request.path, request[KEY_REAL_IP]) old_auth_warning.add(request.path) if (hdrs.AUTHORIZATION in request.headers and - await async_validate_auth_header(request, api_password)): + await async_validate_auth_header(request)): # it included both use_auth and api_password Basic auth authenticated = True @@ -69,133 +188,21 @@ def setup_auth(app, trusted_networks, api_password): await async_validate_signed_request(request)): authenticated = True - elif (api_password and HTTP_HEADER_HA_AUTH in request.headers and - hmac.compare_digest( - api_password.encode('utf-8'), - request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))): - # A valid auth header has been set + elif (trusted_networks and + await async_validate_trusted_networks(request)): authenticated = True - request['hass_user'] = await legacy_api_password.async_get_user( - app['hass']) - elif (api_password and DATA_API_PASSWORD in request.query and - hmac.compare_digest( - api_password.encode('utf-8'), - request.query[DATA_API_PASSWORD].encode('utf-8'))): + elif (support_legacy and HTTP_HEADER_HA_AUTH in request.headers and + await async_validate_legacy_api_password( + request, request.headers[HTTP_HEADER_HA_AUTH])): authenticated = True - request['hass_user'] = await legacy_api_password.async_get_user( - app['hass']) - elif _is_trusted_ip(request, trusted_networks): - users = await app['hass'].auth.async_get_users() - for user in users: - if user.is_owner: - request['hass_user'] = user - authenticated = True - break + elif (support_legacy and DATA_API_PASSWORD in request.query and + await async_validate_legacy_api_password( + request, request.query[DATA_API_PASSWORD])): + authenticated = True request[KEY_AUTHENTICATED] = authenticated return await handler(request) app.middlewares.append(auth_middleware) - - -def _is_trusted_ip(request, trusted_networks): - """Test if request is from a trusted ip.""" - ip_addr = request[KEY_REAL_IP] - - return any( - ip_addr in trusted_network for trusted_network - in trusted_networks) - - -def validate_password(request, api_password): - """Test if password is valid.""" - return hmac.compare_digest( - api_password.encode('utf-8'), - request.app['hass'].http.api_password.encode('utf-8')) - - -async def async_validate_auth_header(request, api_password=None): - """ - Test authorization header against access token. - - Basic auth_type is legacy code, should be removed with api_password. - """ - if hdrs.AUTHORIZATION not in request.headers: - return False - - try: - auth_type, auth_val = \ - request.headers.get(hdrs.AUTHORIZATION).split(' ', 1) - except ValueError: - # If no space in authorization header - return False - - hass = request.app['hass'] - - if auth_type == 'Bearer': - refresh_token = await hass.auth.async_validate_access_token(auth_val) - if refresh_token is None: - return False - - request['hass_refresh_token'] = refresh_token - request['hass_user'] = refresh_token.user - return True - - if auth_type == 'Basic' and api_password is not None: - decoded = base64.b64decode(auth_val).decode('utf-8') - try: - username, password = decoded.split(':', 1) - except ValueError: - # If no ':' in decoded - return False - - if username != 'homeassistant': - return False - - if not hmac.compare_digest(api_password.encode('utf-8'), - password.encode('utf-8')): - return False - - request['hass_user'] = await legacy_api_password.async_get_user(hass) - return True - - return False - - -async def async_validate_signed_request(request): - """Validate a signed request.""" - hass = request.app['hass'] - secret = hass.data.get(DATA_SIGN_SECRET) - - if secret is None: - return False - - signature = request.query.get(SIGN_QUERY_PARAM) - - if signature is None: - return False - - try: - claims = jwt.decode( - signature, - secret, - algorithms=['HS256'], - options={'verify_iss': False} - ) - except jwt.InvalidTokenError: - return False - - if claims['path'] != request.path: - return False - - refresh_token = await hass.auth.async_get_refresh_token(claims['iss']) - - if refresh_token is None: - return False - - request['hass_refresh_token'] = refresh_token - request['hass_user'] = refresh_token.user - - return True diff --git a/homeassistant/components/http/const.py b/homeassistant/components/http/const.py index e5494e945c4..f26220e63d1 100644 --- a/homeassistant/components/http/const.py +++ b/homeassistant/components/http/const.py @@ -1,3 +1,5 @@ """HTTP specific constants.""" KEY_AUTHENTICATED = 'ha_authenticated' +KEY_HASS = 'hass' +KEY_HASS_USER = 'hass_user' KEY_REAL_IP = 'ha_real_ip' diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 9662f3e6c23..bb7f2c2fee2 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -14,7 +14,7 @@ from homeassistant.const import CONTENT_TYPE_JSON from homeassistant.core import Context, is_callback from homeassistant.helpers.json import JSONEncoder -from .const import KEY_AUTHENTICATED, KEY_REAL_IP +from .const import KEY_AUTHENTICATED, KEY_REAL_IP, KEY_HASS _LOGGER = logging.getLogger(__name__) @@ -91,7 +91,7 @@ def request_handler_factory(view, handler): async def handle(request): """Handle incoming request.""" - if not request.app['hass'].is_running: + if not request.app[KEY_HASS].is_running: return web.Response(status=503) authenticated = request.get(KEY_AUTHENTICATED, False) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e430b1fbc9f..18ebb004209 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -399,14 +399,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: conf = dict(conf) if CONF_EMBEDDED in conf or CONF_BROKER not in conf: - if (conf.get(CONF_PASSWORD) is None and - config.get('http', {}).get('api_password') is not None): - _LOGGER.error( - "Starting from release 0.76, the embedded MQTT broker does not" - " use api_password as default password anymore. Please set" - " password configuration. See https://home-assistant.io/docs/" - "mqtt/broker#embedded-broker for details") - return False broker_config = await _async_setup_server(hass, config) diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index f175327bf28..dbb43e08780 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -2,11 +2,12 @@ import voluptuous as vol from voluptuous.humanize import humanize_error -from homeassistant.const import __version__ -from homeassistant.components.http.auth import validate_password -from homeassistant.components.http.ban import process_wrong_login, \ - process_success_login from homeassistant.auth.providers import legacy_api_password +from homeassistant.components.http.ban import ( + process_wrong_login, + process_success_login, +) +from homeassistant.const import __version__ from .connection import ActiveConnection from .error import Disconnect @@ -80,9 +81,15 @@ class AuthPhase: refresh_token.user, refresh_token) elif self._hass.auth.support_legacy and 'api_password' in msg: - self._logger.debug("Received api_password") - if validate_password(self._request, msg['api_password']): - user = await legacy_api_password.async_get_user(self._hass) + self._logger.info( + "Received api_password, it is going to deprecate, please use" + " access_token instead. For instructions, see https://" + "developers.home-assistant.io/docs/en/external_api_websocket" + ".html#authentication-phase" + ) + user = await legacy_api_password.async_validate_password( + self._hass, msg['api_password']) + if user is not None: return await self._async_finish_auth(user, None) self._send_message(auth_invalid_message( diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index bf743eaf370..844246528a6 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -30,11 +30,11 @@ def setup(hass, config): zeroconf_name = '{}.{}'.format(hass.config.location_name, ZEROCONF_TYPE) - requires_api_password = hass.config.api.api_password is not None params = { 'version': __version__, 'base_url': hass.config.api.base_url, - 'requires_api_password': requires_api_password, + # always needs authentication + 'requires_api_password': True, } host_ip = util.get_local_ip() diff --git a/homeassistant/config.py b/homeassistant/config.py index 492db240eee..db59e2c2744 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -428,7 +428,7 @@ def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str: async def async_process_ha_core_config( hass: HomeAssistant, config: Dict, - has_api_password: bool = False, + api_password: Optional[str] = None, trusted_networks: Optional[Any] = None) -> None: """Process the [homeassistant] section from the configuration. @@ -444,8 +444,11 @@ async def async_process_ha_core_config( auth_conf = [ {'type': 'homeassistant'} ] - if has_api_password: - auth_conf.append({'type': 'legacy_api_password'}) + if api_password: + auth_conf.append({ + 'type': 'legacy_api_password', + 'api_password': api_password, + }) if trusted_networks: auth_conf.append({ 'type': 'trusted_networks', diff --git a/homeassistant/core.py b/homeassistant/core.py index 253900a39ef..df315ad63c0 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1180,7 +1180,7 @@ class Config: # List of loaded components self.components = set() # type: set - # API (HTTP) server configuration + # API (HTTP) server configuration, see components.http.ApiConfig self.api = None # type: Optional[Any] # Directory that holds the configuration diff --git a/tests/auth/providers/test_legacy_api_password.py b/tests/auth/providers/test_legacy_api_password.py index 96da624161a..3f4c257f000 100644 --- a/tests/auth/providers/test_legacy_api_password.py +++ b/tests/auth/providers/test_legacy_api_password.py @@ -1,6 +1,4 @@ """Tests for the legacy_api_password auth provider.""" -from unittest.mock import Mock - import pytest from homeassistant import auth, data_entry_flow @@ -19,6 +17,7 @@ def provider(hass, store): """Mock provider.""" return legacy_api_password.LegacyApiPasswordAuthProvider(hass, store, { 'type': 'legacy_api_password', + 'api_password': 'test-password', }) @@ -51,32 +50,13 @@ async def test_only_one_credentials(manager, provider): async def test_verify_login(hass, provider): """Test login using legacy api password auth provider.""" - hass.http = Mock(api_password='test-password') provider.async_validate_login('test-password') - hass.http = Mock(api_password='test-password') with pytest.raises(legacy_api_password.InvalidAuthError): provider.async_validate_login('invalid-password') -async def test_login_flow_abort(hass, manager): - """Test wrong config.""" - for http in ( - None, - Mock(api_password=None), - Mock(api_password=''), - ): - hass.http = http - - result = await manager.login_flow.async_init( - handler=('legacy_api_password', None) - ) - assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT - assert result['reason'] == 'no_api_password_set' - - async def test_login_flow_works(hass, manager): """Test wrong config.""" - hass.http = Mock(api_password='hello') result = await manager.login_flow.async_init( handler=('legacy_api_password', None) ) @@ -94,7 +74,7 @@ async def test_login_flow_works(hass, manager): result = await manager.login_flow.async_configure( flow_id=result['flow_id'], user_input={ - 'password': 'hello' + 'password': 'test-password' } ) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index a88c828efe8..c4f227e488b 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -407,14 +407,10 @@ def _listen_count(hass): async def test_api_error_log(hass, aiohttp_client, hass_access_token, - hass_admin_user, legacy_auth): + hass_admin_user): """Test if we can fetch the error log.""" hass.data[DATA_LOGGING] = '/some/path' - await async_setup_component(hass, 'api', { - 'http': { - 'api_password': 'yolo' - } - }) + await async_setup_component(hass, 'api', {}) client = await aiohttp_client(hass.http.app) resp = await client.get(const.URL_API_ERROR_LOG) diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 62e7278ba1f..435e03a1755 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -51,7 +51,6 @@ def test_setup_api_push_api_data(hass, aioclient_mock): with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'http': { - 'api_password': "123456", 'server_port': 9999 }, 'hassio': {} @@ -60,7 +59,6 @@ def test_setup_api_push_api_data(hass, aioclient_mock): assert aioclient_mock.call_count == 3 assert not aioclient_mock.mock_calls[1][2]['ssl'] - assert aioclient_mock.mock_calls[1][2]['password'] == "123456" assert aioclient_mock.mock_calls[1][2]['port'] == 9999 assert aioclient_mock.mock_calls[1][2]['watchdog'] @@ -71,7 +69,6 @@ def test_setup_api_push_api_data_server_host(hass, aioclient_mock): with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'http': { - 'api_password': "123456", 'server_port': 9999, 'server_host': "127.0.0.1" }, @@ -81,7 +78,6 @@ def test_setup_api_push_api_data_server_host(hass, aioclient_mock): assert aioclient_mock.call_count == 3 assert not aioclient_mock.mock_calls[1][2]['ssl'] - assert aioclient_mock.mock_calls[1][2]['password'] == "123456" assert aioclient_mock.mock_calls[1][2]['port'] == 9999 assert not aioclient_mock.mock_calls[1][2]['watchdog'] @@ -98,7 +94,6 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, assert aioclient_mock.call_count == 3 assert not aioclient_mock.mock_calls[1][2]['ssl'] - assert aioclient_mock.mock_calls[1][2]['password'] is None assert aioclient_mock.mock_calls[1][2]['port'] == 8123 refresh_token = aioclient_mock.mock_calls[1][2]['refresh_token'] hassio_user = await hass.auth.async_get_user( @@ -159,7 +154,6 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, assert aioclient_mock.call_count == 3 assert not aioclient_mock.mock_calls[1][2]['ssl'] - assert aioclient_mock.mock_calls[1][2]['password'] is None assert aioclient_mock.mock_calls[1][2]['port'] == 8123 assert aioclient_mock.mock_calls[1][2]['refresh_token'] == token.token diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 304bb4de997..a16b40213b8 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -7,7 +7,7 @@ import pytest from aiohttp import BasicAuth, web from aiohttp.web_exceptions import HTTPUnauthorized -from homeassistant.auth.providers import legacy_api_password +from homeassistant.auth.providers import trusted_networks from homeassistant.components.http.auth import setup_auth, async_sign_path from homeassistant.components.http.const import KEY_AUTHENTICATED from homeassistant.components.http.real_ip import setup_real_ip @@ -16,7 +16,7 @@ from homeassistant.setup import async_setup_component from . import mock_real_ip -API_PASSWORD = 'test1234' +API_PASSWORD = 'test-password' # Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases TRUSTED_NETWORKS = [ @@ -35,17 +35,22 @@ async def mock_handler(request): if not request[KEY_AUTHENTICATED]: raise HTTPUnauthorized - token = request.get('hass_refresh_token') - token_id = token.id if token else None user = request.get('hass_user') user_id = user.id if user else None return web.json_response(status=200, data={ - 'refresh_token_id': token_id, 'user_id': user_id, }) +async def get_legacy_user(auth): + """Get the user in legacy_api_password auth provider.""" + provider = auth.get_auth_provider('legacy_api_password', None) + return await auth.async_get_or_create_user( + await provider.async_get_or_create_credentials({}) + ) + + @pytest.fixture def app(hass): """Fixture to set up a web.Application.""" @@ -65,6 +70,19 @@ def app2(hass): return app +@pytest.fixture +def trusted_networks_auth(hass): + """Load trusted networks auth provider.""" + prv = trusted_networks.TrustedNetworksAuthProvider( + hass, hass.auth._store, { + 'type': 'trusted_networks', + 'trusted_networks': TRUSTED_NETWORKS, + } + ) + hass.auth._providers[(prv.type, prv.id)] = prv + return prv + + async def test_auth_middleware_loaded_by_default(hass): """Test accessing to server from banned IP when feature is off.""" with patch('homeassistant.components.http.setup_auth') as mock_setup: @@ -78,15 +96,14 @@ async def test_auth_middleware_loaded_by_default(hass): async def test_access_with_password_in_header(app, aiohttp_client, legacy_auth, hass): """Test access with password in header.""" - setup_auth(app, [], api_password=API_PASSWORD) + setup_auth(hass, app) client = await aiohttp_client(app) - user = await legacy_api_password.async_get_user(hass) + user = await get_legacy_user(hass.auth) req = await client.get( '/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) assert req.status == 200 assert await req.json() == { - 'refresh_token_id': None, 'user_id': user.id, } @@ -98,16 +115,15 @@ async def test_access_with_password_in_header(app, aiohttp_client, async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth, hass): """Test access with password in URL.""" - setup_auth(app, [], api_password=API_PASSWORD) + setup_auth(hass, app) client = await aiohttp_client(app) - user = await legacy_api_password.async_get_user(hass) + user = await get_legacy_user(hass.auth) resp = await client.get('/', params={ 'api_password': API_PASSWORD }) assert resp.status == 200 assert await resp.json() == { - 'refresh_token_id': None, 'user_id': user.id, } @@ -122,16 +138,15 @@ async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth, async def test_basic_auth_works(app, aiohttp_client, hass, legacy_auth): """Test access with basic authentication.""" - setup_auth(app, [], api_password=API_PASSWORD) + setup_auth(hass, app) client = await aiohttp_client(app) - user = await legacy_api_password.async_get_user(hass) + user = await get_legacy_user(hass.auth) req = await client.get( '/', auth=BasicAuth('homeassistant', API_PASSWORD)) assert req.status == 200 assert await req.json() == { - 'refresh_token_id': None, 'user_id': user.id, } @@ -153,9 +168,11 @@ async def test_basic_auth_works(app, aiohttp_client, hass, legacy_auth): assert req.status == 401 -async def test_access_with_trusted_ip(app2, aiohttp_client, hass_owner_user): +async def test_access_with_trusted_ip(hass, app2, trusted_networks_auth, + aiohttp_client, + hass_owner_user): """Test access with an untrusted ip address.""" - setup_auth(app2, TRUSTED_NETWORKS, api_password='some-pass') + setup_auth(hass, app2) set_mock_ip = mock_real_ip(app2) client = await aiohttp_client(app2) @@ -172,7 +189,6 @@ async def test_access_with_trusted_ip(app2, aiohttp_client, hass_owner_user): assert resp.status == 200, \ "{} should be trusted".format(remote_addr) assert await resp.json() == { - 'refresh_token_id': None, 'user_id': hass_owner_user.id, } @@ -181,7 +197,7 @@ async def test_auth_active_access_with_access_token_in_header( hass, app, aiohttp_client, hass_access_token): """Test access with access token in header.""" token = hass_access_token - setup_auth(app, [], api_password=None) + setup_auth(hass, app) client = await aiohttp_client(app) refresh_token = await hass.auth.async_validate_access_token( hass_access_token) @@ -190,7 +206,6 @@ async def test_auth_active_access_with_access_token_in_header( '/', headers={'Authorization': 'Bearer {}'.format(token)}) assert req.status == 200 assert await req.json() == { - 'refresh_token_id': refresh_token.id, 'user_id': refresh_token.user.id, } @@ -198,7 +213,6 @@ async def test_auth_active_access_with_access_token_in_header( '/', headers={'AUTHORIZATION': 'Bearer {}'.format(token)}) assert req.status == 200 assert await req.json() == { - 'refresh_token_id': refresh_token.id, 'user_id': refresh_token.user.id, } @@ -206,7 +220,6 @@ async def test_auth_active_access_with_access_token_in_header( '/', headers={'authorization': 'Bearer {}'.format(token)}) assert req.status == 200 assert await req.json() == { - 'refresh_token_id': refresh_token.id, 'user_id': refresh_token.user.id, } @@ -226,10 +239,12 @@ async def test_auth_active_access_with_access_token_in_header( assert req.status == 401 -async def test_auth_active_access_with_trusted_ip(app2, aiohttp_client, +async def test_auth_active_access_with_trusted_ip(hass, app2, + trusted_networks_auth, + aiohttp_client, hass_owner_user): """Test access with an untrusted ip address.""" - setup_auth(app2, TRUSTED_NETWORKS, None) + setup_auth(hass, app2) set_mock_ip = mock_real_ip(app2) client = await aiohttp_client(app2) @@ -246,7 +261,6 @@ async def test_auth_active_access_with_trusted_ip(app2, aiohttp_client, assert resp.status == 200, \ "{} should be trusted".format(remote_addr) assert await resp.json() == { - 'refresh_token_id': None, 'user_id': hass_owner_user.id, } @@ -254,15 +268,14 @@ async def test_auth_active_access_with_trusted_ip(app2, aiohttp_client, async def test_auth_legacy_support_api_password_access( app, aiohttp_client, legacy_auth, hass): """Test access using api_password if auth.support_legacy.""" - setup_auth(app, [], API_PASSWORD) + setup_auth(hass, app) client = await aiohttp_client(app) - user = await legacy_api_password.async_get_user(hass) + user = await get_legacy_user(hass.auth) req = await client.get( '/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) assert req.status == 200 assert await req.json() == { - 'refresh_token_id': None, 'user_id': user.id, } @@ -271,7 +284,6 @@ async def test_auth_legacy_support_api_password_access( }) assert resp.status == 200 assert await resp.json() == { - 'refresh_token_id': None, 'user_id': user.id, } @@ -280,7 +292,6 @@ async def test_auth_legacy_support_api_password_access( auth=BasicAuth('homeassistant', API_PASSWORD)) assert req.status == 200 assert await req.json() == { - 'refresh_token_id': None, 'user_id': user.id, } @@ -290,7 +301,7 @@ async def test_auth_access_signed_path( """Test access with signed url.""" app.router.add_post('/', mock_handler) app.router.add_get('/another_path', mock_handler) - setup_auth(app, [], None) + setup_auth(hass, app) client = await aiohttp_client(app) refresh_token = await hass.auth.async_validate_access_token( @@ -303,7 +314,6 @@ async def test_auth_access_signed_path( req = await client.get(signed_path) assert req.status == 200 data = await req.json() - assert data['refresh_token_id'] == refresh_token.id assert data['user_id'] == refresh_token.user.id # Use signature on other path diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index fadb91a3e03..a753dd275fe 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -143,15 +143,13 @@ async def test_api_base_url_removes_trailing_slash(hass): async def test_not_log_password(hass, aiohttp_client, caplog, legacy_auth): """Test access with password doesn't get logged.""" assert await async_setup_component(hass, 'api', { - 'http': { - http.CONF_API_PASSWORD: 'some-pass' - } + 'http': {} }) client = await aiohttp_client(hass.http.app) logging.getLogger('aiohttp.access').setLevel(logging.INFO) resp = await client.get('/api/', params={ - 'api_password': 'some-pass' + 'api_password': 'test-password' }) assert resp.status == 200 diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index 2589adf2f9c..71ef1dc1e43 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -19,24 +19,6 @@ class TestMQTT: """Stop everything that was started.""" self.hass.stop() - @patch('passlib.apps.custom_app_context', Mock(return_value='')) - @patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock())) - @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock())) - @patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro())) - @patch('homeassistant.components.mqtt.MQTT') - def test_creating_config_with_http_pass_only(self, mock_mqtt): - """Test if the MQTT server failed starts. - - Since 0.77, MQTT server has to set up its own password. - If user has api_password but don't have mqtt.password, MQTT component - will fail to start - """ - mock_mqtt().async_connect.return_value = mock_coro(True) - self.hass.bus.listen_once = MagicMock() - assert not setup_component(self.hass, mqtt.DOMAIN, { - 'http': {'api_password': 'http_secret'} - }) - @patch('passlib.apps.custom_app_context', Mock(return_value='')) @patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock())) @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock())) diff --git a/tests/components/websocket_api/__init__.py b/tests/components/websocket_api/__init__.py index c218c6165d4..e58197e60be 100644 --- a/tests/components/websocket_api/__init__.py +++ b/tests/components/websocket_api/__init__.py @@ -1,2 +1,2 @@ """Tests for the websocket API.""" -API_PASSWORD = 'test1234' +API_PASSWORD = 'test-password' diff --git a/tests/conftest.py b/tests/conftest.py index 1dc5733cf40..efe24c51533 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -153,10 +153,12 @@ def legacy_auth(hass): """Load legacy API password provider.""" prv = legacy_api_password.LegacyApiPasswordAuthProvider( hass, hass.auth._store, { - 'type': 'legacy_api_password' + 'type': 'legacy_api_password', + 'api_password': 'test-password', } ) hass.auth._providers[(prv.type, prv.id)] = prv + return prv @pytest.fixture @@ -168,6 +170,7 @@ def local_auth(hass): } ) hass.auth._providers[(prv.type, prv.id)] = prv + return prv @pytest.fixture diff --git a/tests/test_config.py b/tests/test_config.py index e860ff53b3d..8afad09c946 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -822,7 +822,7 @@ async def test_auth_provider_config(hass): 'time_zone': 'GMT', CONF_AUTH_PROVIDERS: [ {'type': 'homeassistant'}, - {'type': 'legacy_api_password'}, + {'type': 'legacy_api_password', 'api_password': 'some-pass'}, ], CONF_AUTH_MFA_MODULES: [ {'type': 'totp'}, @@ -873,11 +873,12 @@ async def test_auth_provider_config_default_api_password(hass): } if hasattr(hass, 'auth'): del hass.auth - await config_util.async_process_ha_core_config(hass, core_config, True) + await config_util.async_process_ha_core_config(hass, core_config, 'pass') assert len(hass.auth.auth_providers) == 2 assert hass.auth.auth_providers[0].type == 'homeassistant' assert hass.auth.auth_providers[1].type == 'legacy_api_password' + assert hass.auth.auth_providers[1].api_password == 'pass' async def test_auth_provider_config_default_trusted_networks(hass):