async def test_not_raise_exception_when_service_not_exist(hass): """Test login flow will not raise exception when notify service error.""" hass.auth = await auth_manager_from_config( hass, [{ 'type': 'insecure_example', 'users': [{ 'username': '******', 'password': '******' }], }], [{ 'type': 'notify', }]) user = MockUser( id='mock-user', is_owner=False, is_active=False, name='Paulus', ).add_to_auth_manager(hass.auth) await hass.auth.async_link_user( user, auth_models.Credentials( id='mock-id', auth_provider_type='insecure_example', auth_provider_id=None, data={'username': '******'}, is_new=False, )) await hass.auth.async_enable_user_mfa(user, 'notify', { 'notify_service': 'invalid-notify', }) provider = hass.auth.auth_providers[0] result = await hass.auth.login_flow.async_init( (provider.type, provider.id)) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM with patch('pyotp.HOTP.at', return_value=MOCK_CODE): result = await hass.auth.login_flow.async_configure( result['flow_id'], { 'username': '******', 'password': '******', }) assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT assert result['reason'] == 'unknown_error' # wait service call finished await hass.async_block_till_done()
async def test_async_remove_user(opp): """Test removing a user.""" events = [] @callback def user_removed(event): events.append(event) opp.bus.async_listen("user_removed", user_removed) manager = await auth.auth_manager_from_config( opp, [{ "type": "insecure_example", "users": [{ "username": "******", "password": "******", "name": "Test Name", }], }], [], ) opp.auth = manager ensure_auth_manager_loaded(manager) # Add fake user with credentials for example auth provider. user = MockUser(id="mock-user", is_owner=False, is_active=False, name="Paulus").add_to_auth_manager(manager) user.credentials.append( auth_models.Credentials( id="mock-id", auth_provider_type="insecure_example", auth_provider_id=None, data={"username": "******"}, is_new=False, )) assert len(user.credentials) == 1 await opp.auth.async_remove_user(user) assert len(await manager.async_get_users()) == 0 assert len(user.credentials) == 0 await opp.async_block_till_done() assert len(events) == 1 assert events[0].data["user_id"] == user.id
async def test_one_long_lived_access_token_per_refresh_token(mock_hass): """Test one refresh_token can only have one long-lived access token.""" manager = await auth.auth_manager_from_config(mock_hass, [], []) user = MockUser().add_to_auth_manager(manager) refresh_token = await manager.async_create_refresh_token( user, client_name="GPS Logger", token_type=auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, access_token_expiration=timedelta(days=3000), ) assert refresh_token.token_type == auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN access_token = manager.async_create_access_token(refresh_token) jwt_key = refresh_token.jwt_key rt = await manager.async_validate_access_token(access_token) assert rt.id == refresh_token.id with pytest.raises(ValueError): await manager.async_create_refresh_token( user, client_name="GPS Logger", token_type=auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, access_token_expiration=timedelta(days=3000), ) await manager.async_remove_refresh_token(refresh_token) assert refresh_token.id not in user.refresh_tokens rt = await manager.async_validate_access_token(access_token) assert rt is None, "Previous issued access token has been invoked" refresh_token_2 = await manager.async_create_refresh_token( user, client_name="GPS Logger", token_type=auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, access_token_expiration=timedelta(days=3000), ) assert refresh_token_2.id != refresh_token.id assert refresh_token_2.token_type == auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN access_token_2 = manager.async_create_access_token(refresh_token_2) jwt_key_2 = refresh_token_2.jwt_key assert access_token != access_token_2 assert jwt_key != jwt_key_2 rt = await manager.async_validate_access_token(access_token_2) jwt_payload = jwt.decode(access_token_2, rt.jwt_key, algorithm=["HS256"]) assert jwt_payload["iss"] == refresh_token_2.id assert (jwt_payload["exp"] - jwt_payload["iat"] == timedelta(days=3000).total_seconds())
async def test_not_raise_exception_when_service_not_exist(opp): """Test login flow will not raise exception when notify service error.""" opp.auth = await auth_manager_from_config( opp, [{ "type": "insecure_example", "users": [{ "username": "******", "password": "******" }], }], [{ "type": "notify" }], ) user = MockUser(id="mock-user", is_owner=False, is_active=False, name="Paulus").add_to_auth_manager(opp.auth) await opp.auth.async_link_user( user, auth_models.Credentials( id="mock-id", auth_provider_type="insecure_example", auth_provider_id=None, data={"username": "******"}, is_new=False, ), ) await opp.auth.async_enable_user_mfa(user, "notify", {"notify_service": "invalid-notify"}) provider = opp.auth.auth_providers[0] result = await opp.auth.login_flow.async_init((provider.type, provider.id)) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM with patch("pyotp.HOTP.at", return_value=MOCK_CODE): result = await opp.auth.login_flow.async_configure( result["flow_id"], { "username": "******", "password": "******" }) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "unknown_error" # wait service call finished await opp.async_block_till_done()
async def test_remove_refresh_token(mock_hass): """Test that we can remove a refresh token.""" manager = await auth.auth_manager_from_config(mock_hass, [], []) user = MockUser().add_to_auth_manager(manager) refresh_token = await manager.async_create_refresh_token(user, CLIENT_ID) access_token = manager.async_create_access_token(refresh_token) await manager.async_remove_refresh_token(refresh_token) assert ( await manager.async_get_refresh_token(refresh_token.id) is None ) assert ( await manager.async_validate_access_token(access_token) is None )
async def test_refresh_token_type(hass): """Test create a refresh token with token type.""" manager = await auth.auth_manager_from_config(hass, [], []) user = MockUser().add_to_auth_manager(manager) with pytest.raises(ValueError): await manager.async_create_refresh_token( user, CLIENT_ID, token_type=auth_models.TOKEN_TYPE_SYSTEM) token = await manager.async_create_refresh_token( user, CLIENT_ID, token_type=auth_models.TOKEN_TYPE_NORMAL) assert token is not None assert token.client_id == CLIENT_ID assert token.token_type == auth_models.TOKEN_TYPE_NORMAL
async def test_register_revoke_token_callback(mock_hass): """Test that a registered revoke token callback is called.""" manager = await auth.auth_manager_from_config(mock_hass, [], []) user = MockUser().add_to_auth_manager(manager) refresh_token = await manager.async_create_refresh_token(user, CLIENT_ID) called = False def cb(): nonlocal called called = True manager.async_register_revoke_token_callback(refresh_token.id, cb) await manager.async_remove_refresh_token(refresh_token) assert called
async def test_having_owner_finishes_user_step(hass, hass_storage): """If owner user already exists, mark user step as complete.""" MockUser(is_owner=True).add_to_hass(hass) with patch("homeassistant.components.onboarding.views.async_setup" ) as mock_setup, patch.object(onboarding, "STEPS", [onboarding.STEP_USER]): assert await async_setup_component(hass, "onboarding", {}) assert len(mock_setup.mock_calls) == 0 assert onboarding.DOMAIN not in hass.data assert onboarding.async_is_onboarded(hass) done = hass_storage[onboarding.STORAGE_KEY]["data"]["done"] assert onboarding.STEP_USER in done
async def test_event_user_updated_fires(hass): """Test the user updated event fires.""" manager = await auth.auth_manager_from_config(hass, [], []) user = MockUser().add_to_auth_manager(manager) await manager.async_create_refresh_token(user, CLIENT_ID) assert len(list(user.refresh_tokens.values())) == 1 events = async_capture_events(hass, EVENT_USER_UPDATED) await manager.async_update_user(user, name="new name") assert user.name == "new name" await hass.async_block_till_done() assert len(events) == 1
async def test_create_access_token(mock_hass): """Test normal refresh_token's jwt_key keep same after used.""" manager = await auth.auth_manager_from_config(mock_hass, [], []) user = MockUser().add_to_auth_manager(manager) refresh_token = await manager.async_create_refresh_token(user, CLIENT_ID) assert refresh_token.token_type == auth_models.TOKEN_TYPE_NORMAL jwt_key = refresh_token.jwt_key access_token = manager.async_create_access_token(refresh_token) assert access_token is not None assert refresh_token.jwt_key == jwt_key jwt_payload = jwt.decode(access_token, jwt_key, algorithm=["HS256"]) assert jwt_payload["iss"] == refresh_token.id assert ( jwt_payload["exp"] - jwt_payload["iat"] == timedelta(minutes=30).total_seconds() )
async def test_refresh_token_requires_client_for_user(hass): """Test create refresh token for a user with client_id.""" manager = await auth.auth_manager_from_config(hass, [], []) user = MockUser().add_to_auth_manager(manager) assert user.system_generated is False with pytest.raises(ValueError): await manager.async_create_refresh_token(user) token = await manager.async_create_refresh_token(user, CLIENT_ID) assert token is not None assert token.client_id == CLIENT_ID assert token.token_type == auth_models.TOKEN_TYPE_NORMAL # default access token expiration assert token.access_token_expiration == auth_const.ACCESS_TOKEN_EXPIRATION
async def test_create_long_lived_access_token(mock_hass): """Test refresh_token's jwt_key changed for long-lived access token.""" manager = await auth.auth_manager_from_config(mock_hass, [], []) user = MockUser().add_to_auth_manager(manager) refresh_token = await manager.async_create_refresh_token( user, client_name='GPS Logger', token_type=auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, access_token_expiration=timedelta(days=300)) assert refresh_token.token_type == \ auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN access_token = manager.async_create_access_token(refresh_token) jwt_payload = jwt.decode( access_token, refresh_token.jwt_key, algorithm=['HS256']) assert jwt_payload['iss'] == refresh_token.id assert jwt_payload['exp'] - jwt_payload['iat'] == \ timedelta(days=300).total_seconds()
async def test_delete(hass, hass_ws_client, hass_access_token): """Test delete command works.""" client = await hass_ws_client(hass, hass_access_token) test_user = MockUser(id='efg', ).add_to_hass(hass) assert len(await hass.auth.async_get_users()) == 2 await client.send_json({ 'id': 5, 'type': auth_config.WS_TYPE_DELETE, 'user_id': test_user.id, }) result = await client.receive_json() assert result['success'], result assert len(await hass.auth.async_get_users()) == 1
async def test_rename_does_not_change_refresh_token(mock_hass): """Test that we can rename without changing refresh token.""" manager = await auth.auth_manager_from_config(mock_hass, [], []) user = MockUser().add_to_auth_manager(manager) await manager.async_create_refresh_token(user, CLIENT_ID) assert len(list(user.refresh_tokens.values())) == 1 token_before = list(user.refresh_tokens.values())[0] await manager.async_update_user(user, name="new name") assert user.name == "new name" assert len(list(user.refresh_tokens.values())) == 1 token_after = list(user.refresh_tokens.values())[0] assert token_before == token_after
async def test_delete(hass, hass_ws_client, hass_access_token): """Test delete command works.""" client = await hass_ws_client(hass, hass_access_token) test_user = MockUser(id="efg").add_to_hass(hass) assert len(await hass.auth.async_get_users()) == 2 await client.send_json({ "id": 5, "type": auth_config.WS_TYPE_DELETE, "user_id": test_user.id }) result = await client.receive_json() assert result["success"], result assert len(await hass.auth.async_get_users()) == 1
async def test_async_remove_user(hass): """Test removing a user.""" events = [] @callback def user_removed(event): events.append(event) hass.bus.async_listen('user_removed', user_removed) manager = await auth.auth_manager_from_config(hass, [{ 'type': 'insecure_example', 'users': [{ 'username': '******', 'password': '******', 'name': 'Test Name' }] }], []) hass.auth = manager ensure_auth_manager_loaded(manager) # Add fake user with credentials for example auth provider. user = MockUser( id='mock-user', is_owner=False, is_active=False, name='Paulus', ).add_to_auth_manager(manager) user.credentials.append( auth_models.Credentials( id='mock-id', auth_provider_type='insecure_example', auth_provider_id=None, data={'username': '******'}, is_new=False, )) assert len(user.credentials) == 1 await hass.auth.async_remove_user(user) assert len(await manager.async_get_users()) == 0 assert len(user.credentials) == 0 await hass.async_block_till_done() assert len(events) == 1 assert events[0].data['user_id'] == user.id
async def test_refresh_token_type_long_lived_access_token(hass): """Test create a refresh token has long-lived access token type.""" manager = await auth.auth_manager_from_config(hass, [], []) user = MockUser().add_to_auth_manager(manager) with pytest.raises(ValueError): await manager.async_create_refresh_token( user, token_type=auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN) token = await manager.async_create_refresh_token( user, client_name='GPS LOGGER', client_icon='mdi:home', token_type=auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN) assert token is not None assert token.client_id is None assert token.client_name == 'GPS LOGGER' assert token.client_icon == 'mdi:home' assert token.token_type == auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
async def test_create_auth_system_generated_user(hass, hass_ws_client): """Test we can't add auth to system generated users.""" system_user = MockUser(system_generated=True).add_to_hass(hass) client = await hass_ws_client(hass) await client.send_json({ "id": 5, "type": "config/auth_provider/homeassistant/create", "user_id": system_user.id, "username": "******", "password": "******", }) result = await client.receive_json() assert not result["success"], result assert result["error"]["code"] == "system_generated"
async def test_cannot_retrieve_expired_access_token(hass): """Test that we cannot retrieve expired access tokens.""" manager = await auth.auth_manager_from_config(hass, []) user = MockUser().add_to_auth_manager(manager) refresh_token = await manager.async_create_refresh_token(user, CLIENT_ID) assert refresh_token.user.id is user.id assert refresh_token.client_id == CLIENT_ID access_token = manager.async_create_access_token(refresh_token) assert manager.async_get_access_token(access_token.token) is access_token with patch('homeassistant.auth.dt_util.utcnow', return_value=dt_util.utcnow() + auth.ACCESS_TOKEN_EXPIRATION): assert manager.async_get_access_token(access_token.token) is None # Even with unpatched time, it should have been removed from manager assert manager.async_get_access_token(access_token.token) is None
async def test_login_as_existing_user(mock_hass): """Test login as existing user.""" manager = await auth.auth_manager_from_config(mock_hass, [{ 'type': 'insecure_example', 'users': [{ 'username': '******', 'password': '******', 'name': 'Test Name' }] }]) ensure_auth_manager_loaded(manager) # Add fake user with credentials for example auth provider. user = MockUser( id='mock-user', is_owner=False, is_active=False, name='Paulus', ).add_to_auth_manager(manager) user.credentials.append( auth.Credentials( id='mock-id', auth_provider_type='insecure_example', auth_provider_id=None, data={'username': '******'}, is_new=False, )) step = await manager.login_flow.async_init(('insecure_example', None)) assert step['type'] == data_entry_flow.RESULT_TYPE_FORM step = await manager.login_flow.async_configure(step['flow_id'], { 'username': '******', 'password': '******', }) assert step['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY credentials = step['result'] user = await manager.async_get_or_create_user(credentials) assert user is not None assert user.id == 'mock-user' assert user.is_owner is False assert user.is_active is False assert user.name == 'Paulus'
async def test_cannot_retrieve_expired_access_token(hass): """Test that we cannot retrieve expired access tokens.""" manager = await auth.auth_manager_from_config(hass, [], []) user = MockUser().add_to_auth_manager(manager) refresh_token = await manager.async_create_refresh_token(user, CLIENT_ID) assert refresh_token.user.id is user.id assert refresh_token.client_id == CLIENT_ID access_token = manager.async_create_access_token(refresh_token) assert (await manager.async_validate_access_token(access_token) is refresh_token) with patch('homeassistant.util.dt.utcnow', return_value=dt_util.utcnow() - auth_const.ACCESS_TOKEN_EXPIRATION - timedelta(seconds=11)): access_token = manager.async_create_access_token(refresh_token) assert (await manager.async_validate_access_token(access_token) is None)
async def test_create_auth_system_generated_user(hass, hass_access_token, hass_ws_client): """Test we can't add auth to system generated users.""" system_user = MockUser(system_generated=True).add_to_hass(hass) client = await hass_ws_client(hass, hass_access_token) await client.send_json({ 'id': 5, 'type': auth_ha.WS_TYPE_CREATE, 'user_id': system_user.id, 'username': '******', 'password': '******', }) result = await client.receive_json() assert not result['success'], result assert result['error']['code'] == 'system_generated'
async def test_deactivate_owner(opp, opp_ws_client): """Test that owner cannot be deactivated.""" user = MockUser(id="abc", name="Test Owner", is_owner=True).add_to_opp(opp) assert user.is_active is True assert user.is_owner is True client = await opp_ws_client(opp) await client.send_json({ "id": 5, "type": "config/auth/update", "user_id": user.id, "is_active": False }) result = await client.receive_json() assert not result["success"], result assert result["error"]["code"] == "cannot_deactivate_owner"
async def test_create_auth_system_generated_user(opp, opp_access_token, opp_ws_client): """Test we can't add auth to system generated users.""" system_user = MockUser(system_generated=True).add_to_opp(opp) client = await opp_ws_client(opp, opp_access_token) await client.send_json( { "id": 5, "type": auth_ha.WS_TYPE_CREATE, "user_id": system_user.id, "username": "******", "password": "******", } ) result = await client.receive_json() assert not result["success"], result assert result["error"]["code"] == "system_generated"
def test_auth_code_store_expiration(): """Test that the auth code store will not return expired tokens.""" store, retrieve = auth._create_auth_code_store() client_id = 'bla' user = MockUser(id='mock_user') now = utcnow() with patch('homeassistant.util.dt.utcnow', return_value=now): code = store(client_id, user) with patch('homeassistant.util.dt.utcnow', return_value=now + timedelta(minutes=10)): assert retrieve(client_id, RESULT_TYPE_USER, code) is None with patch('homeassistant.util.dt.utcnow', return_value=now): code = store(client_id, user) with patch('homeassistant.util.dt.utcnow', return_value=now + timedelta(minutes=9, seconds=59)): assert retrieve(client_id, RESULT_TYPE_USER, code) == user
async def test_refresh_token_provider_validation(mock_hass): """Test that creating access token from refresh token checks with provider.""" manager = await auth.auth_manager_from_config( mock_hass, [ { "type": "insecure_example", "users": [{"username": "******", "password": "******"}], } ], [], ) credential = auth_models.Credentials( id="mock-credential-id", auth_provider_type="insecure_example", auth_provider_id=None, data={"username": "******"}, is_new=False, ) user = MockUser().add_to_auth_manager(manager) user.credentials.append(credential) refresh_token = await manager.async_create_refresh_token( user, CLIENT_ID, credential=credential ) ip = "127.0.0.1" assert manager.async_create_access_token(refresh_token, ip) is not None with patch( "homeassistant.auth.providers.insecure_example.ExampleAuthProvider.async_validate_refresh_token", side_effect=InvalidAuthError("Invalid access"), ) as call: with pytest.raises(InvalidAuthError): manager.async_create_access_token(refresh_token, ip) call.assert_called_with(refresh_token, ip)
async def test_exception_handling(): """Test handling of exceptions.""" send_messages = [] user = MockUser() refresh_token = Mock() conn = websocket_api.ActiveConnection(logging.getLogger(__name__), None, send_messages.append, user, refresh_token) for (exc, code, err) in ( (exceptions.Unauthorized(), websocket_api.ERR_UNAUTHORIZED, "Unauthorized"), ( vol.Invalid("Invalid something"), websocket_api.ERR_INVALID_FORMAT, "Invalid something. Got {'id': 5}", ), (asyncio.TimeoutError(), websocket_api.ERR_TIMEOUT, "Timeout"), ( exceptions.HomeAssistantError("Failed to do X"), websocket_api.ERR_UNKNOWN_ERROR, "Failed to do X", ), (ValueError("Really bad"), websocket_api.ERR_UNKNOWN_ERROR, "Unknown error"), ( exceptions.HomeAssistantError(), websocket_api.ERR_UNKNOWN_ERROR, "Unknown error", ), ): send_messages.clear() conn.async_handle_exception({"id": 5}, exc) assert len(send_messages) == 1 assert send_messages[0]["error"]["code"] == code assert send_messages[0]["error"]["message"] == err
async def test_login_flow_validates_mfa(hass): """Test login flow with mfa enabled.""" hass.auth = await auth_manager_from_config( hass, [{ 'type': 'insecure_example', 'users': [{ 'username': '******', 'password': '******' }], }], [{ 'type': 'notify', }]) user = MockUser( id='mock-user', is_owner=False, is_active=False, name='Paulus', ).add_to_auth_manager(hass.auth) await hass.auth.async_link_user( user, auth_models.Credentials( id='mock-id', auth_provider_type='insecure_example', auth_provider_id=None, data={'username': '******'}, is_new=False, )) notify_calls = async_mock_service(hass, 'notify', 'test-notify', NOTIFY_SERVICE_SCHEMA) await hass.auth.async_enable_user_mfa(user, 'notify', { 'notify_service': 'test-notify', }) provider = hass.auth.auth_providers[0] result = await hass.auth.login_flow.async_init( (provider.type, provider.id)) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM result = await hass.auth.login_flow.async_configure( result['flow_id'], { 'username': '******', 'password': '******', }) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM assert result['errors']['base'] == 'invalid_auth' result = await hass.auth.login_flow.async_configure( result['flow_id'], { 'username': '******', 'password': '******', }) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM assert result['errors']['base'] == 'invalid_auth' with patch('pyotp.HOTP.at', return_value=MOCK_CODE): result = await hass.auth.login_flow.async_configure( result['flow_id'], { 'username': '******', 'password': '******', }) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM assert result['step_id'] == 'mfa' assert result['data_schema'].schema.get('code') == str # wait service call finished await hass.async_block_till_done() assert len(notify_calls) == 1 notify_call = notify_calls[0] assert notify_call.domain == 'notify' assert notify_call.service == 'test-notify' message = notify_call.data['message'] message.hass = hass assert MOCK_CODE in message.async_render() with patch('pyotp.HOTP.verify', return_value=False): result = await hass.auth.login_flow.async_configure( result['flow_id'], {'code': 'invalid-code'}) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM assert result['step_id'] == 'mfa' assert result['errors']['base'] == 'invalid_code' # wait service call finished await hass.async_block_till_done() # would not send new code, allow user retry assert len(notify_calls) == 1 # retry twice with patch('pyotp.HOTP.verify', return_value=False), \ patch('pyotp.HOTP.at', return_value=MOCK_CODE_2): result = await hass.auth.login_flow.async_configure( result['flow_id'], {'code': 'invalid-code'}) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM assert result['step_id'] == 'mfa' assert result['errors']['base'] == 'invalid_code' # after the 3rd failure, flow abort result = await hass.auth.login_flow.async_configure( result['flow_id'], {'code': 'invalid-code'}) assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT assert result['reason'] == 'too_many_retry' # wait service call finished await hass.async_block_till_done() # restart login result = await hass.auth.login_flow.async_init( (provider.type, provider.id)) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM with patch('pyotp.HOTP.at', return_value=MOCK_CODE): result = await hass.auth.login_flow.async_configure( result['flow_id'], { 'username': '******', 'password': '******', }) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM assert result['step_id'] == 'mfa' assert result['data_schema'].schema.get('code') == str # wait service call finished await hass.async_block_till_done() assert len(notify_calls) == 2 notify_call = notify_calls[1] assert notify_call.domain == 'notify' assert notify_call.service == 'test-notify' message = notify_call.data['message'] message.hass = hass assert MOCK_CODE in message.async_render() with patch('pyotp.HOTP.verify', return_value=True): result = await hass.auth.login_flow.async_configure( result['flow_id'], {'code': MOCK_CODE}) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result['data'].id == 'mock-user'
async def test_login_flow_validates_mfa(opp): """Test login flow with mfa enabled.""" opp.auth = await auth_manager_from_config( opp, [{ "type": "insecure_example", "users": [{ "username": "******", "password": "******" }], }], [{ "type": "notify" }], ) user = MockUser(id="mock-user", is_owner=False, is_active=False, name="Paulus").add_to_auth_manager(opp.auth) await opp.auth.async_link_user( user, auth_models.Credentials( id="mock-id", auth_provider_type="insecure_example", auth_provider_id=None, data={"username": "******"}, is_new=False, ), ) notify_calls = async_mock_service(opp, "notify", "test-notify", NOTIFY_SERVICE_SCHEMA) await opp.auth.async_enable_user_mfa(user, "notify", {"notify_service": "test-notify"}) provider = opp.auth.auth_providers[0] result = await opp.auth.login_flow.async_init((provider.type, provider.id)) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await opp.auth.login_flow.async_configure(result["flow_id"], { "username": "******", "password": "******" }) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"]["base"] == "invalid_auth" result = await opp.auth.login_flow.async_configure( result["flow_id"], { "username": "******", "password": "******" }) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"]["base"] == "invalid_auth" with patch("pyotp.HOTP.at", return_value=MOCK_CODE): result = await opp.auth.login_flow.async_configure( result["flow_id"], { "username": "******", "password": "******" }) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mfa" assert result["data_schema"].schema.get("code") == str # wait service call finished await opp.async_block_till_done() assert len(notify_calls) == 1 notify_call = notify_calls[0] assert notify_call.domain == "notify" assert notify_call.service == "test-notify" message = notify_call.data["message"] message.opp = opp assert MOCK_CODE in message.async_render() with patch("pyotp.HOTP.verify", return_value=False): result = await opp.auth.login_flow.async_configure( result["flow_id"], {"code": "invalid-code"}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mfa" assert result["errors"]["base"] == "invalid_code" # wait service call finished await opp.async_block_till_done() # would not send new code, allow user retry assert len(notify_calls) == 1 # retry twice with patch("pyotp.HOTP.verify", return_value=False), patch("pyotp.HOTP.at", return_value=MOCK_CODE_2): result = await opp.auth.login_flow.async_configure( result["flow_id"], {"code": "invalid-code"}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mfa" assert result["errors"]["base"] == "invalid_code" # after the 3rd failure, flow abort result = await opp.auth.login_flow.async_configure( result["flow_id"], {"code": "invalid-code"}) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "too_many_retry" # wait service call finished await opp.async_block_till_done() # restart login result = await opp.auth.login_flow.async_init((provider.type, provider.id)) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM with patch("pyotp.HOTP.at", return_value=MOCK_CODE): result = await opp.auth.login_flow.async_configure( result["flow_id"], { "username": "******", "password": "******" }) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mfa" assert result["data_schema"].schema.get("code") == str # wait service call finished await opp.async_block_till_done() assert len(notify_calls) == 2 notify_call = notify_calls[1] assert notify_call.domain == "notify" assert notify_call.service == "test-notify" message = notify_call.data["message"] message.opp = opp assert MOCK_CODE in message.async_render() with patch("pyotp.HOTP.verify", return_value=True): result = await opp.auth.login_flow.async_configure( result["flow_id"], {"code": MOCK_CODE}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"].id == "mock-user"
def hass_read_only_user(hass, local_auth): """Return a Home Assistant read only user.""" read_only_group = hass.loop.run_until_complete( hass.auth.async_get_group(GROUP_ID_READ_ONLY)) return MockUser(groups=[read_only_group]).add_to_hass(hass)