Skip to content

Commit

Permalink
Merge pull request #136 from NabuCasa/dev
Browse files Browse the repository at this point in the history
Release 0.32.1
  • Loading branch information
pvizeli authored Mar 6, 2020
2 parents c7f9c58 + b4907d2 commit 11bad81
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 107 deletions.
4 changes: 2 additions & 2 deletions hass_nabucasa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ async def fetch_subscription_info(self):

async def login(self, email: str, password: str) -> None:
"""Log a user in."""
with async_timeout.timeout(10):
await self.run_executor(self.auth.login, email, password)
with async_timeout.timeout(15):
await self.auth.async_login(email, password)
await self.start()

async def logout(self) -> None:
Expand Down
139 changes: 74 additions & 65 deletions hass_nabucasa/auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Package to communicate with the authentication API."""
import asyncio
from functools import partial
import logging
import random

Expand Down Expand Up @@ -65,17 +66,18 @@ def __init__(self, cloud):
self.cloud = cloud
self._refresh_task = None
self._session = boto3.session.Session()
self._request_lock = asyncio.Lock()

cloud.iot.register_on_connect(self.on_connect)
cloud.iot.register_on_disconnect(self.on_disconnect)

async def handle_token_refresh(self):
async def _async_handle_token_refresh(self):
"""Handle Cloud access token refresh."""
sleep_time = random.randint(2400, 3600)
while True:
try:
await asyncio.sleep(sleep_time)
await self.cloud.run_executor(self.renew_access_token)
await self.async_renew_access_token()
except CloudError as err:
_LOGGER.error("Can't refresh cloud token: %s", err)
except asyncio.CancelledError:
Expand All @@ -86,137 +88,144 @@ async def handle_token_refresh(self):

async def on_connect(self):
"""When the instance is connected."""
self._refresh_task = self.cloud.run_task(self.handle_token_refresh())
self._refresh_task = self.cloud.run_task(self._async_handle_token_refresh())

async def on_disconnect(self):
"""When the instance is disconnected."""
self._refresh_task.cancel()

def register(self, email, password):
async def async_register(self, email, password):
"""Register a new account."""
cognito = self._cognito()

try:
cognito.register(email, password)
async with self._request_lock:
await self.cloud.run_executor(cognito.register, email, password)

except ClientError as err:
raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()

def resend_email_confirm(self, email):
async def async_resend_email_confirm(self, email):
"""Resend email confirmation."""
cognito = self._cognito(username=email)

try:
cognito.client.resend_confirmation_code(
Username=email, ClientId=cognito.client_id
)
async with self._request_lock:
await self.cloud.run_executor(
partial(
cognito.client.resend_confirmation_code,
Username=email,
ClientId=cognito.client_id,
)
)
except ClientError as err:
raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()

def forgot_password(self, email):
async def async_forgot_password(self, email):
"""Initialize forgotten password flow."""
cognito = self._cognito(username=email)

try:
cognito.initiate_forgot_password()
async with self._request_lock:
await self.cloud.run_executor(cognito.initiate_forgot_password)

except ClientError as err:
raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()

def login(self, email, password):
async def async_login(self, email, password):
"""Log user in and fetch certificate."""
cognito = self._authenticate(email, password)
self.cloud.id_token = cognito.id_token
self.cloud.access_token = cognito.access_token
self.cloud.refresh_token = cognito.refresh_token
self.cloud.write_user_info()

async def async_check_token(self):
"""Check that the token is valid."""
try:
await self.cloud.run_executor(self._check_token)
except Unauthenticated as err:
_LOGGER.error("Unable to refresh token: %s", err)

self.cloud.client.user_message(
"cloud_subscription_expired", "Home Assistant Cloud", MESSAGE_AUTH_FAIL
)

# Don't await it because it could cancel this task
self.cloud.run_task(self.cloud.logout())
raise

def _check_token(self):
"""Check that the token is valid and verify if needed."""
cognito = self._cognito(
access_token=self.cloud.access_token, refresh_token=self.cloud.refresh_token
)
assert not self.cloud.is_logged_in, "Cannot login if already logged in."

cognito = self._cognito(username=email)
try:
if cognito.check_token():
async with self._request_lock:
await self.cloud.run_executor(
partial(cognito.authenticate, password=password)
)
self.cloud.id_token = cognito.id_token
self.cloud.access_token = cognito.access_token
self.cloud.write_user_info()
self.cloud.refresh_token = cognito.refresh_token
await self.cloud.run_executor(self.cloud.write_user_info)

except ForceChangePasswordException:
raise PasswordChangeRequired()

except ClientError as err:
raise _map_aws_exception(err)

except EndpointConnectionError:
raise UnknownError()

def renew_access_token(self):
"""Renew access token."""
cognito = self._cognito(
access_token=self.cloud.access_token, refresh_token=self.cloud.refresh_token
)
async def async_check_token(self):
"""Check that the token is valid."""
cognito = self._authenticated_cognito

try:
cognito.renew_access_token()
self.cloud.id_token = cognito.id_token
self.cloud.access_token = cognito.access_token
self.cloud.write_user_info()
async with self._request_lock:
if not cognito.check_token(renew=False):
return

except ClientError as err:
raise _map_aws_exception(err)
try:
await self._async_renew_access_token()
except Unauthenticated as err:
_LOGGER.error("Unable to refresh token: %s", err)

except EndpointConnectionError:
raise UnknownError()
self.cloud.client.user_message(
"cloud_subscription_expired",
"Home Assistant Cloud",
MESSAGE_AUTH_FAIL,
)

def _authenticate(self, email, password):
"""Log in and return an authenticated Cognito instance."""
assert not self.cloud.is_logged_in, "Cannot login if already logged in."
# Don't await it because it could cancel this task
self.cloud.run_task(self.cloud.logout())
raise

cognito = self._cognito(username=email)
try:
cognito.authenticate(password=password)
return cognito
async def async_renew_access_token(self):
"""Renew access token."""
async with self._request_lock:
await self._async_renew_access_token()

except ForceChangePasswordException:
raise PasswordChangeRequired()
async def _async_renew_access_token(self):
"""Renew access token internals.
Does not consume lock.
"""
cognito = self._authenticated_cognito

try:
await self.cloud.run_executor(cognito.renew_access_token)
self.cloud.id_token = cognito.id_token
self.cloud.access_token = cognito.access_token
await self.cloud.run_executor(self.cloud.write_user_info)

except ClientError as err:
raise _map_aws_exception(err)

except EndpointConnectionError:
raise UnknownError()

@property
def _authenticated_cognito(self):
"""Return an authenticated cognito instance."""
return self._cognito(
access_token=self.cloud.access_token, refresh_token=self.cloud.refresh_token
)

def _cognito(self, **kwargs):
"""Get the client credentials."""
cognito = pycognito.Cognito(
return pycognito.Cognito(
user_pool_id=self.cloud.user_pool_id,
client_id=self.cloud.cognito_client_id,
user_pool_region=self.cloud.region,
botocore_config=botocore.config.Config(signature_version=botocore.UNSIGNED),
session=self._session,
**kwargs,
)
return cognito


def _map_aws_exception(err):
Expand Down
1 change: 1 addition & 0 deletions requirements_tests.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
asynctest==0.13.0
flake8==3.7.9
pylint==2.4.4
pytest==5.3.5
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from setuptools import setup

VERSION = "0.32"
VERSION = "0.32.1"

setup(
name="hass-nabucasa",
Expand Down
Loading

0 comments on commit 11bad81

Please sign in to comment.