2016-11-25 21:04:06 +00:00
|
|
|
"""Authentication for HTTP component."""
|
2018-03-09 01:51:49 +00:00
|
|
|
|
2017-09-28 07:49:35 +00:00
|
|
|
import base64
|
2016-11-25 21:04:06 +00:00
|
|
|
import hmac
|
|
|
|
import logging
|
|
|
|
|
2017-09-28 07:49:35 +00:00
|
|
|
from aiohttp import hdrs
|
2017-11-06 02:42:31 +00:00
|
|
|
from aiohttp.web import middleware
|
2018-10-25 14:44:57 +00:00
|
|
|
import jwt
|
2017-09-28 07:49:35 +00:00
|
|
|
|
2018-02-15 21:06:14 +00:00
|
|
|
from homeassistant.core import callback
|
2016-11-25 21:04:06 +00:00
|
|
|
from homeassistant.const import HTTP_HEADER_HA_AUTH
|
2018-11-27 09:41:44 +00:00
|
|
|
from homeassistant.auth.providers import legacy_api_password
|
2018-10-25 14:44:57 +00:00
|
|
|
from homeassistant.auth.util import generate_secret
|
|
|
|
from homeassistant.util import dt as dt_util
|
|
|
|
|
2018-02-15 21:06:14 +00:00
|
|
|
from .const import KEY_AUTHENTICATED, KEY_REAL_IP
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
DATA_API_PASSWORD = 'api_password'
|
2018-10-25 14:44:57 +00:00
|
|
|
DATA_SIGN_SECRET = 'http.auth.sign_secret'
|
|
|
|
SIGN_QUERY_PARAM = 'authSig'
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2018-10-25 14:44:57 +00:00
|
|
|
@callback
|
|
|
|
def async_sign_path(hass, refresh_token_id, path, expiration):
|
|
|
|
"""Sign a path for temporary access without auth header."""
|
|
|
|
secret = hass.data.get(DATA_SIGN_SECRET)
|
|
|
|
|
|
|
|
if secret is None:
|
|
|
|
secret = hass.data[DATA_SIGN_SECRET] = generate_secret()
|
|
|
|
|
|
|
|
now = dt_util.utcnow()
|
|
|
|
return "{}?{}={}".format(path, SIGN_QUERY_PARAM, jwt.encode({
|
|
|
|
'iss': refresh_token_id,
|
|
|
|
'path': path,
|
|
|
|
'iat': now,
|
|
|
|
'exp': now + expiration,
|
|
|
|
}, secret, algorithm='HS256').decode())
|
|
|
|
|
|
|
|
|
2018-02-15 21:06:14 +00:00
|
|
|
@callback
|
2018-12-02 15:32:53 +00:00
|
|
|
def setup_auth(app, trusted_networks, api_password):
|
2018-02-15 21:06:14 +00:00
|
|
|
"""Create auth middleware for the app."""
|
2018-07-27 13:53:46 +00:00
|
|
|
old_auth_warning = set()
|
|
|
|
|
2018-02-15 21:06:14 +00:00
|
|
|
@middleware
|
2018-03-09 01:51:49 +00:00
|
|
|
async def auth_middleware(request, handler):
|
2018-02-15 21:06:14 +00:00
|
|
|
"""Authenticate as middleware."""
|
|
|
|
authenticated = False
|
|
|
|
|
2018-12-02 15:32:53 +00:00
|
|
|
if (HTTP_HEADER_HA_AUTH in request.headers or
|
|
|
|
DATA_API_PASSWORD in request.query):
|
2018-07-27 13:53:46 +00:00
|
|
|
if request.path not in old_auth_warning:
|
2018-08-25 05:57:36 +00:00
|
|
|
_LOGGER.log(
|
2018-12-02 15:32:53 +00:00
|
|
|
logging.INFO if api_password else logging.WARNING,
|
2018-08-27 08:37:03 +00:00
|
|
|
'You need to use a bearer token to access %s from %s',
|
2018-08-25 05:57:36 +00:00
|
|
|
request.path, request[KEY_REAL_IP])
|
2018-07-27 13:53:46 +00:00
|
|
|
old_auth_warning.add(request.path)
|
2018-07-01 02:31:36 +00:00
|
|
|
|
|
|
|
if (hdrs.AUTHORIZATION in request.headers and
|
2018-12-02 15:32:53 +00:00
|
|
|
await async_validate_auth_header(request, api_password)):
|
2018-07-01 02:31:36 +00:00
|
|
|
# it included both use_auth and api_password Basic auth
|
|
|
|
authenticated = True
|
|
|
|
|
2018-10-25 14:44:57 +00:00
|
|
|
# We first start with a string check to avoid parsing query params
|
|
|
|
# for every request.
|
|
|
|
elif (request.method == "GET" and SIGN_QUERY_PARAM in request.query and
|
|
|
|
await async_validate_signed_request(request)):
|
|
|
|
authenticated = True
|
|
|
|
|
2018-12-02 15:32:53 +00:00
|
|
|
elif (api_password and HTTP_HEADER_HA_AUTH in request.headers and
|
2018-07-01 02:31:36 +00:00
|
|
|
hmac.compare_digest(
|
|
|
|
api_password.encode('utf-8'),
|
|
|
|
request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))):
|
2018-02-15 21:06:14 +00:00
|
|
|
# A valid auth header has been set
|
|
|
|
authenticated = True
|
2018-11-27 09:41:44 +00:00
|
|
|
request['hass_user'] = await legacy_api_password.async_get_user(
|
|
|
|
app['hass'])
|
2018-02-15 21:06:14 +00:00
|
|
|
|
2018-12-02 15:32:53 +00:00
|
|
|
elif (api_password and DATA_API_PASSWORD in request.query and
|
2018-05-01 16:20:41 +00:00
|
|
|
hmac.compare_digest(
|
|
|
|
api_password.encode('utf-8'),
|
|
|
|
request.query[DATA_API_PASSWORD].encode('utf-8'))):
|
2018-02-15 21:06:14 +00:00
|
|
|
authenticated = True
|
2018-11-27 09:41:44 +00:00
|
|
|
request['hass_user'] = await legacy_api_password.async_get_user(
|
|
|
|
app['hass'])
|
2018-02-15 21:06:14 +00:00
|
|
|
|
2018-07-01 02:31:36 +00:00
|
|
|
elif _is_trusted_ip(request, trusted_networks):
|
2018-11-30 16:32:47 +00:00
|
|
|
users = await app['hass'].auth.async_get_users()
|
|
|
|
for user in users:
|
|
|
|
if user.is_owner:
|
|
|
|
request['hass_user'] = user
|
|
|
|
break
|
2018-02-15 21:06:14 +00:00
|
|
|
authenticated = True
|
|
|
|
|
|
|
|
request[KEY_AUTHENTICATED] = authenticated
|
2018-03-09 01:51:49 +00:00
|
|
|
return await handler(request)
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2018-11-21 19:55:21 +00:00
|
|
|
app.middlewares.append(auth_middleware)
|
2017-09-28 07:49:35 +00:00
|
|
|
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2018-02-15 21:06:14 +00:00
|
|
|
def _is_trusted_ip(request, trusted_networks):
|
2016-11-25 21:04:06 +00:00
|
|
|
"""Test if request is from a trusted ip."""
|
2018-02-15 21:06:14 +00:00
|
|
|
ip_addr = request[KEY_REAL_IP]
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2018-02-15 21:06:14 +00:00
|
|
|
return any(
|
2016-11-25 21:04:06 +00:00
|
|
|
ip_addr in trusted_network for trusted_network
|
2018-02-15 21:06:14 +00:00
|
|
|
in trusted_networks)
|
2016-11-27 02:23:28 +00:00
|
|
|
|
|
|
|
|
|
|
|
def validate_password(request, api_password):
|
|
|
|
"""Test if password is valid."""
|
2017-04-30 05:04:49 +00:00
|
|
|
return hmac.compare_digest(
|
2018-05-01 16:20:41 +00:00
|
|
|
api_password.encode('utf-8'),
|
|
|
|
request.app['hass'].http.api_password.encode('utf-8'))
|
2017-09-28 07:49:35 +00:00
|
|
|
|
|
|
|
|
2018-07-01 02:31:36 +00:00
|
|
|
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.
|
|
|
|
"""
|
2017-09-28 07:49:35 +00:00
|
|
|
if hdrs.AUTHORIZATION not in request.headers:
|
|
|
|
return False
|
|
|
|
|
2018-05-10 08:38:11 +00:00
|
|
|
try:
|
|
|
|
auth_type, auth_val = \
|
|
|
|
request.headers.get(hdrs.AUTHORIZATION).split(' ', 1)
|
|
|
|
except ValueError:
|
|
|
|
# If no space in authorization header
|
|
|
|
return False
|
2017-09-28 07:49:35 +00:00
|
|
|
|
2018-11-30 16:32:47 +00:00
|
|
|
hass = request.app['hass']
|
|
|
|
|
2018-07-01 02:31:36 +00:00
|
|
|
if auth_type == 'Bearer':
|
2018-08-14 19:14:12 +00:00
|
|
|
refresh_token = await hass.auth.async_validate_access_token(auth_val)
|
|
|
|
if refresh_token is None:
|
2018-07-01 02:31:36 +00:00
|
|
|
return False
|
|
|
|
|
2018-10-01 14:09:31 +00:00
|
|
|
request['hass_refresh_token'] = refresh_token
|
2018-08-14 19:14:12 +00:00
|
|
|
request['hass_user'] = refresh_token.user
|
2018-07-01 02:31:36 +00:00
|
|
|
return True
|
|
|
|
|
2018-07-23 08:16:05 +00:00
|
|
|
if auth_type == 'Basic' and api_password is not None:
|
2018-05-01 16:20:41 +00:00
|
|
|
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
|
2017-09-28 07:49:35 +00:00
|
|
|
|
2018-11-30 16:32:47 +00:00
|
|
|
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
|
2018-05-01 16:20:41 +00:00
|
|
|
|
2018-07-23 08:16:05 +00:00
|
|
|
return False
|
2018-10-25 14:44:57 +00:00
|
|
|
|
|
|
|
|
|
|
|
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
|