Skip to content
10 changes: 10 additions & 0 deletions firebase_admin/_auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -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,
Expand Down
14 changes: 13 additions & 1 deletion firebase_admin/_user_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from collections import defaultdict
import json
from urllib import parse
import warnings

import requests

Expand Down Expand Up @@ -490,14 +491,18 @@ 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
self.ios_bundle_id = ios_bundle_id
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):
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions firebase_admin/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
'ImportUserRecord',
'InsufficientPermissionError',
'InvalidDynamicLinkDomainError',
'InvalidHostingLinkDomainError',
'InvalidIdTokenError',
'InvalidSessionCookieError',
'ListProviderConfigsPage',
Expand Down Expand Up @@ -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
Expand Down
25 changes: 23 additions & 2 deletions tests/test_user_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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']
Expand Down Expand Up @@ -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('[email protected]', 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,
Expand Down Expand Up @@ -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
Expand Down