Repository: firebase/firebase-admin-java
Pull request #34 (2017-05-26 00:28:08)

Implementing the User Management API
* Introduces a set of new methods to `FirebaseAuth` to create and manage user accounts.
* Introduces new public types `User`, `UserMetadata` and `ProviderUserInfo` for passing around user data.
* New unit and integration tests to cover the new user cases.

Related issue: #33 



hiranya911: The API for this is still being reviewed, but I think it's stable enough to start a code review. I don't expect major changes except for perhaps a trivial renaming of couple of things.

Patch:
diff --git a/pom.xml b/pom.xml
index 66bcb93..9aecd08 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
 
     <groupId>com.google.firebase</groupId>
     <artifactId>firebase-admin</artifactId>
-    <version>5.0.1-SNAPSHOT</version>
+    <version>5.0.2-SNAPSHOT</version>
     <packaging>jar</packaging>
 
     <name>firebase-admin</name>
@@ -320,6 +320,11 @@
             <artifactId>google-api-client-gson</artifactId>
             <version>1.22.0</version>
         </dependency>
+        <dependency>
+            <groupId>com.google.http-client</groupId>
+            <artifactId>google-http-client</artifactId>
+            <version>1.22.0</version>
+        </dependency>
         <dependency>
             <groupId>org.json</groupId>
             <artifactId>json</artifactId>
diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
index 51be4e6..3b20145 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
@@ -16,18 +16,24 @@
 
 package com.google.firebase.auth;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
 import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
+import com.google.api.client.googleapis.util.Utils;
 import com.google.api.client.json.JsonFactory;
 import com.google.api.client.json.gson.GsonFactory;
 import com.google.api.client.util.Clock;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseException;
 import com.google.firebase.ImplFirebaseTrampolines;
 import com.google.firebase.auth.internal.FirebaseTokenFactory;
 import com.google.firebase.auth.internal.FirebaseTokenVerifier;
 import com.google.firebase.internal.FirebaseService;
+import com.google.firebase.internal.GetTokenResult;
 import com.google.firebase.internal.NonNull;
 import com.google.firebase.tasks.Continuation;
 import com.google.firebase.tasks.Task;
@@ -51,6 +57,7 @@ public class FirebaseAuth {
   private final FirebaseApp firebaseApp;
   private final GooglePublicKeysManager googlePublicKeysManager;
   private final Clock clock;
+  private final FirebaseUserManager userManager;
 
   private FirebaseAuth(FirebaseApp firebaseApp) {
     this(firebaseApp, FirebaseTokenVerifier.DEFAULT_KEY_MANAGER, Clock.SYSTEM);
@@ -66,6 +73,7 @@ public class FirebaseAuth {
     this.firebaseApp = firebaseApp;
     this.googlePublicKeysManager = googlePublicKeysManager;
     this.clock = clock;
+    this.userManager = new FirebaseUserManager(jsonFactory, Utils.getDefaultTransport());
   }
 
   /**
@@ -195,6 +203,110 @@ public class FirebaseAuth {
             });
   }
 
+  /**
+   * Gets the user data corresponding to the specified user ID.
+   *
+   * @param uid A user ID string.
+   * @return A {@link Task} which will complete successfully with a {@link User} instance.
+   *     If an error occurs while retrieving user data or if the specified user ID does not exist,
+   *     the task fails with a FirebaseAuthException.
+   * @throws IllegalArgumentException If the user ID string is null or empty.
+   */
+  public Task<User> getUser(final String uid) {
+    checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
+    return ImplFirebaseTrampolines.getToken(firebaseApp, false).continueWith(
+        new Continuation<GetTokenResult, User>() {
+          @Override
+          public User then(Task<GetTokenResult> task) throws Exception {
+            return userManager.getUserById(uid, task.getResult().getToken());
+          }
+        });
+  }
+
+  /**
+   * Gets the user data corresponding to the specified user email.
+   *
+   * @param email A user email address string.
+   * @return A {@link Task} which will complete successfully with a {@link User} instance.
+   *     If an error occurs while retrieving user data or if the email address does not correspond
+   *     to a user, the task fails with a FirebaseAuthException.
+   * @throws IllegalArgumentException If the email is null or empty.
+   */
+  public Task<User> getUserByEmail(final String email) {
+    checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty");
+    return ImplFirebaseTrampolines.getToken(firebaseApp, false).continueWith(
+        new Continuation<GetTokenResult, User>() {
+          @Override
+          public User then(Task<GetTokenResult> task) throws Exception {
+            return userManager.getUserByEmail(email, task.getResult().getToken());
+          }
+        });
+  }
+
+  /**
+   * Creates a new user account with the attributes contained in the specified {@link User.Builder}.
+   *
+   * @param builder A non-null {@link User.Builder} instance.
+   * @return A {@link Task} which will complete successfully with a {@link User} instance
+   *     corresponding to the newly created account. If an error occurs while creating the user
+   *     account, the task fails with a FirebaseAuthException.
+   * @throws NullPointerException if the provided builder is null.
+   */
+  public Task<User> createUser(final User.Builder builder) {
+    checkNotNull(builder, "builder must not be null");
+    return ImplFirebaseTrampolines.getToken(firebaseApp, false).continueWith(
+        new Continuation<GetTokenResult, User>() {
+          @Override
+          public User then(Task<GetTokenResult> task) throws Exception {
+            String uid = userManager.createUser(builder, task.getResult().getToken());
+            return userManager.getUserById(uid, task.getResult().getToken());
+          }
+        });
+  }
+
+  /**
+   * Updates an existing user account with the attributes contained in the specified
+   * {@link User.Updater}.
+   *
+   * @param updater A non-null {@link User.Updater} instance.
+   * @return A {@link Task} which will complete successfully with a {@link User} instance
+   *     corresponding to the updated user account. If an error occurs while updating the user
+   *     account, the task fails with a FirebaseAuthException.
+   * @throws NullPointerException if the provided updater is null.
+   */
+  public Task<User> updateUser(final User.Updater updater) {
+    checkNotNull(updater, "updater must not be null");
+    return ImplFirebaseTrampolines.getToken(firebaseApp, false).continueWith(
+        new Continuation<GetTokenResult, User>() {
+          @Override
+          public User then(Task<GetTokenResult> task) throws Exception {
+            userManager.updateUser(updater, task.getResult().getToken());
+            return userManager.getUserById(updater.getUid(), task.getResult().getToken());
+          }
+        });
+  }
+
+  /**
+   * Deletes the user identified by the specified user ID.
+   *
+   * @param uid A user ID string.
+   * @return A {@link Task} which will complete successfully when the specified user account has
+   *     been deleted. If an error occurs while deleting the user account, the task fails with a
+   *     FirebaseAuthException.
+   * @throws IllegalArgumentException If the user ID string is null or empty.
+   */
+  public Task<Void> deleteUser(final String uid) {
+    checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
+    return ImplFirebaseTrampolines.getToken(firebaseApp, false).continueWith(
+        new Continuation<GetTokenResult, Void>() {
+          @Override
+          public Void then(Task<GetTokenResult> task) throws Exception {
+            userManager.deleteUser(uid, task.getResult().getToken());
+            return null;
+          }
+        });
+  }
+
   private static final String SERVICE_ID = FirebaseAuth.class.getName();
 
   private static class FirebaseAuthService extends FirebaseService<FirebaseAuth> {
@@ -206,8 +318,8 @@ public class FirebaseAuth {
     @Override
     public void destroy() {
       // NOTE: We don't explicitly tear down anything here, but public methods of FirebaseAuth
-      // will now fail because calls to getCredential() will hit FirebaseApp.getOptions() which
-      // will throw once the app is deleted.
+      // will now fail because calls to getCredential() and getToken() will hit FirebaseApp,
+      // which will throw once the app is deleted.
     }
   }
 }
diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuthException.java b/src/main/java/com/google/firebase/auth/FirebaseAuthException.java
index f4a741b..2314a69 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseAuthException.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseAuthException.java
@@ -37,7 +37,12 @@ public class FirebaseAuthException extends FirebaseException {
   private final String errorCode;
 
   public FirebaseAuthException(@NonNull String errorCode, @NonNull String detailMessage) {
-    super(detailMessage);
+    this(errorCode, detailMessage, null);
+  }
+
+  public FirebaseAuthException(@NonNull String errorCode, @NonNull String detailMessage,
+                               Throwable throwable) {
+    super(detailMessage, throwable);
     checkArgument(!Strings.isNullOrEmpty(errorCode));
     this.errorCode = errorCode;
   }
diff --git a/src/main/java/com/google/firebase/auth/FirebaseCredentials.java b/src/main/java/com/google/firebase/auth/FirebaseCredentials.java
index 9fac282..aa59b11 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseCredentials.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseCredentials.java
@@ -50,7 +50,8 @@ public class FirebaseCredentials {
   private static final List<String> FIREBASE_SCOPES =
       ImmutableList.of(
           "https://www.googleapis.com/auth/firebase.database",
-          "https://www.googleapis.com/auth/userinfo.email");
+          "https://www.googleapis.com/auth/userinfo.email",
+          "https://www.googleapis.com/auth/identitytoolkit");
 
   private FirebaseCredentials() {
   }
diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
new file mode 100644
index 0000000..f3be905
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.http.json.JsonHttpContent;
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.JsonObjectParser;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.firebase.auth.internal.GetAccountInfoResponse;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * FirebaseUserManager provides methods for interacting with the Google Identity Toolkit via its
+ * REST API. This class does not hold any mutable state, and is thread safe.
+ *
+ * @see <a href="https://developers.google.com/identity/toolkit/web/reference/relyingparty">
+ *   Google Identity Toolkit</a>
+ */
+class FirebaseUserManager {
+
+  static final String USER_NOT_FOUND_ERROR = "USER_NOT_FOUND_ERROR";
+  static final String USER_CREATE_ERROR = "USER_CREATE_ERROR";
+  static final String USER_UPDATE_ERROR = "USER_UPDATE_ERROR";
+  static final String USER_DELETE_ERROR = "USER_DELETE_ERROR";
+  static final String INTERNAL_ERROR = "INTERNAL_ERROR";
+
+  private static final String ID_TOOLKIT_URL =
+      "https://www.googleapis.com/identitytoolkit/v3/relyingparty/";
+
+  private final JsonFactory jsonFactory;
+  private final HttpRequestFactory requestFactory;
+
+  /**
+   * Creates a new FirebaseUserManager instance.
+   *
+   * @param jsonFactory JsonFactory instance used to transform Java objects into JSON and back.
+   * @param transport HttpTransport used to make REST API calls.
+   */
+  FirebaseUserManager(JsonFactory jsonFactory, HttpTransport transport) {
+    this.jsonFactory = checkNotNull(jsonFactory, "jsonFactory must not be null");
+    this.requestFactory = transport.createRequestFactory();
+  }
+
+  User getUserById(String uid, String token) throws FirebaseAuthException {
+    final Map<String, Object> payload = ImmutableMap.<String, Object>of(
+        "localId", ImmutableList.of(uid));
+    GetAccountInfoResponse response;
+    try {
+      response = post("getAccountInfo", token, payload,
+          GetAccountInfoResponse.class);
+    } catch (IOException e) {
+      throw new FirebaseAuthException(INTERNAL_ERROR,
+          "IO error while retrieving user with ID: " + uid, e);
+    }
+
+    if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) {
+      throw new FirebaseAuthException(USER_NOT_FOUND_ERROR,
+          "No user record found for the provided user ID: " + uid);
+    }
+    return new User(response.getUsers().get(0));
+  }
+
+  User getUserByEmail(String email, String token) throws FirebaseAuthException {
+    final Map<String, Object> payload = ImmutableMap.<String, Object>of(
+        "email", ImmutableList.of(email));
+    GetAccountInfoResponse response;
+    try {
+      response = post("getAccountInfo", token, payload, GetAccountInfoResponse.class);
+    } catch (IOException e) {
+      throw new FirebaseAuthException(INTERNAL_ERROR,
+          "IO error while retrieving user with email: " + email, e);
+    }
+
+    if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) {
+      throw new FirebaseAuthException(USER_NOT_FOUND_ERROR,
+          "No user record found for the provided email: " + email);
+    }
+    return new User(response.getUsers().get(0));
+  }
+
+  String createUser(User.Builder builder, String token) throws FirebaseAuthException {
+    GenericJson response;
+    try {
+      response = post("signupNewUser", token, builder.build(), GenericJson.class);
+    } catch (IOException e) {
+      throw new FirebaseAuthException(USER_CREATE_ERROR,
+          "IO error while creating user account", e);
+    }
+
+    if (response != null) {
+      String uid = (String) response.get("localId");
+      if (!Strings.isNullOrEmpty(uid)) {
+        return uid;
+      }
+    }
+    throw new FirebaseAuthException(USER_CREATE_ERROR, "Failed to create new user");
+  }
+
+  void updateUser(User.Updater updater, String token) throws FirebaseAuthException {
+    GenericJson response;
+    try {
+      response = post("setAccountInfo", token, updater.update(), GenericJson.class);
+    } catch (IOException e) {
+      throw new FirebaseAuthException(USER_UPDATE_ERROR,
+          "IO error while updating user: " + updater.getUid(), e);
+    }
+
+    if (response == null || !updater.getUid().equals(response.get("localId"))) {
+      throw new FirebaseAuthException(USER_UPDATE_ERROR,
+          "Failed to update user: " + updater.getUid());
+    }
+  }
+
+  void deleteUser(String uid, String token) throws FirebaseAuthException {
+    final Map<String, Object> payload = ImmutableMap.<String, Object>of("localId", uid);
+    GenericJson response;
+    try {
+      response = post("deleteAccount", token, payload, GenericJson.class);
+    } catch (IOException e) {
+      throw new FirebaseAuthException(USER_DELETE_ERROR,
+          "IO error while deleting user: " + uid, e);
+    }
+    if (response == null || !response.containsKey("kind")) {
+      throw new FirebaseAuthException(USER_DELETE_ERROR,
+          "Failed to delete user: " + uid);
+    }
+  }
+
+  private <T> T post(String path, String token, Object content, Class<T> clazz) throws IOException {
+    checkArgument(!Strings.isNullOrEmpty(path), "path must not be null or empty");
+    checkArgument(!Strings.isNullOrEmpty(token), "OAuth token must not be null or empty");
+    checkNotNull(content, "content must not be null");
+    checkNotNull(clazz, "response class must not be null");
+
+    GenericUrl url = new GenericUrl(ID_TOOLKIT_URL + path);
+    HttpRequest request = requestFactory.buildPostRequest(url,
+        new JsonHttpContent(jsonFactory, content));
+    request.setParser(new JsonObjectParser(jsonFactory));
+    request.getHeaders().setAuthorization("Bearer " + token);
+    HttpResponse response = request.execute();
+    try {
+      return response.parseAs(clazz);
+    } finally {
+      response.disconnect();
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/ProviderUserInfo.java b/src/main/java/com/google/firebase/auth/ProviderUserInfo.java
new file mode 100644
index 0000000..25b1f65
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/ProviderUserInfo.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import com.google.firebase.auth.internal.GetAccountInfoResponse;
+import com.google.firebase.internal.Nullable;
+
+/**
+ * Contains metadata regarding how a user is known by a particular identity provider (IdP).
+ * Instances of this class are immutable and thread safe.
+ */
+public class ProviderUserInfo {
+
+  private final String uid;
+  private final String displayName;
+  private final String email;
+  private final String photoUrl;
+  private final String providerId;
+
+  ProviderUserInfo(GetAccountInfoResponse.Provider response) {
+    this.uid = response.getUid();
+    this.displayName = response.getDisplayName();
+    this.email = response.getEmail();
+    this.photoUrl = response.getPhotoUrl();
+    this.providerId = response.getProviderId();
+  }
+
+  /**
+   * Returns the user's unique ID assigned by the identity provider.
+   *
+   * @return a user ID string.
+   */
+  public String getUid() {
+    return uid;
+  }
+
+  /**
+   * Returns the user's display name.
+   *
+   * @return a display name string or null.
+   */
+  @Nullable
+  public String getDisplayName() {
+    return displayName;
+  }
+
+  /**
+   * Returns the user's email address.
+   *
+   * @return an email address string or null.
+   */
+  @Nullable
+  public String getEmail() {
+    return email;
+  }
+
+  /**
+   * Returns the user's photo URL.
+   *
+   * @return a URL string or null.
+   */
+  @Nullable
+  public String getPhotoUrl() {
+    return photoUrl;
+  }
+
+  /**
+   * Returns the ID of the identity provider. This can be a short domain name (e.g. google.com) or
+   * the identifier of an OpenID identity provider.
+   *
+   * @return an ID string that uniquely identifies the identity provider.
+   */
+  public String getProviderId() {
+    return providerId;
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/User.java b/src/main/java/com/google/firebase/auth/User.java
new file mode 100644
index 0000000..0fda9fc
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/User.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.firebase.auth.internal.GetAccountInfoResponse;
+import com.google.firebase.internal.Nullable;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Contains metadata associated with a Firebase user account. Instances of this class are immutable
+ * and thread safe.
+ */
+public class User {
+
+  private static final Map<String, String> REMOVABLE_FIELDS = ImmutableMap.of(
+      "displayName", "DISPLAY_NAME",
+      "photoUrl", "PHOTO_URL");
+
+  private final String uid;
+  private final String email;
+  private final boolean emailVerified;
+  private final String displayName;
+  private final String photoUrl;
+  private final boolean disabled;
+  private final ProviderUserInfo[] providers;
+  private final UserMetadata userMetadata;
+
+  User(GetAccountInfoResponse.User response) {
+    checkNotNull(response, "Response must not be null");
+    checkArgument(!Strings.isNullOrEmpty(response.getUid()), "uid must not be null or empty");
+    this.uid = response.getUid();
+    this.email = response.getEmail();
+    this.emailVerified = response.isEmailVerified();
+    this.displayName = response.getDisplayName();
+    this.photoUrl = response.getPhotoUrl();
+    this.disabled = response.isDisabled();
+    if (response.getProviders() == null || response.getProviders().length == 0) {
+      this.providers = new ProviderUserInfo[0];
+    } else {
+      this.providers = new ProviderUserInfo[response.getProviders().length];
+      for (int i = 0; i < this.providers.length; i++) {
+        this.providers[i] = new ProviderUserInfo(response.getProviders()[i]);
+      }
+    }
+    this.userMetadata = new UserMetadata(response.getCreatedAt(), response.getLastLoginAt());
+  }
+
+  /**
+   * Returns the user ID of this user.
+   *
+   * @return a non-null, non-empty user ID string.
+   */
+  public String getUid() {
+    return uid;
+  }
+
+  /**
+   * Returns the email address associated with this user.
+   *
+   * @return an email address string or null.
+   */
+  @Nullable
+  public String getEmail() {
+    return email;
+  }
+
+  /**
+   * Returns whether the email address of this user has been verified.
+   *
+   * @return true if the email has been verified, and false otherwise.
+   */
+  public boolean isEmailVerified() {
+    return emailVerified;
+  }
+
+  /**
+   * Returns the display name of this user.
+   *
+   * @return a display name string or null.
+   */
+  @Nullable
+  public String getDisplayName() {
+    return displayName;
+  }
+
+  /**
+   * Returns the photo URL of this user.
+   *
+   * @return a URL string or null.
+   */
+  @Nullable
+  public String getPhotoUrl() {
+    return photoUrl;
+  }
+
+  /**
+   * Returns whether this user account is disabled.
+   *
+   * @return true if the user account is disabled, and false otherwise.
+   */
+  public boolean isDisabled() {
+    return disabled;
+  }
+
+  /**
+   * Returns the identity providers associated with this user.
+   *
+   * @return an array of {@link ProviderUserInfo} instances, which may be empty.
+   */
+  public ProviderUserInfo[] getProviderData() {
+    return providers;
+  }
+
+  /**
+   * Returns additional metadata associated with this user.
+   *
+   * @return a non-null UserMetadata instance.
+   */
+  public UserMetadata getUserMetadata() {
+    return this.userMetadata;
+  }
+
+  /**
+   * Returns a new {@link User.Updater} instance, which can be used to update the attributes
+   * of this user.
+   *
+   * @return a non-null User.Updater instance.
+   */
+  public Updater updater() {
+    return new Updater(uid);
+  }
+
+  /**
+   * Returns a new {@link User.Updater} instance, which can be used to update the attributes
+   * of the user identified by the specified user ID. This method allows updating attributes of
+   * a user account, without first having to call {@link FirebaseAuth#getUser(String)}.
+   *
+   * @param uid a non-null, non-empty user ID string.
+   * @return a non-null User.Updater instance.
+   * @throws IllegalArgumentException If the user ID is null or empty.
+   */
+  public static Updater updater(String uid) {
+    return new Updater(uid);
+  }
+
+  /**
+   * Returns a new {@link User.Builder} instance, which can be used to create a new user. The
+   * returned builder should be passed to {@link FirebaseAuth#createUser(Builder)} to register
+   * the user information persistently.
+   *
+   * @return a non-null User.Builder instance.
+   */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  private static void checkEmail(String email) {
+    checkArgument(!Strings.isNullOrEmpty(email), "email cannot be null or empty");
+    checkArgument(email.matches("^[^@]+@[^@]+$"));
+  }
+
+  private static void checkPassword(String password) {
+    checkArgument(!Strings.isNullOrEmpty(password), "password cannot be null or empty");
+    checkArgument(password.length() >= 6, "password must be at least 6 characters long");
+  }
+
+  /**
+   * A builder class for creating new user accounts. Set the initial attributes of the new user
+   * account by calling various setter methods available in this class. None of the attributes
+   * are required.
+   */
+  public static class Builder {
+
+    private final Map<String,Object> properties = new HashMap<>();
+
+    private Builder() {
+    }
+
+    /**
+     * Sets a user ID for the new user.
+     *
+     * @param uid a non-null, non-empty user ID that uniquely identifies the new user. The user ID
+     *     must not be longer than 128 characters.
+     */
+    public Builder setUid(String uid) {
+      checkArgument(!Strings.isNullOrEmpty(uid), "uid cannot be null or empty");
+      checkArgument(uid.length() <= 128, "UID cannot be longer than 128 characters");
+      properties.put("localId", uid);
+      return this;
+    }
+
+    /**
+     * Sets an email address for the new user.
+     *
+     * @param email a non-null, non-empty email address string.
+     */
+    public Builder setEmail(String email) {
+      checkEmail(email);
+      properties.put("email", email);
+      return this;
+    }
+
+    /**
+     * Sets whether the user email address has been verified or not.
+     *
+     * @param emailVerified a boolean indicating the email verification status.
+     */
+    public Builder setEmailVerified(boolean emailVerified) {
+      properties.put("emailVerified", emailVerified);
+      return this;
+    }
+
+    /**
+     * Sets the display name for the new user.
+     *
+     * @param displayName a non-null, non-empty display name string.
+     */
+    public Builder setDisplayName(String displayName) {
+      checkNotNull(displayName, "displayName cannot be null or empty");
+      properties.put("displayName", displayName);
+      return this;
+    }
+
+    /**
+     * Sets the photo URL for the new user.
+     *
+     * @param photoUrl a non-null, non-empty URL string.
+     */
+    public Builder setPhotoUrl(String photoUrl) {
+      checkArgument(!Strings.isNullOrEmpty(photoUrl), "photoUrl cannot be null or empty");
+      try {
+        new URL(photoUrl);
+      } catch (MalformedURLException e) {
+        throw new IllegalArgumentException("malformed photoUrl string", e);
+      }
+      properties.put("photoUrl", photoUrl);
+      return this;
+    }
+
+    /**
+     * Sets whether the new user account should be disabled by default or not.
+     *
+     * @param disabled a boolean indicating whether the new account should be disabled.
+     */
+    public Builder setDisabled(boolean disabled) {
+      properties.put("disabled", disabled);
+      return this;
+    }
+
+    /**
+     * Sets the password for the new user.
+     *
+     * @param password a password string that is at least 6 characters long.
+     */
+    public Builder setPassword(String password) {
+      checkPassword(password);
+      properties.put("password", password);
+      return this;
+    }
+
+    Map<String, Object> build() {
+      return ImmutableMap.copyOf(properties);
+    }
+  }
+
+  /**
+   * A class for updating the attributes of an existing user. An instance of this class can be
+   * obtained via a {@link User} object, or from a user ID string. Specify the changes to be
+   * made in the user account by calling the various setter methods available in this class.
+   */
+  public static class Updater {
+
+    private final Map<String,Object> properties = new HashMap<>();
+
+    private Updater(String uid) {
+      checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
+      properties.put("localId", uid);
+    }
+
+    String getUid() {
+      return (String) properties.get("localId");
+    }
+
+    /**
+     * Updates the email address associated with this user.
+     *
+     * @param email a non-null, non-empty email address to be associated with the user.
+     */
+    public Updater setEmail(String email) {
+      checkEmail(email);
+      properties.put("email", email);
+      return this;
+    }
+
+    /**
+     * Updates the email verification status of this account.
+     *
+     * @param emailVerified a boolean indicating whether the email address has been verified.
+     */
+    public Updater setEmailVerified(boolean emailVerified) {
+      properties.put("emailVerified", emailVerified);
+      return this;
+    }
+
+    /**
+     * Updates the display name of this user. Calling this method with a null argument removes the
+     * display name attribute from the user account.
+     *
+     * @param displayName a display name string or null
+     */
+    public Updater setDisplayName(String displayName) {
+      properties.put("displayName", displayName);
+      return this;
+    }
+
+    /**
+     * Updates the Photo URL of this user. Calling this method with a null argument removes
+     * the photo URL attribute from the user account.
+     *
+     * @param photoUrl a valid URL string or null
+     */
+    public Updater setPhotoUrl(String photoUrl) {
+      if (photoUrl != null) {
+        try {
+          new URL(photoUrl);
+        } catch (MalformedURLException e) {
+          throw new IllegalArgumentException("malformed photoUrl string", e);
+        }
+      }
+      properties.put("photoUrl", photoUrl);
+      return this;
+    }
+
+    /**
+     * Enables or disables this user account.
+     *
+     * @param disabled a boolean indicating whether this account should be disabled.
+     */
+    public Updater setDisabled(boolean disabled) {
+      properties.put("disableUser", disabled);
+      return this;
+    }
+
+    /**
+     * Updates the password of this user.
+     *
+     * @param password a new password string that is at least 6 characters long.
+     */
+    public Updater setPassword(String password) {
+      checkPassword(password);
+      properties.put("password", password);
+      return this;
+    }
+
+    Map<String, Object> update() {
+      Map<String, Object> copy = new HashMap<>(properties);
+      List<String> remove = new ArrayList<>();
+      for (Map.Entry<String, String> entry : REMOVABLE_FIELDS.entrySet()) {
+        if (copy.containsKey(entry.getKey()) && copy.get(entry.getKey()) == null) {
+          remove.add(entry.getValue());
+          copy.remove(entry.getKey());
+        }
+      }
+
+      if (!remove.isEmpty()) {
+        copy.put("deleteAttribute", ImmutableList.copyOf(remove));
+      }
+      return ImmutableMap.copyOf(copy);
+    }
+  }
+
+}
diff --git a/src/main/java/com/google/firebase/auth/UserMetadata.java b/src/main/java/com/google/firebase/auth/UserMetadata.java
new file mode 100644
index 0000000..eb4da93
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/UserMetadata.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+/**
+ * Contains additional metadata associated with a user account.
+ */
+public class UserMetadata {
+
+  private final long creationTimestamp;
+  private final long lastSignInTimestamp;
+
+  UserMetadata(long creationTimestamp, long lastSignInTimestamp) {
+    this.creationTimestamp = creationTimestamp;
+    this.lastSignInTimestamp = lastSignInTimestamp;
+  }
+
+  /**
+   * Returns the time at which the account was created.
+   *
+   * @return Milliseconds since epoch timestamp.
+   */
+  public long getCreationTimestamp() {
+    return creationTimestamp;
+  }
+
+  /**
+   * Returns the time at which the user last signed in.
+   *
+   * @return Milliseconds since epoch timestamp, or 0 if the user has never signed in.
+   */
+  public long getLastSignInTimestamp() {
+    return lastSignInTimestamp;
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java
new file mode 100644
index 0000000..f169897
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth.internal;
+
+import com.google.api.client.util.Key;
+import java.util.List;
+
+/**
+ * JSON data binding for getAccountInfoResponse messages sent by Google identity toolkit service.
+ */
+public final class GetAccountInfoResponse {
+
+  @Key("kind")
+  private String kind;
+
+  @Key("users")
+  private List<User> users;
+
+  public String getKind() {
+    return kind;
+  }
+
+  public List<User> getUsers() {
+    return users;
+  }
+
+  /**
+   * JSON data binding for user records.
+   */
+  public static final class User {
+
+    @Key("localId")
+    private String uid;
+
+    @Key("email")
+    private String email;
+
+    @Key("emailVerified")
+    private boolean emailVerified;
+
+    @Key("displayName")
+    private String displayName;
+
+    @Key("photoUrl")
+    private String photoUrl;
+
+    @Key("disabled")
+    private boolean disabled;
+
+    @Key("providerUserInfo")
+    private Provider[] providers;
+
+    @Key("createdAt")
+    private long createdAt;
+
+    @Key("lastLoginAt")
+    private long lastLoginAt;
+
+    public String getUid() {
+      return uid;
+    }
+
+    public String getEmail() {
+      return email;
+    }
+
+    public boolean isEmailVerified() {
+      return emailVerified;
+    }
+
+    public String getDisplayName() {
+      return displayName;
+    }
+
+    public String getPhotoUrl() {
+      return photoUrl;
+    }
+
+    public boolean isDisabled() {
+      return disabled;
+    }
+
+    public Provider[] getProviders() {
+      return providers;
+    }
+
+    public long getCreatedAt() {
+      return createdAt;
+    }
+
+    public long getLastLoginAt() {
+      return lastLoginAt;
+    }
+  }
+
+  /**
+   * JSON data binding for provider data.
+   */
+  public static final class Provider {
+
+    @Key("rawId")
+    private String uid;
+
+    @Key("displayName")
+    private String displayName;
+
+    @Key("email")
+    private String email;
+
+    @Key("photoUrl")
+    private String photoUrl;
+
+    @Key("providerId")
+    private String providerId;
+
+    public String getUid() {
+      return uid;
+    }
+
+    public String getDisplayName() {
+      return displayName;
+    }
+
+    public String getEmail() {
+      return email;
+    }
+
+    public String getPhotoUrl() {
+      return photoUrl;
+    }
+
+    public String getProviderId() {
+      return providerId;
+    }
+  }
+}


Test patch:
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
new file mode 100644
index 0000000..59f1051
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.tasks.Tasks;
+import com.google.firebase.testing.IntegrationTestUtils;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class FirebaseAuthIT {
+
+  private static FirebaseAuth auth;
+
+  @BeforeClass
+  public static void setUpClass() throws Exception {
+    FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp();
+    auth = FirebaseAuth.getInstance(masterApp);
+  }
+
+  @Test
+  public void testGetNonExistingUser() throws Exception {
+    try {
+      Tasks.await(auth.getUser("non.existing"));
+      fail("No error thrown for non existing uid");
+    } catch (ExecutionException e) {
+      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
+          ((FirebaseAuthException) e.getCause()).getErrorCode());
+    }
+  }
+
+  @Test
+  public void testGetNonExistingUserByEmail() throws Exception {
+    try {
+      Tasks.await(auth.getUserByEmail("non.existing@definitely.non.existing"));
+      fail("No error thrown for non existing email");
+    } catch (ExecutionException e) {
+      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
+          ((FirebaseAuthException) e.getCause()).getErrorCode());
+    }
+  }
+
+  @Test
+  public void testUpdateNonExistingUser() throws Exception {
+    try {
+      Tasks.await(auth.updateUser(User.updater("non.existing")));
+      fail("No error thrown for non existing uid");
+    } catch (ExecutionException e) {
+      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      assertEquals(FirebaseUserManager.USER_UPDATE_ERROR,
+          ((FirebaseAuthException) e.getCause()).getErrorCode());
+    }
+  }
+
+  @Test
+  public void testDeleteNonExistingUser() throws Exception {
+    try {
+      Tasks.await(auth.deleteUser("non.existing"));
+      fail("No error thrown for non existing uid");
+    } catch (ExecutionException e) {
+      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      assertEquals(FirebaseUserManager.USER_DELETE_ERROR,
+          ((FirebaseAuthException) e.getCause()).getErrorCode());
+    }
+  }
+
+  @Test
+  public void testCreateUserWithParams() throws Exception {
+    String randomId = UUID.randomUUID().toString().replaceAll("-", "");
+    String userEmail = ("test" + randomId.substring(0, 12) + "@example." + randomId.substring(12)
+        + ".com").toLowerCase();
+    User.Builder builder = User.builder()
+        .setUid(randomId)
+        .setEmail(userEmail)
+        .setDisplayName("Random User")
+        .setPhotoUrl("https://example.com/photo.png")
+        .setEmailVerified(true)
+        .setPassword("password");
+
+    User user = Tasks.await(auth.createUser(builder));
+    try {
+      assertEquals(randomId, user.getUid());
+      assertEquals("Random User", user.getDisplayName());
+      assertEquals(userEmail, user.getEmail());
+      assertEquals("https://example.com/photo.png", user.getPhotoUrl());
+      assertTrue(user.isEmailVerified());
+      assertFalse(user.isDisabled());
+
+      checkRecreate(randomId);
+    } finally {
+      Tasks.await(auth.deleteUser(user.getUid()));
+    }
+  }
+
+  private void checkRecreate(String uid) throws Exception {
+    try {
+      Tasks.await(auth.createUser(User.builder().setUid(uid)));
+      fail("No error thrown for creating user with existing ID");
+    } catch (ExecutionException e) {
+      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      assertEquals(FirebaseUserManager.USER_CREATE_ERROR,
+          ((FirebaseAuthException) e.getCause()).getErrorCode());
+    }
+  }
+
+  @Test
+  public void testUserLifecycle() throws Exception {
+    // Create user
+    User user = Tasks.await(auth.createUser(User.builder()));
+    String uid = user.getUid();
+
+    // Get user
+    user = Tasks.await(auth.getUser(user.getUid()));
+    assertEquals(uid, user.getUid());
+    assertNull(user.getDisplayName());
+    assertNull(user.getEmail());
+    assertNull(user.getPhotoUrl());
+    assertFalse(user.isEmailVerified());
+    assertFalse(user.isDisabled());
+    assertTrue(user.getUserMetadata().getCreationTimestamp() > 0);
+    assertEquals(0, user.getUserMetadata().getLastSignInTimestamp());
+
+    // Update user
+    String randomId = UUID.randomUUID().toString().replaceAll("-", "");
+    String userEmail = ("test" + randomId.substring(0, 12) + "@example." + randomId.substring(12)
+        + ".com").toLowerCase();
+    User.Updater updater = user.updater()
+        .setDisplayName("Updated Name")
+        .setEmail(userEmail)
+        .setPhotoUrl("https://example.com/photo.png")
+        .setEmailVerified(true)
+        .setPassword("secret");
+    user = Tasks.await(auth.updateUser(updater));
+    assertEquals(uid, user.getUid());
+    assertEquals("Updated Name", user.getDisplayName());
+    assertEquals(userEmail, user.getEmail());
+    assertEquals("https://example.com/photo.png", user.getPhotoUrl());
+    assertTrue(user.isEmailVerified());
+    assertFalse(user.isDisabled());
+
+    // Disable user and remove properties
+    updater = user.updater()
+        .setPhotoUrl(null)
+        .setDisplayName(null)
+        .setDisabled(true);
+    user = Tasks.await(auth.updateUser(updater));
+    assertEquals(uid, user.getUid());
+    assertNull(user.getDisplayName());
+    assertEquals(userEmail, user.getEmail());
+    assertNull(user.getPhotoUrl());
+    assertTrue(user.isEmailVerified());
+    assertTrue(user.isDisabled());
+
+    // Delete user
+    Tasks.await(auth.deleteUser(user.getUid()));
+    try {
+      Tasks.await(auth.getUser(user.getUid()));
+      fail("No error thrown for deleted user");
+    } catch (ExecutionException e) {
+      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
+          ((FirebaseAuthException) e.getCause()).getErrorCode());
+    }
+  }
+
+}
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
index f3e7174..7e5f7c5 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
@@ -44,7 +44,6 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintWriter;
-import java.io.UnsupportedEncodingException;
 import java.nio.file.Files;
 import java.util.Arrays;
 import java.util.Collection;
diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
new file mode 100644
index 0000000..8b0466a
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.common.collect.ImmutableList;
+import com.google.firebase.testing.TestUtils;
+
+import java.io.IOException;
+import java.util.Map;
+import org.junit.Test;
+
+public class FirebaseUserManagerTest {
+
+  private static final GsonFactory gson = new GsonFactory();
+
+  @Test
+  public void testGetUser() throws Exception {
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+    response.setContent(TestUtils.loadResource("getUser.json"));
+    MockHttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(response)
+        .build();
+    FirebaseUserManager userManager = new FirebaseUserManager(gson, transport);
+    User user = userManager.getUserById("testuser", "token");
+    checkUser(user);
+  }
+
+  @Test
+  public void testGetUserWithNotFoundError() throws Exception {
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+    response.setContent(TestUtils.loadResource("getUserError.json"));
+    MockHttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(response)
+        .build();
+    FirebaseUserManager userManager = new FirebaseUserManager(gson, transport);
+    try {
+      userManager.getUserById("testuser", "token");
+      fail("No error thrown for invalid response");
+    } catch (FirebaseAuthException e) {
+      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, e.getErrorCode());
+    }
+  }
+
+  @Test
+  public void testGetUserByEmail() throws Exception {
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+    response.setContent(TestUtils.loadResource("getUser.json"));
+    MockHttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(response)
+        .build();
+    FirebaseUserManager userManager = new FirebaseUserManager(gson, transport);
+    User user = userManager.getUserByEmail("testuser@example.com", "token");
+    checkUser(user);
+  }
+
+  @Test
+  public void testCreateUser() throws Exception {
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+    response.setContent(TestUtils.loadResource("createUser.json"));
+    MockHttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(response)
+        .build();
+    FirebaseUserManager userManager = new FirebaseUserManager(gson, transport);
+    String uid = userManager.createUser(User.builder(), "token");
+    assertEquals("testuser", uid);
+  }
+
+  @Test
+  public void testUpdateUser() throws Exception {
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+    response.setContent(TestUtils.loadResource("createUser.json"));
+    MockHttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(response)
+        .build();
+    FirebaseUserManager userManager = new FirebaseUserManager(gson, transport);
+    // should not throw
+    userManager.updateUser(User.updater("testuser"), "token");
+  }
+
+  @Test
+  public void testGetUserHttpError() throws Exception {
+    for (int code : ImmutableList.of(302, 400, 401, 404, 500)) {
+      MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+      response.setContent("{}");
+      response.setStatusCode(code);
+      MockHttpTransport transport = new MockHttpTransport.Builder()
+          .setLowLevelHttpResponse(response)
+          .build();
+      FirebaseUserManager userManager = new FirebaseUserManager(gson, transport);
+      try {
+        userManager.getUserById("testuser", "token");
+        fail("No error thrown for HTTP error");
+      }  catch (FirebaseAuthException e) {
+        assertTrue(e.getCause() instanceof IOException);
+        assertEquals(FirebaseUserManager.INTERNAL_ERROR, e.getErrorCode());
+      }
+    }
+  }
+
+  @Test
+  public void testGetUserJsonError() throws Exception {
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+    response.setContent("{\"not\" json}");
+    MockHttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(response)
+        .build();
+    FirebaseUserManager userManager = new FirebaseUserManager(gson, transport);
+    try {
+      userManager.getUserById("testuser", "token");
+      fail("No error thrown for JSON error");
+    }  catch (FirebaseAuthException e) {
+      assertTrue(e.getCause() instanceof IOException);
+    }
+  }
+
+  @Test
+  public void testUserBuilder() {
+    Map<String, Object> map = User.builder()
+        .build();
+    assertTrue(map.isEmpty());
+  }
+
+  @Test
+  public void testUserBuilderWithParams() {
+    Map<String, Object> map = User.builder()
+        .setUid("TestUid")
+        .setDisplayName("Display Name")
+        .setPhotoUrl("http://test.com/example.png")
+        .setEmail("test@example.com")
+        .setEmailVerified(true)
+        .setPassword("secret")
+        .build();
+    assertEquals(6, map.size());
+    assertEquals("TestUid", map.get("localId"));
+    assertEquals("Display Name", map.get("displayName"));
+    assertEquals("http://test.com/example.png", map.get("photoUrl"));
+    assertEquals("test@example.com", map.get("email"));
+    assertTrue((Boolean) map.get("emailVerified"));
+    assertEquals("secret", map.get("password"));
+  }
+
+  @Test
+  public void testInvalidUid() {
+    User.Builder builder = User.builder();
+    try {
+      builder.setUid(null);
+      fail("No error thrown for null uid");
+    } catch (Exception ignore) {
+      // expected
+    }
+
+    try {
+      builder.setUid("");
+      fail("No error thrown for empty uid");
+    } catch (Exception ignore) {
+      // expected
+    }
+
+    try {
+      builder.setUid(String.format("%0129d", 0));
+      fail("No error thrown for long uid");
+    } catch (Exception ignore) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testInvalidDisplayName() {
+    User.Builder builder = User.builder();
+    try {
+      builder.setDisplayName(null);
+      fail("No error thrown for null display name");
+    } catch (Exception ignore) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testInvalidPhotoUrl() {
+    User.Builder builder = User.builder();
+    try {
+      builder.setPhotoUrl(null);
+      fail("No error thrown for null photo url");
+    } catch (Exception ignore) {
+      // expected
+    }
+
+    try {
+      builder.setPhotoUrl("");
+      fail("No error thrown for invalid photo url");
+    } catch (Exception ignore) {
+      // expected
+    }
+
+    try {
+      builder.setPhotoUrl("not-a-url");
+      fail("No error thrown for invalid photo url");
+    } catch (Exception ignore) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testInvalidEmail() {
+    User.Builder builder = User.builder();
+    try {
+      builder.setEmail(null);
+      fail("No error thrown for null email");
+    } catch (Exception ignore) {
+      // expected
+    }
+
+    try {
+      builder.setEmail("");
+      fail("No error thrown for invalid email");
+    } catch (Exception ignore) {
+      // expected
+    }
+
+    try {
+      builder.setEmail("not-an-email");
+      fail("No error thrown for invalid email");
+    } catch (Exception ignore) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testInvalidPassword() {
+    User.Builder builder = User.builder();
+    try {
+      builder.setPassword(null);
+      fail("No error thrown for null password");
+    } catch (Exception ignore) {
+      // expected
+    }
+
+    try {
+      builder.setPassword("aaaaa");
+      fail("No error thrown for short password");
+    } catch (Exception ignore) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testUserUpdater() {
+    User.Updater updater = User.updater("test");
+    Map<String, Object> map = updater
+        .setDisplayName("Display Name")
+        .setPhotoUrl("http://test.com/example.png")
+        .setEmail("test@example.com")
+        .setEmailVerified(true)
+        .setPassword("secret")
+        .update();
+    assertEquals(6, map.size());
+    assertEquals(updater.getUid(), map.get("localId"));
+    assertEquals("Display Name", map.get("displayName"));
+    assertEquals("http://test.com/example.png", map.get("photoUrl"));
+    assertEquals("test@example.com", map.get("email"));
+    assertTrue((Boolean) map.get("emailVerified"));
+    assertEquals("secret", map.get("password"));
+  }
+
+  @Test
+  public void testDeleteDisplayName() {
+    Map<String, Object> map = User.updater("test")
+        .setDisplayName(null)
+        .update();
+    assertEquals(ImmutableList.of("DISPLAY_NAME"), map.get("deleteAttribute"));
+  }
+
+  @Test
+  public void testDeletePhotoUrl() {
+    Map<String, Object> map = User.updater("test")
+        .setPhotoUrl(null)
+        .update();
+    assertEquals(ImmutableList.of("PHOTO_URL"), map.get("deleteAttribute"));
+  }
+
+  @Test
+  public void testInvalidUpdatePhotoUrl() {
+    User.Updater updater = User.updater("test");
+    try {
+      updater.setPhotoUrl("");
+      fail("No error thrown for invalid photo url");
+    } catch (Exception ignore) {
+      // expected
+    }
+
+    try {
+      updater.setPhotoUrl("not-a-url");
+      fail("No error thrown for invalid photo url");
+    } catch (Exception ignore) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testInvalidUpdateEmail() {
+    User.Updater updater = User.updater("test");
+    try {
+      updater.setEmail(null);
+      fail("No error thrown for null email");
+    } catch (Exception ignore) {
+      // expected
+    }
+
+    try {
+      updater.setEmail("");
+      fail("No error thrown for invalid email");
+    } catch (Exception ignore) {
+      // expected
+    }
+
+    try {
+      updater.setEmail("not-an-email");
+      fail("No error thrown for invalid email");
+    } catch (Exception ignore) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testInvalidUpdatePassword() {
+    User.Updater updater = User.updater("test");
+    try {
+      updater.setPassword(null);
+      fail("No error thrown for null password");
+    } catch (Exception ignore) {
+      // expected
+    }
+
+    try {
+      updater.setPassword("aaaaa");
+      fail("No error thrown for short password");
+    } catch (Exception ignore) {
+      // expected
+    }
+  }
+
+  private void checkUser(User user) {
+    assertEquals("testuser", user.getUid());
+    assertEquals("testuser@example.com", user.getEmail());
+    assertEquals("Test User", user.getDisplayName());
+    assertEquals("http://www.example.com/testuser/photo.png", user.getPhotoUrl());
+    assertEquals(1234567890, user.getUserMetadata().getCreationTimestamp());
+    assertEquals(0, user.getUserMetadata().getLastSignInTimestamp());
+    assertEquals(1, user.getProviderData().length);
+    assertFalse(user.isDisabled());
+    assertTrue(user.isEmailVerified());
+
+    ProviderUserInfo provider = user.getProviderData()[0];
+    assertEquals("testuser@example.com", provider.getUid());
+    assertEquals("testuser@example.com", provider.getEmail());
+    assertEquals("Test User", provider.getDisplayName());
+    assertEquals("http://www.example.com/testuser/photo.png", provider.getPhotoUrl());
+    assertEquals("password", provider.getProviderId());
+  }
+
+}
diff --git a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java
index 537ed4e..b710038 100644
--- a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java
+++ b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java
@@ -56,7 +56,7 @@ public class IntegrationTestUtils {
       String certificatePath = System.getProperty("firebase.it.certificate");
       checkArgument(!Strings.isNullOrEmpty(certificatePath),
           "Service account certificate path not set. Set the "
-              + "file.it.certificate system property and try again.");
+              + "firebase.it.certificate system property and try again.");
       try (InputStreamReader reader = new InputStreamReader(new FileInputStream(certificatePath))) {
         IT_SERVICE_ACCOUNT = new JSONObject(CharStreams.toString(reader));
       } catch (IOException e) {
diff --git a/src/test/java/com/google/firebase/testing/TestUtils.java b/src/test/java/com/google/firebase/testing/TestUtils.java
index 23b91dc..c5bccf1 100644
--- a/src/test/java/com/google/firebase/testing/TestUtils.java
+++ b/src/test/java/com/google/firebase/testing/TestUtils.java
@@ -70,7 +70,7 @@ public class TestUtils {
     }
   }
 
-  static String loadResource(String path) {
+  public static String loadResource(String path) {
     InputStream stream = TestUtils.class.getClassLoader().getResourceAsStream(path);
     checkNotNull(stream, "Failed to load resource: %s", path);
     try (InputStreamReader reader = new InputStreamReader(stream)) {
diff --git a/src/test/resources/createUser.json b/src/test/resources/createUser.json
new file mode 100644
index 0000000..7bf6f02
--- /dev/null
+++ b/src/test/resources/createUser.json
@@ -0,0 +1,4 @@
+{
+  "kind": "identitytoolkit#SignupNewUserResponse",
+  "localId": "testuser"
+}
diff --git a/src/test/resources/getUser.json b/src/test/resources/getUser.json
new file mode 100644
index 0000000..8287ae1
--- /dev/null
+++ b/src/test/resources/getUser.json
@@ -0,0 +1,23 @@
+{
+  "kind" : "identitytoolkit#GetAccountInfoResponse",
+  "users" : [ {
+    "localId" : "testuser",
+    "email" : "testuser@example.com",
+    "emailVerified" : true,
+    "displayName" : "Test User",
+    "providerUserInfo" : [ {
+      "providerId" : "password",
+      "displayName" : "Test User",
+      "photoUrl" : "http://www.example.com/testuser/photo.png",
+      "federatedId" : "testuser@example.com",
+      "email" : "testuser@example.com",
+      "rawId" : "testuser@example.com"
+    } ],
+    "photoUrl" : "http://www.example.com/testuser/photo.png",
+    "passwordHash" : "passwordhash",
+    "passwordUpdatedAt" : 1.494364393E+12,
+    "validSince" : "1494364393",
+    "disabled" : false,
+    "createdAt" : "1234567890"
+  } ]
+}
diff --git a/src/test/resources/getUserError.json b/src/test/resources/getUserError.json
new file mode 100644
index 0000000..079b839
--- /dev/null
+++ b/src/test/resources/getUserError.json
@@ -0,0 +1,3 @@
+{
+  "kind" : "identitytoolkit#GetAccountInfoResponse"
+}