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": "..." }`
|
||||
- application/x-www-form-urlencoded `user|name=...&password=...`
|
||||
- BasicAuth
|
||||
|
||||
* POST `/auth/reset`
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "xy",
|
||||
"password": "new-password"
|
||||
}
|
||||
```
|
||||
|
|
|
@ -121,7 +121,9 @@ class RestAPI(CoreSysAttributes):
|
|||
api_auth = APIAuth()
|
||||
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:
|
||||
"""Register Supervisor functions."""
|
||||
|
|
|
@ -1,22 +1,39 @@
|
|||
"""Init file for Hass.io auth/SSO RESTful API."""
|
||||
import asyncio
|
||||
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.hdrs import CONTENT_TYPE, AUTHORIZATION, WWW_AUTHENTICATE
|
||||
import voluptuous as vol
|
||||
|
||||
from .utils import api_process
|
||||
from ..const import REQUEST_FROM, CONTENT_TYPE_JSON, CONTENT_TYPE_URL
|
||||
from ..addons.addon import Addon
|
||||
from ..const import (
|
||||
ATTR_PASSWORD,
|
||||
ATTR_USERNAME,
|
||||
CONTENT_TYPE_JSON,
|
||||
CONTENT_TYPE_URL,
|
||||
REQUEST_FROM,
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import APIForbidden
|
||||
from .utils import api_process, api_validate
|
||||
|
||||
_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):
|
||||
"""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.
|
||||
|
||||
Return a coroutine.
|
||||
|
@ -24,7 +41,9 @@ class APIAuth(CoreSysAttributes):
|
|||
auth = BasicAuth.decode(request.headers[AUTHORIZATION])
|
||||
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.
|
||||
|
||||
Return a coroutine.
|
||||
|
@ -35,7 +54,7 @@ class APIAuth(CoreSysAttributes):
|
|||
return self.sys_auth.check_login(addon, username, password)
|
||||
|
||||
@api_process
|
||||
async def auth(self, request):
|
||||
async def auth(self, request: web.Request) -> bool:
|
||||
"""Process login request."""
|
||||
addon = request[REQUEST_FROM]
|
||||
|
||||
|
@ -59,3 +78,11 @@ class APIAuth(CoreSysAttributes):
|
|||
raise HTTPUnauthorized(
|
||||
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."""
|
||||
import logging
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from .const import FILE_HASSIO_AUTH, ATTR_PASSWORD, ATTR_USERNAME, ATTR_ADDON
|
||||
from .coresys import CoreSysAttributes
|
||||
from .addons.addon import Addon
|
||||
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 .validate import SCHEMA_AUTH_CONFIG
|
||||
from .exceptions import AuthError, HomeAssistantAPIError
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -14,15 +15,15 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
|
|||
class Auth(JsonConfig, CoreSysAttributes):
|
||||
"""Manage SSO for Add-ons with Home Assistant user."""
|
||||
|
||||
def __init__(self, coresys):
|
||||
def __init__(self, coresys: CoreSys) -> None:
|
||||
"""Initialize updater."""
|
||||
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."""
|
||||
username_h = _rehash(username)
|
||||
password_h = _rehash(password, username)
|
||||
username_h = self._rehash(username)
|
||||
password_h = self._rehash(password, username)
|
||||
|
||||
if self._data.get(username_h) == password_h:
|
||||
_LOGGER.info("Cache hit for %s", username)
|
||||
|
@ -31,10 +32,10 @@ class Auth(JsonConfig, CoreSysAttributes):
|
|||
_LOGGER.warning("No cache hit for %s", username)
|
||||
return False
|
||||
|
||||
def _update_cache(self, username, password):
|
||||
def _update_cache(self, username: str, password: str) -> None:
|
||||
"""Cache a username, password."""
|
||||
username_h = _rehash(username)
|
||||
password_h = _rehash(password, username)
|
||||
username_h = self._rehash(username)
|
||||
password_h = self._rehash(password, username)
|
||||
|
||||
if self._data.get(username_h) == password_h:
|
||||
return
|
||||
|
@ -42,10 +43,10 @@ class Auth(JsonConfig, CoreSysAttributes):
|
|||
self._data[username_h] = password_h
|
||||
self.save_data()
|
||||
|
||||
def _dismatch_cache(self, username, password):
|
||||
def _dismatch_cache(self, username: str, password: str) -> None:
|
||||
"""Remove user from cache."""
|
||||
username_h = _rehash(username)
|
||||
password_h = _rehash(password, username)
|
||||
username_h = self._rehash(username)
|
||||
password_h = self._rehash(password, username)
|
||||
|
||||
if self._data.get(username_h) != password_h:
|
||||
return
|
||||
|
@ -53,7 +54,7 @@ class Auth(JsonConfig, CoreSysAttributes):
|
|||
self._data.pop(username_h, None)
|
||||
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."""
|
||||
if password is None:
|
||||
_LOGGER.error("None as password is not supported!")
|
||||
|
@ -89,9 +90,27 @@ class Auth(JsonConfig, CoreSysAttributes):
|
|||
|
||||
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=""):
|
||||
"""Rehash a value."""
|
||||
for idx in range(1, 20):
|
||||
value = hashlib.sha256(f"{value}{idx}{salt2}".encode()).hexdigest()
|
||||
return value
|
||||
_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."""
|
||||
for idx in range(1, 20):
|
||||
value = hashlib.sha256(f"{value}{idx}{salt2}".encode()).hexdigest()
|
||||
return value
|
||||
|
|
|
@ -97,6 +97,10 @@ class AuthError(HassioError):
|
|||
"""Auth errors."""
|
||||
|
||||
|
||||
class AuthPasswordResetError(HassioError):
|
||||
"""Auth error if password reset fails."""
|
||||
|
||||
|
||||
# Host
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue