2019-01-03 21:28:43 +00:00
|
|
|
"""Support for Alexa skill auth."""
|
|
|
|
import asyncio
|
2019-12-08 13:56:42 +00:00
|
|
|
from datetime import timedelta
|
2021-10-22 12:21:34 +00:00
|
|
|
from http import HTTPStatus
|
2019-01-03 21:28:43 +00:00
|
|
|
import json
|
|
|
|
import logging
|
2019-12-08 13:56:42 +00:00
|
|
|
|
2019-01-03 21:28:43 +00:00
|
|
|
import aiohttp
|
|
|
|
import async_timeout
|
|
|
|
|
2021-10-22 12:21:34 +00:00
|
|
|
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
2019-01-03 21:28:43 +00:00
|
|
|
from homeassistant.core import callback
|
|
|
|
from homeassistant.helpers import aiohttp_client
|
|
|
|
from homeassistant.util import dt
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
LWA_TOKEN_URI = "https://api.amazon.com/auth/o2/token"
|
2019-07-31 19:25:30 +00:00
|
|
|
LWA_HEADERS = {"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"}
|
2019-01-03 21:28:43 +00:00
|
|
|
|
|
|
|
PREEMPTIVE_REFRESH_TTL_IN_SECONDS = 300
|
2019-07-31 19:25:30 +00:00
|
|
|
STORAGE_KEY = "alexa_auth"
|
2019-01-03 21:28:43 +00:00
|
|
|
STORAGE_VERSION = 1
|
|
|
|
STORAGE_EXPIRE_TIME = "expire_time"
|
|
|
|
STORAGE_ACCESS_TOKEN = "access_token"
|
|
|
|
STORAGE_REFRESH_TOKEN = "refresh_token"
|
|
|
|
|
|
|
|
|
|
|
|
class Auth:
|
|
|
|
"""Handle authentication to send events to Alexa."""
|
|
|
|
|
|
|
|
def __init__(self, hass, client_id, client_secret):
|
|
|
|
"""Initialize the Auth class."""
|
|
|
|
self.hass = hass
|
|
|
|
|
|
|
|
self.client_id = client_id
|
|
|
|
self.client_secret = client_secret
|
|
|
|
|
|
|
|
self._prefs = None
|
|
|
|
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
|
|
|
|
2019-05-23 04:09:59 +00:00
|
|
|
self._get_token_lock = asyncio.Lock()
|
2019-01-03 21:28:43 +00:00
|
|
|
|
|
|
|
async def async_do_auth(self, accept_grant_code):
|
|
|
|
"""Do authentication with an AcceptGrant code."""
|
|
|
|
# access token not retrieved yet for the first time, so this should
|
|
|
|
# be an access token request
|
|
|
|
|
|
|
|
lwa_params = {
|
|
|
|
"grant_type": "authorization_code",
|
|
|
|
"code": accept_grant_code,
|
2020-05-30 15:27:20 +00:00
|
|
|
CONF_CLIENT_ID: self.client_id,
|
|
|
|
CONF_CLIENT_SECRET: self.client_secret,
|
2019-01-03 21:28:43 +00:00
|
|
|
}
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug(
|
2020-01-02 19:17:10 +00:00
|
|
|
"Calling LWA to get the access token (first time), with: %s",
|
2019-07-31 19:25:30 +00:00
|
|
|
json.dumps(lwa_params),
|
|
|
|
)
|
2019-01-03 21:28:43 +00:00
|
|
|
|
|
|
|
return await self._async_request_new_token(lwa_params)
|
|
|
|
|
2019-08-31 01:34:40 +00:00
|
|
|
@callback
|
|
|
|
def async_invalidate_access_token(self):
|
|
|
|
"""Invalidate access token."""
|
|
|
|
self._prefs[STORAGE_ACCESS_TOKEN] = None
|
|
|
|
|
2019-01-03 21:28:43 +00:00
|
|
|
async def async_get_access_token(self):
|
|
|
|
"""Perform access token or token refresh request."""
|
|
|
|
async with self._get_token_lock:
|
|
|
|
if self._prefs is None:
|
|
|
|
await self.async_load_preferences()
|
|
|
|
|
|
|
|
if self.is_token_valid():
|
2020-07-05 21:04:19 +00:00
|
|
|
_LOGGER.debug("Token still valid, using it")
|
2019-01-03 21:28:43 +00:00
|
|
|
return self._prefs[STORAGE_ACCESS_TOKEN]
|
|
|
|
|
|
|
|
if self._prefs[STORAGE_REFRESH_TOKEN] is None:
|
2020-07-05 21:04:19 +00:00
|
|
|
_LOGGER.debug("Token invalid and no refresh token available")
|
2019-01-03 21:28:43 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
lwa_params = {
|
|
|
|
"grant_type": "refresh_token",
|
|
|
|
"refresh_token": self._prefs[STORAGE_REFRESH_TOKEN],
|
2020-05-30 15:27:20 +00:00
|
|
|
CONF_CLIENT_ID: self.client_id,
|
|
|
|
CONF_CLIENT_SECRET: self.client_secret,
|
2019-01-03 21:28:43 +00:00
|
|
|
}
|
|
|
|
|
2020-07-05 21:04:19 +00:00
|
|
|
_LOGGER.debug("Calling LWA to refresh the access token")
|
2019-01-03 21:28:43 +00:00
|
|
|
return await self._async_request_new_token(lwa_params)
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def is_token_valid(self):
|
|
|
|
"""Check if a token is already loaded and if it is still valid."""
|
|
|
|
if not self._prefs[STORAGE_ACCESS_TOKEN]:
|
|
|
|
return False
|
|
|
|
|
|
|
|
expire_time = dt.parse_datetime(self._prefs[STORAGE_EXPIRE_TIME])
|
|
|
|
preemptive_expire_time = expire_time - timedelta(
|
2019-07-31 19:25:30 +00:00
|
|
|
seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS
|
|
|
|
)
|
2019-01-03 21:28:43 +00:00
|
|
|
|
|
|
|
return dt.utcnow() < preemptive_expire_time
|
|
|
|
|
|
|
|
async def _async_request_new_token(self, lwa_params):
|
|
|
|
|
|
|
|
try:
|
|
|
|
session = aiohttp_client.async_get_clientsession(self.hass)
|
2021-11-04 15:07:50 +00:00
|
|
|
async with async_timeout.timeout(10):
|
2019-07-31 19:25:30 +00:00
|
|
|
response = await session.post(
|
|
|
|
LWA_TOKEN_URI,
|
|
|
|
headers=LWA_HEADERS,
|
|
|
|
data=lwa_params,
|
|
|
|
allow_redirects=True,
|
|
|
|
)
|
2019-01-03 21:28:43 +00:00
|
|
|
|
|
|
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
2020-07-05 21:04:19 +00:00
|
|
|
_LOGGER.error("Timeout calling LWA to get auth token")
|
2019-01-03 21:28:43 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
_LOGGER.debug("LWA response header: %s", response.headers)
|
|
|
|
_LOGGER.debug("LWA response status: %s", response.status)
|
|
|
|
|
2021-10-22 12:21:34 +00:00
|
|
|
if response.status != HTTPStatus.OK:
|
2020-07-05 21:04:19 +00:00
|
|
|
_LOGGER.error("Error calling LWA to get auth token")
|
2019-01-03 21:28:43 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
response_json = await response.json()
|
|
|
|
_LOGGER.debug("LWA response body : %s", response_json)
|
|
|
|
|
|
|
|
access_token = response_json["access_token"]
|
|
|
|
refresh_token = response_json["refresh_token"]
|
|
|
|
expires_in = response_json["expires_in"]
|
|
|
|
expire_time = dt.utcnow() + timedelta(seconds=expires_in)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
await self._async_update_preferences(
|
|
|
|
access_token, refresh_token, expire_time.isoformat()
|
|
|
|
)
|
2019-01-03 21:28:43 +00:00
|
|
|
|
|
|
|
return access_token
|
|
|
|
|
|
|
|
async def async_load_preferences(self):
|
|
|
|
"""Load preferences with stored tokens."""
|
|
|
|
self._prefs = await self._store.async_load()
|
|
|
|
|
|
|
|
if self._prefs is None:
|
|
|
|
self._prefs = {
|
|
|
|
STORAGE_ACCESS_TOKEN: None,
|
|
|
|
STORAGE_REFRESH_TOKEN: None,
|
2019-07-31 19:25:30 +00:00
|
|
|
STORAGE_EXPIRE_TIME: None,
|
2019-01-03 21:28:43 +00:00
|
|
|
}
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def _async_update_preferences(self, access_token, refresh_token, expire_time):
|
2019-01-03 21:28:43 +00:00
|
|
|
"""Update user preferences."""
|
|
|
|
if self._prefs is None:
|
|
|
|
await self.async_load_preferences()
|
|
|
|
|
|
|
|
if access_token is not None:
|
|
|
|
self._prefs[STORAGE_ACCESS_TOKEN] = access_token
|
|
|
|
if refresh_token is not None:
|
|
|
|
self._prefs[STORAGE_REFRESH_TOKEN] = refresh_token
|
|
|
|
if expire_time is not None:
|
|
|
|
self._prefs[STORAGE_EXPIRE_TIME] = expire_time
|
|
|
|
await self._store.async_save(self._prefs)
|