8000 fixes for review comments · firebase/firebase-admin-python@95beb1a · GitHub
[go: up one dir, main page]

Skip to content

Commit 95beb1a

Browse files
hshahbridge2solutionshardikns
authored andcommitted
fixes for review comments
1 parent 8e7df34 commit 95beb1a

File tree

6 files changed

+179
-127
lines changed

6 files changed

+179
-127
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Unreleased
22

33
- [added] Added `generate_password_reset_link()`,
4-
`generate_email_verification_link()` and `generate_email_sign_in_link()`
4+
`generate_email_verification_link()` and `generate_sign_in_with_email_link()`
55
methods to the `auth` API.
66
- [added] Migrated the `auth` user management API to the
77
new Identity Toolkit endpoint.

firebase_admin/_auth_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash', 'exp', 'iat',
2727
'iss', 'jti', 'nbf', 'nonce', 'sub', 'firebase',
2828
])
29-
VALID_ACTION_TYPE = set(['VERIFY_EMAIL', 'EMAIL_SIGNIN', 'PASSWORD_RESET'])
29+
VALID_EMAIL_ACTION_TYPES = set(['VERIFY_EMAIL', 'EMAIL_SIGNIN', 'PASSWORD_RESET'])
3030

3131

3232
def validate_uid(uid, required=False):
@@ -184,7 +184,7 @@ def validate_custom_claims(custom_claims, required=False):
184184
return claims_str
185185

186186
def validate_action_type(action_type):
187-
if action_type not in VALID_ACTION_TYPE:
187+
if action_type not in VALID_EMAIL_ACTION_TYPES:
188188
raise ValueError('Invalid action type provided action_type: {0}. \
189-
Valid values are {1}'.format(action_type, VALID_ACTION_TYPE))
189+
Valid values are {1}'.format(action_type, ', '.join(VALID_EMAIL_ACTION_TYPES)))
190190
return action_type

firebase_admin/_user_mgt.py

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
USER_DELETE_ERROR = 'USER_DELETE_ERROR'
3232
USER_IMPORT_ERROR = 'USER_IMPORT_ERROR'
3333
USER_DOWNLOAD_ERROR = 'LIST_USERS_ERROR'
34-
USER_LINK_GENERATE_ERROR = 'USER_LINK_GENERATE_ERROR'
34+
GENERATE_EMAIL_ACTION_LINK_ERROR = 'GENERATE_EMAIL_ACTION_LINK_ERROR'
3535

3636
MAX_LIST_USERS_RESULTS = 1000
3737
MAX_IMPORT_USERS_SIZE = 1000
@@ -396,56 +396,64 @@ def encode_action_code_settings(settings):
396396
settings - ``ActionCodeSettings`` object provided to be encoded
397397
returns - dict of parameters to be passed for link gereration.
398398
"""
399-
if not isinstance(settings, ActionCodeSettings):
400-
raise ValueError('Invalid data argument: {0}. Must be a dictionary.'.format(settings))
401399

402400
parameters = {}
403-
# Validate url
404-
if settings.url:
405-
try:
406-
parsed = urllib.parse.urlparse(settings.url)
407-
if not parsed.netloc:
408-
raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url))
409-
parameters['continueUrl'] = settings.url
410-
except Exception:
411-
raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url))
401+
# url
402+
if not settings.url:
403+
raise ValueError("Dynamic action links url is mandatory")
412404

413-
# Validate boolean types
414-
for field in ['handle_code_in_app', 'android_install_app']:
415-
value = getattr(settings, field, None)
416-
if value != None and not isinstance(value, bool):
417-
raise ValueError('Invalid value provided for {0}: {1}'.format(field, value))
418-
419-
# Validate string types
420-
for field in ['dynamic_link_domain', 'ios_bundle_id',
421-
'android_package_name', 'android_minimum_version']:
422-
value = getattr(settings, field, None)
423-
if value != None and not isinstance(value, six.string_types):
424-
raise ValueError('Invalid value provided for {0}: {1}'.format(field, value))
405+
try:
406+
parsed = urllib.parse.urlparse(settings.url)
407+
if not parsed.netloc:
408+
raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url))
409+
parameters['continueUrl'] = settings.url
410+
except Exception:
411+
raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url))
425412

426413
# handle_code_in_app
F438 427-
if settings.handle_code_in_app != None:
414+
if settings.handle_code_in_app is not None:
415+
if not isinstance(settings.handle_code_in_app, bool):
416+
raise ValueError('Invalid value provided for handle_code_in_app: {0}'
417+
.format(settings.handle_code_in_app))
428418
parameters['canHandleCodeInApp'] = settings.handle_code_in_app
429419

430420
# dynamic_link_domain
431-
if settings.dynamic_link_domain != None:
421+
if settings.dynamic_link_domain is not None:
422+
if not isinstance(settings.dynamic_link_domain, six.string_types):
423+
raise ValueError('Invalid value provided for dynamic_link_domain: {0}'
424+
.format(settings.dynamic_link_domain))
432425
parameters['dynamicLinkDomain'] = settings.dynamic_link_domain
433426

434427
# ios_bundle_id
435-
if settings.ios_bundle_id:
428+
if settings.ios_bundle_id is not None:
429+
if not isinstance(settings.ios_bundle_id, six.string_types):
430+
raise ValueError('Invalid value provided for ios_bundle_id: {0}'
431+
.format(settings.ios_bundle_id))
436432
parameters['iosBundleId'] = settings.ios_bundle_id
437433

438434
# android_* attributes
439435
if (settings.android_minimum_version or settings.android_install_app) \
440436
and not settings.android_package_name:
441437
raise ValueError("Android package name is required when specifying other Android settings")
442438

443-
if settings.android_package_name:
439+
if settings.android_package_name is not None:
440+
if not isinstance(settings.android_package_name, six.string_types):
441+
raise ValueError('Invalid value provided for android_package_name: {0}'
442+
.format(settings.android_package_name))
444443
parameters['androidPackageName'] = settings.android_package_name
445-
if settings.android_minimum_version:
444+
445+
if settings.android_minimum_version is not None:
446+
if not isinstance(settings.android_minimum_version, six.string_types):
447+
raise ValueError('Invalid value provided for android_minimum_version: {0}'
448+
.format(settings.android_minimum_version))
446449
parameters['androidMinimumVersion'] = settings.android_minimum_version
447-
if settings.android_install_app:
450+
451+
if settings.android_install_app is not None:
452+
if not isinstance(settings.android_install_app, bool):
453+
raise ValueError('Invalid value provided for android_install_app: {0}'
454+
.format(settings.android_install_app))
448455
parameters['androidInstallApp'] = settings.android_install_app
456+
449457
return parameters
450458

451459
class UserManager(object):
@@ -635,18 +643,16 @@ def generate_email_action_link(self, action_type, email, action_code_settings=No
635643
}
636644

637645
if action_code_settings:
638-
if not isinstance(action_code_settings, ActionCodeSettings):
639-
raise ValueError("'action_code_settings' parameter should be " + \
640-
"of type ActionCodeSettings")
641646
payload.update(encode_action_code_settings(action_code_settings))
642647

643648
try:
644649
response = self._client.body('post', '/accounts:sendOobCode', json=payload)
645650
except requests.exceptions.RequestException as error:
646-
self._handle_http_error(USER_LINK_GENERATE_ERROR, 'Failed to generate link.', error)
651+
self._handle_http_error(GENERATE_EMAIL_ACTION_LINK_ERROR, 'Failed to generate link.',
652+
error)
647653
else:
648654
if not response or not response.get('oobLink'):
649-
raise ApiCallError(USER_LINK_GENERATE_ERROR, 'Failed to generate link.')
655+
raise ApiCallError(GENERATE_EMAIL_ACTION_LINK_ERROR, 'Failed to generate link.')
650656
return response.get('oobLink')
651657

652658
def _handle_http_error(self, code, msg, error):

firebase_admin/auth.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ def generate_password_reset_link(email, action_code_settings=None, app=None):
461461
email: The email of the user whose password is to be reset.
462462
action_code_settings: ``ActionCodeSettings`` instance (optional). Defines whether
463463
the link is to be handled by a mobile app and the additional state information to be
464-
passed in the deep link, etc.
464+
passed in the deep link.
465465
app: An App instance (optional).
466466
Returns:
467467
link: The password reset link created by API
@@ -485,7 +485,7 @@ def generate_email_verification_link(email, action_code_settings=None, app=None)
485485
email: The email of the user to be verified.
486486
action_code_settings: ``ActionCodeSettings`` instance (optional). Defines whether
487487
the link is to be handled by a mobile app and the additional state information to be
488-
passed in the deep link, etc.
488+
passed in the deep link.
489489
app: An App instance (optional).
490490
Returns:
491491
link: The email verification link created by API
@@ -509,7 +509,7 @@ def generate_sign_in_with_email_link(email, action_code_settings, app=None):
509509
email: The email of the user signing in.
510510
action_code_settings: ``ActionCodeSettings`` instance. Defines whether
511511
the link is to be handled by a mobile app and the additional state information to be
512-
passed in the deep link, etc.
512+
passed in the deep link.
513513
app: An App instance (optional).
514514
Returns:
515515
link: The email sign in link created by API

integration/test_auth.py

Lines changed: 75 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@
3131

3232
_verify_token_url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken'
3333
_verify_password_url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword'
34+
_password_reset_url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/resetPassword'
35+
_verify_email_url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccountInfo'
36+
_email_sign_in_url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/emailLinkSignin'
3437

38+
ACTION_LINK_CONTINUE_URL = 'http://localhost?a=1&b=5#f=1'
3539

3640
def _sign_in(custom_token, api_key):
3741
body = {'token' : custom_token.decode(), 'returnSecureToken' : True}
@@ -55,6 +59,35 @@ def _random_id():
5559
def _random_phone():
5660
return '+1' + ''.join([str(random.randint(0, 9)) for _ in range(0, 10)])
5761

62+
def _reset_password(oob_code, new_password, api_key):
63+
body = {'oobCode': oob_code, 'newPassword': new_password}
64+
params = {'key' : api_key}
65+
resp = requests.request('post', _password_reset_url, params=params, json=body)
66+
resp.raise_for_status()
67+
return resp.json().get('email')
68+
69+
def _verify_email(oob_code, api_key):
70+
body = {'oobCode': oob_code}
71+
params = {'key' : api_key}
72+
resp = requests.request('post', _verify_email_url, params=params, json=body)
73+
resp.raise_for_status()
74+
return resp.json().get('email')
75+
76+
def _sign_in_with_email_link(email, oob_code, api_key):
77+
body = {'oobCode': oob_code, 'email': email}
78+
params = {'key' : api_key}
79+
resp = requests.request('post', _email_sign_in_url, params=params, json=body)
80+
resp.raise_for_status()
81+
return resp.json().get('idToken')
82+
83+
def _validate_link_url(link, check_continue_url=True):
84+
assert isinstance(link, six.string_types)
85+
query = six.moves.urllib.parse.urlparse(link).query
86+
query_dict = dict(six.moves.urllib.parse.parse_qsl(query))
87+
if check_continue_url:
88+
assert query_dict['continueUrl'] == ACTION_LINK_CONTINUE_URL
89+
return query_dict['oobCode']
90+
5891
def test_custom_token(api_key):
5992
custom_token = auth.create_custom_token('user1')
6093
id_token = _sign_in(custom_token, api_key)
@@ -152,6 +185,18 @@ def new_user_list():
152185
for uid in users:
153186
auth.delete_user(uid)
154187

188+
@pytest.fixture
189+
def new_user_email_unverified():
190+
random_id, email = _random_id()
191+
user = auth.create_user(
192+
uid=random_id,
193+
email=email,
194+
email_verified=False,
195+
password='password'
196+
)
197+
yield user
198+
auth.delete_user(user.uid)
199+
155200
def test_get_user(new_user_with_params):
156201
user = auth.get_user(new_user_with_params.uid)
157202
assert user.uid == new_user_with_params.uid
@@ -373,37 +418,41 @@ def test_import_users_with_password(api_key):
373418
finally:
374419
auth.delete_user(uid)
375420

376-
@pytest.fixture
377-
def action_code_settings():
378-
return auth.ActionCodeSettings('http://localhost')
379-
380-
def _validate_link_url(link):
381-
assert isinstance(link, six.string_types)
382-
six.moves.urllib.parse.urlparse(link)
383-
384-
def test_password_reset(new_user_with_params):
385-
link = auth.generate_password_reset_link(new_user_with_params.email)
386-
_validate_link_url(link)
387-
388-
def test_email_verification(new_user_with_params):
389-
link = auth.generate_email_verification_link(new_user_with_params.email)
390-
_validate_link_url(link)
391-
392-
def test_password_reset_with_settings(new_user_with_params, action_code_settings):
393-
link = auth.generate_password_reset_link(new_user_with_params.email,
421+
def test_password_reset(new_user_email_unverified, api_key):
422+
link = auth.generate_password_reset_link(new_user_email_unverified.email)
423+
oob_code = _validate_link_url(link, check_continue_url=False)
424+
assert new_user_email_unverified.email == _reset_password(oob_code, "newPassword", api_key)
425+
assert auth.get_user(new_user_email_unverified.uid).email_verified
426+
427+
def test_email_verification(new_user_email_unverified, api_key):
428+
link = auth.generate_email_verification_link(new_user_email_unverified.email)
429+
oob_code = _validate_link_url(link, check_continue_url=False)
430+
assert new_user_email_unverified.email == _verify_email(oob_code, api_key)
431+
assert auth.get_user(new_user_email_unverified.uid).email_verified
432+
433+
def test_password_reset_with_settings(new_user_email_unverified, api_key):
434+
action_code_settings = auth.ActionCodeSettings(ACTION_LINK_CONTINUE_URL)
435+
link = auth.generate_password_reset_link(new_user_email_unverified.email,
394436
action_code_settings=action_code_settings)
395-
_validate_link_url(link)
437+
oob_code = _validate_link_url(link)
438+
assert new_user_email_unverified.email == _reset_password(oob_code, "newPassword", api_key)
439+
assert auth.get_user(new_user_email_unverified.uid).email_verified
396440

397-
def test_email_verification_with_settings(new_user_with_params, action_code_settings):
398-
link = auth.generate_email_verification_link(new_user_with_params.email,
441+
def test_email_verification_with_settings(new_user_email_unverified, api_key):
442+
action_code_settings = auth.ActionCodeSettings(ACTION_LINK_CONTINUE_URL)
443+
link = auth.generate_email_verification_link(new_user_email_unverified.email,
399444
action_code_settings=action_code_settings)
400-
_validate_link_url(link)
445+
oob_code = _validate_link_url(link)
446+
assert new_user_email_unverified.email == _verify_email(oob_code, api_key)
447+
assert auth.get_user(new_user_email_unverified.uid).email_verified
401448

402-
def test_email_sign_in_with_settings(new_user_with_params, action_code_settings):
403-
link = auth.generate_sign_in_with_email_link(new_user_with_params.email,
449+
def test_email_sign_in_with_settings(new_user_email_unverified, api_key):
450+
action_code_settings = auth.ActionCodeSettings(ACTION_LINK_CONTINUE_URL)
451+
link = auth.generate_sign_in_with_email_link(new_user_email_unverified.email,
404452
action_code_settings=action_code_settings)
405-
_validate_link_url(link)
406-
453+
oob_code = _validate_link_url(link)
454+
assert _sign_in_with_email_link(new_user_email_unverified.email, oob_code, api_key)
455+
assert auth.get_user(new_user_email_unverified.uid).email_verified
407456

408457
class CredentialWrapper(credentials.Base):
409458
"""A custom Firebase credential that wraps an OAuth2 token."""

0 commit comments

Comments
 (0)
0