Password reset (#1433)

* API to reset password

* Fix error handling

* fix lint

* fix typing

* fix await
This commit is contained in:
Pascal Vizeli 2020-01-15 18:16:19 +01:00 committed by GitHub
parent 9d6f4f5392
commit 69959b2c97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 90 additions and 29 deletions

9
API.md
View File

@ -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"
}
```

View File

@ -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."""

View File

@ -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])
)

View File

@ -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

View File

@ -97,6 +97,10 @@ class AuthError(HassioError):
"""Auth errors."""
class AuthPasswordResetError(HassioError):
"""Auth error if password reset fails."""
# Host