diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index c41b417576e..3e236876d6a 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -102,6 +102,7 @@ a limited expiration. "token_type": "Bearer" } """ +from datetime import timedelta import logging import uuid @@ -114,6 +115,7 @@ from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, FlowManagerResourceView) from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.util import dt as dt_util from . import indieauth @@ -349,12 +351,26 @@ def _create_cred_store(): def store_credentials(client_id, credentials): """Store credentials and return a code to retrieve it.""" code = uuid.uuid4().hex - temp_credentials[(client_id, code)] = credentials + temp_credentials[(client_id, code)] = (dt_util.utcnow(), credentials) return code @callback def retrieve_credentials(client_id, code): """Retrieve credentials.""" - return temp_credentials.pop((client_id, code), None) + key = (client_id, code) + + if key not in temp_credentials: + return None + + created, credentials = temp_credentials.pop(key) + + # OAuth 4.2.1 + # The authorization code MUST expire shortly after it is issued to + # mitigate the risk of leaks. A maximum authorization code lifetime of + # 10 minutes is RECOMMENDED. + if dt_util.utcnow() - created < timedelta(minutes=10): + return credentials + + return None return store_credentials, retrieve_credentials diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 68a77d18d56..c5c46d55e39 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -1,4 +1,10 @@ """Integration tests for the auth component.""" +from datetime import timedelta +from unittest.mock import patch + +from homeassistant.util.dt import utcnow +from homeassistant.components import auth + from . import async_setup_auth from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI @@ -58,3 +64,25 @@ async def test_login_new_user_and_refresh_token(hass, aiohttp_client): 'authorization': 'Bearer {}'.format(tokens['access_token']) }) assert resp.status == 200 + + +def test_credential_store_expiration(): + """Test that the credential store will not return expired tokens.""" + store, retrieve = auth._create_cred_store() + client_id = 'bla' + credentials = 'creds' + now = utcnow() + + with patch('homeassistant.util.dt.utcnow', return_value=now): + code = store(client_id, credentials) + + with patch('homeassistant.util.dt.utcnow', + return_value=now + timedelta(minutes=10)): + assert retrieve(client_id, code) is None + + with patch('homeassistant.util.dt.utcnow', return_value=now): + code = store(client_id, credentials) + + with patch('homeassistant.util.dt.utcnow', + return_value=now + timedelta(minutes=9, seconds=59)): + assert retrieve(client_id, code) == credentials