diff --git a/firebase_admin/_auth_utils.py b/firebase_admin/_auth_utils.py index ac7b322f..127a508a 100644 --- a/firebase_admin/_auth_utils.py +++ b/firebase_admin/_auth_utils.py @@ -328,6 +328,15 @@ def __init__(self, message, cause, http_response): exceptions.InvalidArgumentError.__init__(self, message, cause, http_response) +class InvalidHostingLinkDomainError(exceptions.InvalidArgumentError): + """Hosting link domain in ActionCodeSettings is not authorized.""" + + default_message = 'Hosting link domain specified in ActionCodeSettings is not authorized' + + def __init__(self, message, cause, http_response): + exceptions.InvalidArgumentError.__init__(self, message, cause, http_response) + + class InvalidIdTokenError(exceptions.InvalidArgumentError): """The provided ID token is not a valid Firebase ID token.""" @@ -427,6 +436,7 @@ def __init__(self, message, cause=None, http_response=None): 'EMAIL_NOT_FOUND': EmailNotFoundError, 'INSUFFICIENT_PERMISSION': InsufficientPermissionError, 'INVALID_DYNAMIC_LINK_DOMAIN': InvalidDynamicLinkDomainError, + 'INVALID_HOSTING_LINK_DOMAIN': InvalidHostingLinkDomainError, 'INVALID_ID_TOKEN': InvalidIdTokenError, 'PHONE_NUMBER_EXISTS': PhoneNumberAlreadyExistsError, 'TENANT_NOT_FOUND': TenantNotFoundError, diff --git a/firebase_admin/_user_mgt.py b/firebase_admin/_user_mgt.py index aa0dfb0a..a03262a3 100644 --- a/firebase_admin/_user_mgt.py +++ b/firebase_admin/_user_mgt.py @@ -18,6 +18,7 @@ from collections import defaultdict import json from urllib import parse +import warnings import requests @@ -490,7 +491,10 @@ class ActionCodeSettings: """ def __init__(self, url, handle_code_in_app=None, dynamic_link_domain=None, ios_bundle_id=None, - android_package_name=None, android_install_app=None, android_minimum_version=None): + android_package_name=None, android_install_app=None, android_minimum_version=None, + link_domain=None): + if dynamic_link_domain is not None: + warnings.warn('dynamic_link_domain is deprecated, use link_domain instead', DeprecationWarning) self.url = url self.handle_code_in_app = handle_code_in_app self.dynamic_link_domain = dynamic_link_domain @@ -498,6 +502,7 @@ def __init__(self, url, handle_code_in_app=None, dynamic_link_domain=None, ios_b self.android_package_name = android_package_name self.android_install_app = android_install_app self.android_minimum_version = android_minimum_version + self.link_domain = link_domain def encode_action_code_settings(settings): @@ -535,6 +540,13 @@ def encode_action_code_settings(settings): .format(settings.dynamic_link_domain)) parameters['dynamicLinkDomain'] = settings.dynamic_link_domain + # link_domain + if settings.link_domain is not None: + if not isinstance(settings.link_domain, str): + raise ValueError('Invalid value provided for link_domain: {0}' + .format(settings.link_domain)) + parameters['linkDomain'] = settings.link_domain + # ios_bundle_id if settings.ios_bundle_id is not None: if not isinstance(settings.ios_bundle_id, str): diff --git a/firebase_admin/auth.py b/firebase_admin/auth.py index ced14311..cb63ab7f 100644 --- a/firebase_admin/auth.py +++ b/firebase_admin/auth.py @@ -49,6 +49,7 @@ 'ImportUserRecord', 'InsufficientPermissionError', 'InvalidDynamicLinkDomainError', + 'InvalidHostingLinkDomainError', 'InvalidIdTokenError', 'InvalidSessionCookieError', 'ListProviderConfigsPage', @@ -125,6 +126,7 @@ ImportUserRecord = _user_import.ImportUserRecord InsufficientPermissionError = _auth_utils.InsufficientPermissionError InvalidDynamicLinkDomainError = _auth_utils.InvalidDynamicLinkDomainError +InvalidHostingLinkDomainError = _auth_utils.InvalidHostingLinkDomainError InvalidIdTokenError = _auth_utils.InvalidIdTokenError InvalidSessionCookieError = _token_gen.InvalidSessionCookieError ListProviderConfigsPage = _auth_providers.ListProviderConfigsPage diff --git a/tests/test_user_mgt.py b/tests/test_user_mgt.py index 34b698be..e471e5b1 100644 --- a/tests/test_user_mgt.py +++ b/tests/test_user_mgt.py @@ -43,7 +43,8 @@ MOCK_ACTION_CODE_DATA = { 'url': 'http://localhost', 'handle_code_in_app': True, - 'dynamic_link_domain': 'http://testly', + 'dynamic_link_domain': 'http://dynamic-link-domain', + 'link_domain': 'http://link-domain', 'ios_bundle_id': 'test.bundle', 'android_package_name': 'test.bundle', 'android_minimum_version': '7', @@ -1364,7 +1365,8 @@ def test_valid_data(self): data = { 'url': 'http://localhost', 'handle_code_in_app': True, - 'dynamic_link_domain': 'http://testly', + 'dynamic_link_domain': 'http://dynamic-link-domain', + 'link_domain': 'http://link-domain', 'ios_bundle_id': 'test.bundle', 'android_package_name': 'test.bundle', 'android_minimum_version': '7', @@ -1375,6 +1377,7 @@ def test_valid_data(self): assert parameters['continueUrl'] == data['url'] assert parameters['canHandleCodeInApp'] == data['handle_code_in_app'] assert parameters['dynamicLinkDomain'] == data['dynamic_link_domain'] + assert parameters['linkDomain'] == data['link_domain'] assert parameters['iOSBundleId'] == data['ios_bundle_id'] assert parameters['androidPackageName'] == data['android_package_name'] assert parameters['androidMinimumVersion'] == data['android_minimum_version'] @@ -1497,6 +1500,23 @@ def test_invalid_dynamic_link(self, user_mgt_app, func): assert excinfo.value.http_response is not None assert excinfo.value.cause is not None + @pytest.mark.parametrize('func', [ + auth.generate_sign_in_with_email_link, + auth.generate_email_verification_link, + auth.generate_password_reset_link, + ]) + def test_invalid_hosting_link(self, user_mgt_app, func): + resp = '{"error":{"message": "INVALID_HOSTING_LINK_DOMAIN: Because of this reason."}}' + _instrument_user_manager(user_mgt_app, 500, resp) + with pytest.raises(auth.InvalidHostingLinkDomainError) as excinfo: + func('test@test.com', MOCK_ACTION_CODE_SETTINGS, app=user_mgt_app) + assert isinstance(excinfo.value, exceptions.InvalidArgumentError) + assert str(excinfo.value) == ('Hosting link domain specified in ActionCodeSettings is ' + 'not authorized (INVALID_HOSTING_LINK_DOMAIN). Because ' + 'of this reason.') + assert excinfo.value.http_response is not None + assert excinfo.value.cause is not None + @pytest.mark.parametrize('func', [ auth.generate_sign_in_with_email_link, auth.generate_email_verification_link, @@ -1535,6 +1555,7 @@ def _validate_request(self, request, settings=None): assert request['continueUrl'] == settings.url assert request['canHandleCodeInApp'] == settings.handle_code_in_app assert request['dynamicLinkDomain'] == settings.dynamic_link_domain + assert request['linkDomain'] == settings.link_domain assert request['iOSBundleId'] == settings.ios_bundle_id assert request['androidPackageName'] == settings.android_package_name assert request['androidMinimumVersion'] == settings.android_minimum_version