diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8c58e63e..7b4a0ea8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -115,7 +115,7 @@ pylint firebase_admin However, it is recommended that you use the [`lint.sh`](lint.sh) bash script to invoke pylint. This script will run the linter on both `firebase_admin` and the corresponding -`tests` module. It suprresses some of the noisy warnings that get generated +`tests` module. It suppresses some of the noisy warnings that get generated when running pylint on test code. Note that by default `lint.sh` will only validate the locally modified source files. To validate all source files, pass `all` as an argument. @@ -181,13 +181,23 @@ Then set up your Firebase/GCP project as follows: to set up Firestore either in the locked mode or in the test mode. 2. Enable password auth: Select "Authentication" from the "Develop" menu in Firebase Console. Select the "Sign-in method" tab, and enable the - "Email/Password" sign-in method. + "Email/Password" sign-in method, including the Email link (passwordless + sign-in) option. + 3. Enable the IAM API: Go to the [Google Cloud Platform Console](https://console.cloud.google.com) and make sure your Firebase/GCP project is selected. Select "APIs & Services > Dashboard" from the main menu, and click the "ENABLE APIS AND SERVICES" button. Search for and enable the "Identity and Access Management (IAM) API". +4. Grant your service account the 'Firebase Authentication Admin' role. This is + required to ensure that exported user records contain the password hashes of + the user accounts: + 1. Go to [Google Cloud Platform Console / IAM & admin](https://console.cloud.google.com/iam-admin). + 2. Find your service account in the list, and click the 'pencil' icon to edit it's permissions. + 3. Click 'ADD ANOTHER ROLE' and choose 'Firebase Authentication Admin'. + 4. Click 'SAVE'. + Now you can invoke the integration test suite as follows: diff --git a/firebase_admin/_user_mgt.py b/firebase_admin/_user_mgt.py index 867b6dd8..2e10fac1 100644 --- a/firebase_admin/_user_mgt.py +++ b/firebase_admin/_user_mgt.py @@ -14,8 +14,8 @@ """Firebase user management sub module.""" +import base64 import json - import requests import six from six.moves import urllib @@ -26,6 +26,7 @@ MAX_LIST_USERS_RESULTS = 1000 MAX_IMPORT_USERS_SIZE = 1000 +B64_REDACTED = base64.b64encode(b'REDACTED') class Sentinel(object): @@ -257,9 +258,17 @@ def password_hash(self): If the Firebase Auth hashing algorithm (SCRYPT) was used to create the user account, this is the base64-encoded password hash of the user. If a different hashing algorithm was used to create this user, as is typical when migrating from another Auth system, this - is an empty string. If no password is set, this is ``None``. + is an empty string. If no password is set, or if the service account doesn't have permission + to read the password, then this is ``None``. """ - return self._data.get('passwordHash') + password_hash = self._data.get('passwordHash') + + # If the password hash is redacted (probably due to missing permissions) then clear it out, + # similar to how the salt is returned. (Otherwise, it *looks* like a b64-encoded hash is + # present, which is confusing.) + if password_hash == B64_REDACTED: + return None + return password_hash @property def password_salt(self): @@ -268,7 +277,8 @@ def password_salt(self): If the Firebase Auth hashing algorithm (SCRYPT) was used to create the user account, this is the base64-encoded password salt of the user. If a different hashing algorithm was used to create this user, as is typical when migrating from another Auth system, this is - an empty string. If no password is set, this is ``None``. + an empty string. If no password is set, or if the service account doesn't have permission to + read the password, then this is ``None``. """ return self._data.get('salt') diff --git a/integration/test_auth.py b/integration/test_auth.py index 1a4bacce..9d5d0dfe 100644 --- a/integration/test_auth.py +++ b/integration/test_auth.py @@ -220,6 +220,10 @@ def test_get_user(new_user_with_params): assert provider_ids == ['password', 'phone'] def test_list_users(new_user_list): + err_msg_template = ( + 'Missing {field} field. A common cause would be forgetting to add the "Firebase ' + + 'Authentication Admin" permission. See instructions in CONTRIBUTING.md') + fetched = [] # Test exporting all user accounts. page = auth.list_users() @@ -228,8 +232,10 @@ def test_list_users(new_user_list): assert isinstance(user, auth.ExportedUserRecord) if user.uid in new_user_list: fetched.append(user.uid) - assert user.password_hash is not None - assert user.password_salt is not None + assert user.password_hash is not None, ( + err_msg_template.format(field='password_hash')) + assert user.password_salt is not None, ( + err_msg_template.format(field='password_salt')) page = page.get_next_page() assert len(fetched) == len(new_user_list) @@ -239,8 +245,10 @@ def test_list_users(new_user_list): assert isinstance(user, auth.ExportedUserRecord) if user.uid in new_user_list: fetched.append(user.uid) - assert user.password_hash is not None - assert user.password_salt is not None + assert user.password_hash is not None, ( + err_msg_template.format(field='password_hash')) + assert user.password_salt is not None, ( + err_msg_template.format(field='password_salt')) assert len(fetched) == len(new_user_list) def test_create_user(new_user): diff --git a/tests/test_user_mgt.py b/tests/test_user_mgt.py index 3c19a98d..8b1bab13 100644 --- a/tests/test_user_mgt.py +++ b/tests/test_user_mgt.py @@ -14,6 +14,7 @@ """Test cases for the firebase_admin._user_mgt module.""" +import base64 import json import time @@ -152,6 +153,13 @@ def test_exported_record_empty_password(self): assert user.password_hash == '' assert user.password_salt == '' + def test_redacted_passwords_cleared(self): + user = auth.ExportedUserRecord({ + 'localId': 'user', + 'passwordHash': base64.b64encode(b'REDACTED'), + }) + assert user.password_hash is None + def test_custom_claims(self): user = auth.UserRecord({ 'localId' : 'user',