mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-27 02:56:31 +00:00
Password reset (#1433)
* API to reset password * Fix error handling * fix lint * fix typing * fix await
This commit is contained in:
parent
9d6f4f5392
commit
69959b2c97
9
API.md
9
API.md
@ -828,3 +828,12 @@ We support:
|
|||||||
- Json `{ "user|name": "...", "password": "..." }`
|
- Json `{ "user|name": "...", "password": "..." }`
|
||||||
- application/x-www-form-urlencoded `user|name=...&password=...`
|
- application/x-www-form-urlencoded `user|name=...&password=...`
|
||||||
- BasicAuth
|
- BasicAuth
|
||||||
|
|
||||||
|
* POST `/auth/reset`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "xy",
|
||||||
|
"password": "new-password"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
@ -121,7 +121,9 @@ class RestAPI(CoreSysAttributes):
|
|||||||
api_auth = APIAuth()
|
api_auth = APIAuth()
|
||||||
api_auth.coresys = self.coresys
|
api_auth.coresys = self.coresys
|
||||||
|
|
||||||
self.webapp.add_routes([web.post("/auth", api_auth.auth)])
|
self.webapp.add_routes(
|
||||||
|
[web.post("/auth", api_auth.auth), web.post("/auth/reset", api_auth.reset)]
|
||||||
|
)
|
||||||
|
|
||||||
def _register_supervisor(self) -> None:
|
def _register_supervisor(self) -> None:
|
||||||
"""Register Supervisor functions."""
|
"""Register Supervisor functions."""
|
||||||
|
@ -1,22 +1,39 @@
|
|||||||
"""Init file for Hass.io auth/SSO RESTful API."""
|
"""Init file for Hass.io auth/SSO RESTful API."""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
from aiohttp import BasicAuth
|
from aiohttp import BasicAuth, web
|
||||||
|
from aiohttp.hdrs import AUTHORIZATION, CONTENT_TYPE, WWW_AUTHENTICATE
|
||||||
from aiohttp.web_exceptions import HTTPUnauthorized
|
from aiohttp.web_exceptions import HTTPUnauthorized
|
||||||
from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION, WWW_AUTHENTICATE
|
import voluptuous as vol
|
||||||
|
|
||||||
from .utils import api_process
|
from ..addons.addon import Addon
|
||||||
from ..const import REQUEST_FROM, CONTENT_TYPE_JSON, CONTENT_TYPE_URL
|
from ..const import (
|
||||||
|
ATTR_PASSWORD,
|
||||||
|
ATTR_USERNAME,
|
||||||
|
CONTENT_TYPE_JSON,
|
||||||
|
CONTENT_TYPE_URL,
|
||||||
|
REQUEST_FROM,
|
||||||
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import APIForbidden
|
from ..exceptions import APIForbidden
|
||||||
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SCHEMA_PASSWORD_RESET = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_USERNAME): vol.Coerce(str),
|
||||||
|
vol.Required(ATTR_PASSWORD): vol.Coerce(str),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class APIAuth(CoreSysAttributes):
|
class APIAuth(CoreSysAttributes):
|
||||||
"""Handle RESTful API for auth functions."""
|
"""Handle RESTful API for auth functions."""
|
||||||
|
|
||||||
def _process_basic(self, request, addon):
|
def _process_basic(self, request: web.Request, addon: Addon) -> bool:
|
||||||
"""Process login request with basic auth.
|
"""Process login request with basic auth.
|
||||||
|
|
||||||
Return a coroutine.
|
Return a coroutine.
|
||||||
@ -24,7 +41,9 @@ class APIAuth(CoreSysAttributes):
|
|||||||
auth = BasicAuth.decode(request.headers[AUTHORIZATION])
|
auth = BasicAuth.decode(request.headers[AUTHORIZATION])
|
||||||
return self.sys_auth.check_login(addon, auth.login, auth.password)
|
return self.sys_auth.check_login(addon, auth.login, auth.password)
|
||||||
|
|
||||||
def _process_dict(self, request, addon, data):
|
def _process_dict(
|
||||||
|
self, request: web.Request, addon: Addon, data: Dict[str, str]
|
||||||
|
) -> bool:
|
||||||
"""Process login with dict data.
|
"""Process login with dict data.
|
||||||
|
|
||||||
Return a coroutine.
|
Return a coroutine.
|
||||||
@ -35,7 +54,7 @@ class APIAuth(CoreSysAttributes):
|
|||||||
return self.sys_auth.check_login(addon, username, password)
|
return self.sys_auth.check_login(addon, username, password)
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def auth(self, request):
|
async def auth(self, request: web.Request) -> bool:
|
||||||
"""Process login request."""
|
"""Process login request."""
|
||||||
addon = request[REQUEST_FROM]
|
addon = request[REQUEST_FROM]
|
||||||
|
|
||||||
@ -59,3 +78,11 @@ class APIAuth(CoreSysAttributes):
|
|||||||
raise HTTPUnauthorized(
|
raise HTTPUnauthorized(
|
||||||
headers={WWW_AUTHENTICATE: 'Basic realm="Hass.io Authentication"'}
|
headers={WWW_AUTHENTICATE: 'Basic realm="Hass.io Authentication"'}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def reset(self, request: web.Request) -> None:
|
||||||
|
"""Process reset password request."""
|
||||||
|
body: Dict[str, str] = await api_validate(SCHEMA_PASSWORD_RESET, request)
|
||||||
|
await asyncio.shield(
|
||||||
|
self.sys_auth.change_password(body[ATTR_USERNAME], body[ATTR_PASSWORD])
|
||||||
|
)
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
"""Manage SSO for Add-ons with Home Assistant user."""
|
"""Manage SSO for Add-ons with Home Assistant user."""
|
||||||
import logging
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import logging
|
||||||
|
|
||||||
from .const import FILE_HASSIO_AUTH, ATTR_PASSWORD, ATTR_USERNAME, ATTR_ADDON
|
from .addons.addon import Addon
|
||||||
from .coresys import CoreSysAttributes
|
from .const import ATTR_ADDON, ATTR_PASSWORD, ATTR_USERNAME, FILE_HASSIO_AUTH
|
||||||
|
from .coresys import CoreSys, CoreSysAttributes
|
||||||
|
from .exceptions import AuthError, AuthPasswordResetError, HomeAssistantAPIError
|
||||||
from .utils.json import JsonConfig
|
from .utils.json import JsonConfig
|
||||||
from .validate import SCHEMA_AUTH_CONFIG
|
from .validate import SCHEMA_AUTH_CONFIG
|
||||||
from .exceptions import AuthError, HomeAssistantAPIError
|
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -14,15 +15,15 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
|
|||||||
class Auth(JsonConfig, CoreSysAttributes):
|
class Auth(JsonConfig, CoreSysAttributes):
|
||||||
"""Manage SSO for Add-ons with Home Assistant user."""
|
"""Manage SSO for Add-ons with Home Assistant user."""
|
||||||
|
|
||||||
def __init__(self, coresys):
|
def __init__(self, coresys: CoreSys) -> None:
|
||||||
"""Initialize updater."""
|
"""Initialize updater."""
|
||||||
super().__init__(FILE_HASSIO_AUTH, SCHEMA_AUTH_CONFIG)
|
super().__init__(FILE_HASSIO_AUTH, SCHEMA_AUTH_CONFIG)
|
||||||
self.coresys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
|
|
||||||
def _check_cache(self, username, password):
|
def _check_cache(self, username: str, password: str) -> bool:
|
||||||
"""Check password in cache."""
|
"""Check password in cache."""
|
||||||
username_h = _rehash(username)
|
username_h = self._rehash(username)
|
||||||
password_h = _rehash(password, username)
|
password_h = self._rehash(password, username)
|
||||||
|
|
||||||
if self._data.get(username_h) == password_h:
|
if self._data.get(username_h) == password_h:
|
||||||
_LOGGER.info("Cache hit for %s", username)
|
_LOGGER.info("Cache hit for %s", username)
|
||||||
@ -31,10 +32,10 @@ class Auth(JsonConfig, CoreSysAttributes):
|
|||||||
_LOGGER.warning("No cache hit for %s", username)
|
_LOGGER.warning("No cache hit for %s", username)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _update_cache(self, username, password):
|
def _update_cache(self, username: str, password: str) -> None:
|
||||||
"""Cache a username, password."""
|
"""Cache a username, password."""
|
||||||
username_h = _rehash(username)
|
username_h = self._rehash(username)
|
||||||
password_h = _rehash(password, username)
|
password_h = self._rehash(password, username)
|
||||||
|
|
||||||
if self._data.get(username_h) == password_h:
|
if self._data.get(username_h) == password_h:
|
||||||
return
|
return
|
||||||
@ -42,10 +43,10 @@ class Auth(JsonConfig, CoreSysAttributes):
|
|||||||
self._data[username_h] = password_h
|
self._data[username_h] = password_h
|
||||||
self.save_data()
|
self.save_data()
|
||||||
|
|
||||||
def _dismatch_cache(self, username, password):
|
def _dismatch_cache(self, username: str, password: str) -> None:
|
||||||
"""Remove user from cache."""
|
"""Remove user from cache."""
|
||||||
username_h = _rehash(username)
|
username_h = self._rehash(username)
|
||||||
password_h = _rehash(password, username)
|
password_h = self._rehash(password, username)
|
||||||
|
|
||||||
if self._data.get(username_h) != password_h:
|
if self._data.get(username_h) != password_h:
|
||||||
return
|
return
|
||||||
@ -53,7 +54,7 @@ class Auth(JsonConfig, CoreSysAttributes):
|
|||||||
self._data.pop(username_h, None)
|
self._data.pop(username_h, None)
|
||||||
self.save_data()
|
self.save_data()
|
||||||
|
|
||||||
async def check_login(self, addon, username, password):
|
async def check_login(self, addon: Addon, username: str, password: str) -> bool:
|
||||||
"""Check username login."""
|
"""Check username login."""
|
||||||
if password is None:
|
if password is None:
|
||||||
_LOGGER.error("None as password is not supported!")
|
_LOGGER.error("None as password is not supported!")
|
||||||
@ -89,8 +90,26 @@ class Auth(JsonConfig, CoreSysAttributes):
|
|||||||
|
|
||||||
raise AuthError()
|
raise AuthError()
|
||||||
|
|
||||||
|
async def change_password(self, username: str, password: str) -> None:
|
||||||
|
"""Change user password login."""
|
||||||
|
try:
|
||||||
|
async with self.sys_homeassistant.make_request(
|
||||||
|
"post",
|
||||||
|
"api/hassio_auth/password_reset",
|
||||||
|
json={ATTR_USERNAME: username, ATTR_PASSWORD: password},
|
||||||
|
) as req:
|
||||||
|
if req.status == 200:
|
||||||
|
_LOGGER.info("Success password reset %s", username)
|
||||||
|
return
|
||||||
|
|
||||||
def _rehash(value, salt2=""):
|
_LOGGER.warning("Unknown user %s for password reset", username)
|
||||||
|
except HomeAssistantAPIError:
|
||||||
|
_LOGGER.error("Can't request password reset on Home Assistant!")
|
||||||
|
|
||||||
|
raise AuthPasswordResetError()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _rehash(value: str, salt2: str = "") -> str:
|
||||||
"""Rehash a value."""
|
"""Rehash a value."""
|
||||||
for idx in range(1, 20):
|
for idx in range(1, 20):
|
||||||
value = hashlib.sha256(f"{value}{idx}{salt2}".encode()).hexdigest()
|
value = hashlib.sha256(f"{value}{idx}{salt2}".encode()).hexdigest()
|
||||||
|
@ -97,6 +97,10 @@ class AuthError(HassioError):
|
|||||||
"""Auth errors."""
|
"""Auth errors."""
|
||||||
|
|
||||||
|
|
||||||
|
class AuthPasswordResetError(HassioError):
|
||||||
|
"""Auth error if password reset fails."""
|
||||||
|
|
||||||
|
|
||||||
# Host
|
# Host
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user