Skip to content

Commit 3c504e6

Browse files
authored
Remove (base64) 'REDACTED' passwords from user records. (#352)
These values *look* like passwords hashes, but aren't, leading to potential confusion. Additionally, added docs to CONTRIBUTING.md detailing how to add the permission that causes password hashes to be properly returned as well as adjusting the test failure message should the developer not add that permission. b/141189502
1 parent 3cab0c1 commit 3c504e6

File tree

4 files changed

+46
-10
lines changed

4 files changed

+46
-10
lines changed

CONTRIBUTING.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ pylint firebase_admin
115115

116116
However, it is recommended that you use the [`lint.sh`](lint.sh) bash script to invoke
117117
pylint. This script will run the linter on both `firebase_admin` and the corresponding
118-
`tests` module. It suprresses some of the noisy warnings that get generated
118+
`tests` module. It suppresses some of the noisy warnings that get generated
119119
when running pylint on test code. Note that by default `lint.sh` will only
120120
validate the locally modified source files. To validate all source files,
121121
pass `all` as an argument.
@@ -181,13 +181,23 @@ Then set up your Firebase/GCP project as follows:
181181
to set up Firestore either in the locked mode or in the test mode.
182182
2. Enable password auth: Select "Authentication" from the "Develop" menu in
183183
Firebase Console. Select the "Sign-in method" tab, and enable the
184-
"Email/Password" sign-in method.
184+
"Email/Password" sign-in method, including the Email link (passwordless
185+
sign-in) option.
186+
185187
3. Enable the IAM API: Go to the
186188
[Google Cloud Platform Console](https://console.cloud.google.com) and make
187189
sure your Firebase/GCP project is selected. Select "APIs & Services >
188190
Dashboard" from the main menu, and click the "ENABLE APIS AND SERVICES"
189191
button. Search for and enable the "Identity and Access Management (IAM)
190192
API".
193+
4. Grant your service account the 'Firebase Authentication Admin' role. This is
194+
required to ensure that exported user records contain the password hashes of
195+
the user accounts:
196+
1. Go to [Google Cloud Platform Console / IAM & admin](https://console.cloud.google.com/iam-admin).
197+
2. Find your service account in the list, and click the 'pencil' icon to edit it's permissions.
198+
3. Click 'ADD ANOTHER ROLE' and choose 'Firebase Authentication Admin'.
199+
4. Click 'SAVE'.
200+
191201

192202
Now you can invoke the integration test suite as follows:
193203

firebase_admin/_user_mgt.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414

1515
"""Firebase user management sub module."""
1616

17+
import base64
1718
import json
18-
1919
import requests
2020
import six
2121
from six.moves import urllib
@@ -26,6 +26,7 @@
2626

2727
MAX_LIST_USERS_RESULTS = 1000
2828
MAX_IMPORT_USERS_SIZE = 1000
29+
B64_REDACTED = base64.b64encode(b'REDACTED')
2930

3031

3132
class Sentinel(object):
@@ -257,9 +258,17 @@ def password_hash(self):
257258
If the Firebase Auth hashing algorithm (SCRYPT) was used to create the user account, this
258259
is the base64-encoded password hash of the user. If a different hashing algorithm was
259260
used to create this user, as is typical when migrating from another Auth system, this
260-
is an empty string. If no password is set, this is ``None``.
261+
is an empty string. If no password is set, or if the service account doesn't have permission
262+
to read the password, then this is ``None``.
261263
"""
262-
return self._data.get('passwordHash')
264+
password_hash = self._data.get('passwordHash')
265+
266+
# If the password hash is redacted (probably due to missing permissions) then clear it out,
267+
# similar to how the salt is returned. (Otherwise, it *looks* like a b64-encoded hash is
268+
# present, which is confusing.)
269+
if password_hash == B64_REDACTED:
270+
return None
271+
return password_hash
263272

264273
@property
265274
def password_salt(self):
@@ -268,7 +277,8 @@ def password_salt(self):
268277
If the Firebase Auth hashing algorithm (SCRYPT) was used to create the user account, this
269278
is the base64-encoded password salt of the user. If a different hashing algorithm was
270279
used to create this user, as is typical when migrating from another Auth system, this is
271-
an empty string. If no password is set, this is ``None``.
280+
an empty string. If no password is set, or if the service account doesn't have permission to
281+
read the password, then this is ``None``.
272282
"""
273283
return self._data.get('salt')
274284

integration/test_auth.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,10 @@ def test_get_user(new_user_with_params):
220220
assert provider_ids == ['password', 'phone']
221221

222222
def test_list_users(new_user_list):
223+
err_msg_template = (
224+
'Missing {field} field. A common cause would be forgetting to add the "Firebase ' +
225+
'Authentication Admin" permission. See instructions in CONTRIBUTING.md')
226+
223227
fetched = []
224228
# Test exporting all user accounts.
225229
page = auth.list_users()
@@ -228,8 +232,10 @@ def test_list_users(new_user_list):
228232
assert isinstance(user, auth.ExportedUserRecord)
229233
if user.uid in new_user_list:
230234
fetched.append(user.uid)
231-
assert user.password_hash is not None
232-
assert user.password_salt is not None
235+
assert user.password_hash is not None, (
236+
err_msg_template.format(field='password_hash'))
237+
assert user.password_salt is not None, (
238+
err_msg_template.format(field='password_salt'))
233239
page = page.get_next_page()
234240
assert len(fetched) == len(new_user_list)
235241

@@ -239,8 +245,10 @@ def test_list_users(new_user_list):
239245
assert isinstance(user, auth.ExportedUserRecord)
240246
if user.uid in new_user_list:
241247
fetched.append(user.uid)
242-
assert user.password_hash is not None
243-
assert user.password_salt is not None
248+
assert user.password_hash is not None, (
249+
err_msg_template.format(field='password_hash'))
250+
assert user.password_salt is not None, (
251+
err_msg_template.format(field='password_salt'))
244252
assert len(fetched) == len(new_user_list)
245253

246254
def test_create_user(new_user):

tests/test_user_mgt.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
"""Test cases for the firebase_admin._user_mgt module."""
1616

17+
import base64
1718
import json
1819
import time
1920

@@ -152,6 +153,13 @@ def test_exported_record_empty_password(self):
152153
assert user.password_hash == ''
153154
assert user.password_salt == ''
154155

156+
def test_redacted_passwords_cleared(self):
157+
user = auth.ExportedUserRecord({
158+
'localId': 'user',
159+
'passwordHash': base64.b64encode(b'REDACTED'),
160+
})
161+
assert user.password_hash is None
162+
155163
def test_custom_claims(self):
156164
user = auth.UserRecord({
157165
'localId' : 'user',

0 commit comments

Comments
 (0)