core/homeassistant/components/http/auth.py

184 lines
5.3 KiB
Python
Raw Normal View History

"""Authentication for HTTP component."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from datetime import timedelta
2021-11-29 22:01:03 +00:00
from ipaddress import ip_address
import logging
import secrets
from typing import Final
from urllib.parse import unquote
from aiohttp import hdrs
from aiohttp.web import Application, Request, StreamResponse, middleware
import jwt
2021-11-29 22:01:03 +00:00
from homeassistant.auth.models import User
from homeassistant.core import HomeAssistant, callback
from homeassistant.util import dt as dt_util
2021-11-29 22:01:03 +00:00
from homeassistant.util.network import is_local
from .const import KEY_AUTHENTICATED, KEY_HASS_REFRESH_TOKEN_ID, KEY_HASS_USER
2021-11-29 22:01:03 +00:00
from .request_context import current_request
_LOGGER = logging.getLogger(__name__)
DATA_API_PASSWORD: Final = "api_password"
DATA_SIGN_SECRET: Final = "http.auth.sign_secret"
SIGN_QUERY_PARAM: Final = "authSig"
@callback
def async_sign_path(
hass: HomeAssistant, refresh_token_id: str, path: str, expiration: timedelta
) -> str:
"""Sign a path for temporary access without auth header."""
2021-10-17 18:15:48 +00:00
if (secret := hass.data.get(DATA_SIGN_SECRET)) is None:
secret = hass.data[DATA_SIGN_SECRET] = secrets.token_hex()
now = dt_util.utcnow()
encoded = jwt.encode(
{
"iss": refresh_token_id,
"path": unquote(path),
"iat": now,
"exp": now + expiration,
},
secret,
algorithm="HS256",
2019-07-31 19:25:30 +00:00
)
2021-09-08 03:59:02 +00:00
return f"{path}?{SIGN_QUERY_PARAM}={encoded}"
2021-11-29 22:01:03 +00:00
@callback
def async_user_not_allowed_do_auth(
hass: HomeAssistant, user: User, request: Request | None = None
) -> str | None:
"""Validate that user is not allowed to do auth things."""
if not user.is_active:
return "User is not active"
if not user.local_only:
return None
# User is marked as local only, check if they are allowed to do auth
if request is None:
request = current_request.get()
if not request:
return "No request available to validate local access"
if "cloud" in hass.config.components:
# pylint: disable=import-outside-toplevel
from hass_nabucasa import remote
if remote.is_cloud_request.get():
return "User is local only"
try:
remote = ip_address(request.remote)
except ValueError:
return "Invalid remote IP"
if is_local(remote):
return None
return "User cannot authenticate remotely"
@callback
def setup_auth(hass: HomeAssistant, app: Application) -> None:
"""Create auth middleware for the app."""
async def async_validate_auth_header(request: Request) -> bool:
"""
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":
return False
refresh_token = await hass.auth.async_validate_access_token(auth_val)
if refresh_token is None:
return False
2021-11-29 22:01:03 +00:00
if async_user_not_allowed_do_auth(hass, refresh_token.user, request):
return False
request[KEY_HASS_USER] = refresh_token.user
request[KEY_HASS_REFRESH_TOKEN_ID] = refresh_token.id
return True
async def async_validate_signed_request(request: Request) -> bool:
"""Validate a signed request."""
2021-10-17 18:15:48 +00:00
if (secret := hass.data.get(DATA_SIGN_SECRET)) is None:
return False
2021-10-17 18:15:48 +00:00
if (signature := request.query.get(SIGN_QUERY_PARAM)) is None:
return False
try:
claims = jwt.decode(
2019-07-31 19:25:30 +00:00
signature, secret, algorithms=["HS256"], options={"verify_iss": False}
)
except jwt.InvalidTokenError:
return False
2019-07-31 19:25:30 +00:00
if claims["path"] != request.path:
return False
2019-07-31 19:25:30 +00:00
refresh_token = await hass.auth.async_get_refresh_token(claims["iss"])
2018-11-30 16:32:47 +00:00
if refresh_token is None:
return False
request[KEY_HASS_USER] = refresh_token.user
request[KEY_HASS_REFRESH_TOKEN_ID] = refresh_token.id
return True
@middleware
async def auth_middleware(
request: Request, handler: Callable[[Request], Awaitable[StreamResponse]]
) -> StreamResponse:
"""Authenticate as middleware."""
authenticated = False
2019-07-31 19:25:30 +00:00
if hdrs.AUTHORIZATION in request.headers and await async_validate_auth_header(
request
):
authenticated = True
auth_type = "bearer token"
# We first start with a string check to avoid parsing query params
# for every request.
2019-07-31 19:25:30 +00:00
elif (
request.method == "GET"
and SIGN_QUERY_PARAM in request.query
and await async_validate_signed_request(request)
):
authenticated = True
auth_type = "signed request"
if authenticated:
_LOGGER.debug(
"Authenticated %s for %s using %s",
request.remote,
request.path,
auth_type,
2019-07-31 19:25:30 +00:00
)
request[KEY_AUTHENTICATED] = authenticated
return await handler(request)
app.middlewares.append(auth_middleware)