Skip to content
12 changes: 12 additions & 0 deletions firebase_admin/_auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,17 @@ def __init__(self, message, cause, http_response):
exceptions.InvalidArgumentError.__init__(self, message, cause, http_response)


class InvalidHostingLinkDomainError(exceptions.InvalidArgumentError):
"""The provided hosting link domain is not configured in Firebase Hosting
or is not owned by the current project."""

default_message = ('The provided hosting link domain is not configured in Firebase '
'Hosting or is not owned by the current project')

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 @@ -423,6 +434,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
28 changes: 26 additions & 2 deletions firebase_admin/_user_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
import base64
from collections import defaultdict
import json
from typing import Optional
from urllib import parse
import warnings

import requests

Expand Down Expand Up @@ -489,15 +491,30 @@ class ActionCodeSettings:
Used when invoking the email action link generation APIs.
"""

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):
def __init__(
self,
url: str,
handle_code_in_app: Optional[bool] = None,
dynamic_link_domain: Optional[str] = None,
ios_bundle_id: Optional[str] = None,
android_package_name: Optional[str] = None,
android_install_app: Optional[str] = None,
android_minimum_version: Optional[str] = None,
link_domain: Optional[str] = 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 +552,13 @@ def encode_action_code_settings(settings):
f'Invalid value provided for dynamic_link_domain: {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(
f'Invalid value provided for link_domain: {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 @@ -1363,7 +1364,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 @@ -1374,6 +1376,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 @@ -1496,6 +1499,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) == ('The provided hosting link domain is not configured in '
'Firebase Hosting or is not owned by the current project '
'(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 @@ -1534,6 +1554,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