Don't use keyset (#17984)
parent
a91d894132
commit
1e03f945b5
|
@ -4,20 +4,16 @@ Component to integrate the Home Assistant cloud.
|
||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/cloud/
|
https://home-assistant.io/components/cloud/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
import async_timeout
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME)
|
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME)
|
||||||
from homeassistant.helpers import entityfilter, config_validation as cv
|
from homeassistant.helpers import entityfilter, config_validation as cv
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.components.alexa import smart_home as alexa_sh
|
from homeassistant.components.alexa import smart_home as alexa_sh
|
||||||
from homeassistant.components.google_assistant import helpers as ga_h
|
from homeassistant.components.google_assistant import helpers as ga_h
|
||||||
|
@ -129,7 +125,6 @@ class Cloud:
|
||||||
self._google_actions = google_actions
|
self._google_actions = google_actions
|
||||||
self._gactions_config = None
|
self._gactions_config = None
|
||||||
self._prefs = None
|
self._prefs = None
|
||||||
self.jwt_keyset = None
|
|
||||||
self.id_token = None
|
self.id_token = None
|
||||||
self.access_token = None
|
self.access_token = None
|
||||||
self.refresh_token = None
|
self.refresh_token = None
|
||||||
|
@ -262,13 +257,6 @@ class Cloud:
|
||||||
}
|
}
|
||||||
self._prefs = prefs
|
self._prefs = prefs
|
||||||
|
|
||||||
success = await self._fetch_jwt_keyset()
|
|
||||||
|
|
||||||
# Fetching keyset can fail if internet is not up yet.
|
|
||||||
if not success:
|
|
||||||
self.hass.helpers.event.async_call_later(5, self.async_start)
|
|
||||||
return
|
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
"""Load config."""
|
"""Load config."""
|
||||||
# Ensure config dir exists
|
# Ensure config dir exists
|
||||||
|
@ -288,14 +276,6 @@ class Cloud:
|
||||||
if info is None:
|
if info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Validate tokens
|
|
||||||
try:
|
|
||||||
for token in 'id_token', 'access_token':
|
|
||||||
self._decode_claims(info[token])
|
|
||||||
except ValueError as err: # Raised when token is invalid
|
|
||||||
_LOGGER.warning("Found invalid token %s: %s", token, err)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.id_token = info['id_token']
|
self.id_token = info['id_token']
|
||||||
self.access_token = info['access_token']
|
self.access_token = info['access_token']
|
||||||
self.refresh_token = info['refresh_token']
|
self.refresh_token = info['refresh_token']
|
||||||
|
@ -311,49 +291,7 @@ class Cloud:
|
||||||
self._prefs[STORAGE_ENABLE_ALEXA] = alexa_enabled
|
self._prefs[STORAGE_ENABLE_ALEXA] = alexa_enabled
|
||||||
await self._store.async_save(self._prefs)
|
await self._store.async_save(self._prefs)
|
||||||
|
|
||||||
async def _fetch_jwt_keyset(self):
|
def _decode_claims(self, token): # pylint: disable=no-self-use
|
||||||
"""Fetch the JWT keyset for the Cognito instance."""
|
|
||||||
session = async_get_clientsession(self.hass)
|
|
||||||
url = ("https://cognito-idp.us-east-1.amazonaws.com/"
|
|
||||||
"{}/.well-known/jwks.json".format(self.user_pool_id))
|
|
||||||
|
|
||||||
try:
|
|
||||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
|
||||||
req = await session.get(url)
|
|
||||||
self.jwt_keyset = await req.json()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
|
|
||||||
_LOGGER.error("Error fetching Cognito keyset: %s", err)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _decode_claims(self, token):
|
|
||||||
"""Decode the claims in a token."""
|
"""Decode the claims in a token."""
|
||||||
from jose import jwt, exceptions as jose_exceptions
|
from jose import jwt
|
||||||
try:
|
return jwt.get_unverified_claims(token)
|
||||||
header = jwt.get_unverified_header(token)
|
|
||||||
except jose_exceptions.JWTError as err:
|
|
||||||
raise ValueError(str(err)) from None
|
|
||||||
kid = header.get('kid')
|
|
||||||
|
|
||||||
if kid is None:
|
|
||||||
raise ValueError("No kid in header")
|
|
||||||
|
|
||||||
# Locate the key for this kid
|
|
||||||
key = None
|
|
||||||
for key_dict in self.jwt_keyset['keys']:
|
|
||||||
if key_dict['kid'] == kid:
|
|
||||||
key = key_dict
|
|
||||||
break
|
|
||||||
if not key:
|
|
||||||
raise ValueError(
|
|
||||||
"Unable to locate kid ({}) in keyset".format(kid))
|
|
||||||
|
|
||||||
try:
|
|
||||||
return jwt.decode(
|
|
||||||
token, key, audience=self.cognito_client_id, options={
|
|
||||||
'verify_exp': False,
|
|
||||||
})
|
|
||||||
except jose_exceptions.JWTError as err:
|
|
||||||
raise ValueError(str(err)) from None
|
|
||||||
|
|
|
@ -32,8 +32,7 @@ def test_constructor_loads_info_from_constant():
|
||||||
'google_actions_sync_url': 'test-google_actions_sync_url',
|
'google_actions_sync_url': 'test-google_actions_sync_url',
|
||||||
'subscription_info_url': 'test-subscription-info-url'
|
'subscription_info_url': 'test-subscription-info-url'
|
||||||
}
|
}
|
||||||
}), patch('homeassistant.components.cloud.Cloud._fetch_jwt_keyset',
|
}):
|
||||||
return_value=mock_coro(True)):
|
|
||||||
result = yield from cloud.async_setup(hass, {
|
result = yield from cloud.async_setup(hass, {
|
||||||
'cloud': {cloud.CONF_MODE: 'beer'}
|
'cloud': {cloud.CONF_MODE: 'beer'}
|
||||||
})
|
})
|
||||||
|
@ -54,17 +53,15 @@ def test_constructor_loads_info_from_config():
|
||||||
"""Test non-dev mode loads info from SERVERS constant."""
|
"""Test non-dev mode loads info from SERVERS constant."""
|
||||||
hass = MagicMock(data={})
|
hass = MagicMock(data={})
|
||||||
|
|
||||||
with patch('homeassistant.components.cloud.Cloud._fetch_jwt_keyset',
|
result = yield from cloud.async_setup(hass, {
|
||||||
return_value=mock_coro(True)):
|
'cloud': {
|
||||||
result = yield from cloud.async_setup(hass, {
|
cloud.CONF_MODE: cloud.MODE_DEV,
|
||||||
'cloud': {
|
'cognito_client_id': 'test-cognito_client_id',
|
||||||
cloud.CONF_MODE: cloud.MODE_DEV,
|
'user_pool_id': 'test-user_pool_id',
|
||||||
'cognito_client_id': 'test-cognito_client_id',
|
'region': 'test-region',
|
||||||
'user_pool_id': 'test-user_pool_id',
|
'relayer': 'test-relayer',
|
||||||
'region': 'test-region',
|
}
|
||||||
'relayer': 'test-relayer',
|
})
|
||||||
}
|
|
||||||
})
|
|
||||||
assert result
|
assert result
|
||||||
|
|
||||||
cl = hass.data['cloud']
|
cl = hass.data['cloud']
|
||||||
|
@ -89,8 +86,6 @@ async def test_initialize_loads_info(mock_os, hass):
|
||||||
cl.iot.connect.return_value = mock_coro()
|
cl.iot.connect.return_value = mock_coro()
|
||||||
|
|
||||||
with patch('homeassistant.components.cloud.open', mopen, create=True), \
|
with patch('homeassistant.components.cloud.open', mopen, create=True), \
|
||||||
patch('homeassistant.components.cloud.Cloud._fetch_jwt_keyset',
|
|
||||||
return_value=mock_coro(True)), \
|
|
||||||
patch('homeassistant.components.cloud.Cloud._decode_claims'):
|
patch('homeassistant.components.cloud.Cloud._decode_claims'):
|
||||||
await cl.async_start(None)
|
await cl.async_start(None)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue