Repository: firebase/firebase-admin-java
Pull request #314 (2019-10-01 22:16:32)

Remove (base64) 'REDACTED' passwords from user records.
Base64 'REDACTED' passwords 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


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 467c0d3..11012bf 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -136,9 +136,13 @@ Create a new project in the [Firebase console](https://console.firebase.google.c
 not already have one. Use a separate, dedicated project for integration tests since the test suite
 makes a large number of writes to the Firebase realtime database. Download the service account
 private key from the "Settings > Service Accounts" page of the project, and save it as
-`integration_cert.json` at the root of the codebase. Also obtain the web API key of the project
-from the "Settings > General" page, and save it as `integration_apikey.txt` at the root of the
-codebase. Now run the following command to invoke the integration test suite:
+`integration_cert.json` at the root of the codebase. Grant your service account the `Firebase
+Authentication Admin` role at
+[Google Cloud Platform Console / IAM & admin](https://console.cloud.google.com/iam-admin). This is
+required to ensure that exported user records contain the password hashes of the user accounts.
+Also obtain the web API key of the project from the "Settings > General" page, and save it as
+`integration_apikey.txt` at the root of the codebase. Now run the following command to invoke the
+integration test suite:
 
 ```
 mvn verify
diff --git a/src/main/java/com/google/firebase/auth/ExportedUserRecord.java b/src/main/java/com/google/firebase/auth/ExportedUserRecord.java
index e367f55..9fdfc9c 100644
--- a/src/main/java/com/google/firebase/auth/ExportedUserRecord.java
+++ b/src/main/java/com/google/firebase/auth/ExportedUserRecord.java
@@ -17,6 +17,7 @@
 package com.google.firebase.auth;
 
 import com.google.api.client.json.JsonFactory;
+import com.google.common.io.BaseEncoding;
 import com.google.firebase.auth.internal.DownloadAccountResponse.User;
 import com.google.firebase.internal.Nullable;
 
@@ -28,10 +29,17 @@ public class ExportedUserRecord extends UserRecord {
 
   private final String passwordHash;
   private final String passwordSalt;
+  private static final String REDACTED_BASE64 = BaseEncoding.base64Url().encode(
+      "REDACTED".getBytes());
 
   ExportedUserRecord(User response, JsonFactory jsonFactory) {
     super(response, jsonFactory);
-    this.passwordHash = response.getPasswordHash();
+    String passwordHash = response.getPasswordHash();
+    if (passwordHash != null && !passwordHash.equals(REDACTED_BASE64)) {
+      this.passwordHash = passwordHash;
+    } else {
+      this.passwordHash = null;
+    }
     this.passwordSalt = response.getPasswordSalt();
   }
 


Test patch:
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
index badb9ea..6a8361d 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
@@ -264,7 +264,9 @@ public class FirebaseAuthIT {
         for (ExportedUserRecord user : page.getValues()) {
           if (uids.contains(user.getUid())) {
             collected.incrementAndGet();
-            assertNotNull(user.getPasswordHash());
+            assertNotNull("Missing passwordHash field. A common cause would be "
+                + "forgetting to add the \"Firebase Authentication Admin\" permission. See "
+                + "instructions in CONTRIBUTING.md", user.getPasswordHash());
             assertNotNull(user.getPasswordSalt());
           }
         }
diff --git a/src/test/java/com/google/firebase/auth/ListUsersPageTest.java b/src/test/java/com/google/firebase/auth/ListUsersPageTest.java
index 1c6282f..fb4a7d2 100644
--- a/src/test/java/com/google/firebase/auth/ListUsersPageTest.java
+++ b/src/test/java/com/google/firebase/auth/ListUsersPageTest.java
@@ -26,6 +26,7 @@ import static org.junit.Assert.fail;
 import com.google.api.client.googleapis.util.Utils;
 import com.google.api.client.json.JsonFactory;
 import com.google.common.collect.ImmutableList;
+import com.google.common.io.BaseEncoding;
 import com.google.firebase.auth.ListUsersPage.ListUsersResult;
 import com.google.firebase.auth.internal.DownloadAccountResponse;
 import java.io.IOException;
@@ -38,6 +39,9 @@ import org.junit.Test;
 
 public class ListUsersPageTest {
 
+  private static final String REDACTED_BASE64 = BaseEncoding.base64Url().encode(
+      "REDACTED".getBytes());
+
   @Test
   public void testSinglePage() throws FirebaseAuthException, IOException {
     TestUserSource source = new TestUserSource(3);
@@ -55,6 +59,30 @@ public class ListUsersPageTest {
     assertNull(source.calls.get(0));
   }
 
+  @Test
+  public void testRedactedPasswords() throws FirebaseAuthException, IOException {
+    ListUsersResult result = new ListUsersResult(
+        ImmutableList.of(
+            newUser("user0", REDACTED_BASE64),
+            newUser("user1", REDACTED_BASE64),
+            newUser("user2", REDACTED_BASE64)),
+        ListUsersPage.END_OF_LIST);
+    TestUserSource source = new TestUserSource(result);
+    ListUsersPage page = new ListUsersPage.PageFactory(source).create();
+    assertFalse(page.hasNextPage());
+    assertEquals(ListUsersPage.END_OF_LIST, page.getNextPageToken());
+    assertNull(page.getNextPage());
+
+    ImmutableList<ExportedUserRecord> users = ImmutableList.copyOf(page.getValues());
+    assertEquals(3, users.size());
+    for (int i = 0; i < 3; i++) {
+      assertEquals("user" + i, users.get(i).getUid());
+      assertNull(users.get(i).getPasswordHash());
+    }
+    assertEquals(1, source.calls.size());
+    assertNull(source.calls.get(0));
+  }
+
   @Test
   public void testMultiplePages() throws FirebaseAuthException, IOException {
     ListUsersResult result = new ListUsersResult(
@@ -326,6 +354,14 @@ public class ListUsersPageTest {
     return new ExportedUserRecord(parsed, jsonFactory);
   }
 
+  private static ExportedUserRecord newUser(String uid, String passwordHash) throws IOException {
+    JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
+    DownloadAccountResponse.User parsed = jsonFactory.fromString(
+        String.format("{\"localId\":\"%s\", \"passwordHash\":\"%s\"}", uid, passwordHash),
+        DownloadAccountResponse.User.class);
+    return new ExportedUserRecord(parsed, jsonFactory);
+  }
+
   private static class TestUserSource implements ListUsersPage.UserSource {
 
     private ListUsersResult result;