Repository: firebase/firebase-admin-java
Pull request #174 (2018-05-08 20:26:21)

Bulk User Import API
Introduces the following new APIs for importing users to Firebase Auth in bulk (1000 users per API call):

```
FirebaseAuth.importUsers(List<UserImportRecord> users)
FirebaseAuth.importUsers(List<UserImportRecord> users, UserImportOptions options)
FirebaseAuth.importUsersAsync(List<UserImportRecord> users)
FirebaseAuth.importUsersAsync(List<UserImportRecord> users, UserImportOptions options)
```

Also add the `UserImportHash` and `UserImportResult` APIs.

go/firebase-admin-user-import


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14f28a8..26bb3ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,13 @@
 # Unreleased
 
--
+- [added] Added new `importUsersAsync()` API for bulk importing users
+  into Firebase Auth.
+
+# v6.1.0
+
+- [changed] Deprecated the `FirebaseAuth.setCustomClaims()` method.
+  Developers should use the `FirebaseAuth.setCustomUserClaims()` method
+  instead.
 
 # v6.0.0
 
diff --git a/pom.xml b/pom.xml
index 8e21907..32b90b0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
 
     <groupId>com.google.firebase</groupId>
     <artifactId>firebase-admin</artifactId>
-    <version>6.0.1-SNAPSHOT</version>
+    <version>6.1.1-SNAPSHOT</version>
     <packaging>jar</packaging>
 
     <name>firebase-admin</name>
@@ -36,8 +36,8 @@
     </organization>
     <licenses>
         <license>
-            <name>Commercial</name>
-            <url>https://firebase.google.com/terms/</url>
+            <name>Apache License, Version 2.0</name>
+            <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
         </license>
     </licenses>
     <developers>
diff --git a/prepare_release.sh b/prepare_release.sh
index 47a7f4e..cc5969e 100755
--- a/prepare_release.sh
+++ b/prepare_release.sh
@@ -80,7 +80,7 @@ echo "[INFO] Updating CHANGELOG.md"
 if [ $HOST == "Darwin" ]; then
     sed -i "" -e "1 s/# Unreleased//" "CHANGELOG.md"
 else
-    sed -i -e "1 s/# Unreleased//" "CHANGELOG.md"
+    sed -i -e "/# Unreleased/d" "CHANGELOG.md"
 fi
 
 echo -e "# Unreleased\n\n-\n\n# v${VERSION}" | cat - CHANGELOG.md > TEMP_CHANGELOG.md
diff --git a/src/main/java/com/google/firebase/auth/ErrorInfo.java b/src/main/java/com/google/firebase/auth/ErrorInfo.java
new file mode 100644
index 0000000..7f7a5e3
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/ErrorInfo.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018 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 java.util.List;
+
+/**
+ * Represents an error encountered while importing an {@link ImportUserRecord}.
+ */
+public final class ErrorInfo {
+
+  private final int index;
+  private final String reason;
+
+  ErrorInfo(int index, String reason) {
+    this.index = index;
+    this.reason = reason;
+  }
+
+  /**
+   * The index of the failed user in the list passed to the
+   * {@link FirebaseAuth#importUsersAsync(List, UserImportOptions)} method.
+   *
+   * @return an integer index.
+   */
+  public int getIndex() {
+    return index;
+  }
+
+  /**
+   * A string describing the error.
+   *
+   * @return A string error message.
+   */
+  public String getReason() {
+    return reason;
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
index 3181dc5..47dd3ae 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
@@ -29,6 +29,7 @@ import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.ImplFirebaseTrampolines;
+import com.google.firebase.auth.FirebaseUserManager.UserImportRequest;
 import com.google.firebase.auth.ListUsersPage.DefaultUserSource;
 import com.google.firebase.auth.ListUsersPage.PageFactory;
 import com.google.firebase.auth.UserRecord.CreateRequest;
@@ -42,6 +43,7 @@ import com.google.firebase.internal.NonNull;
 import com.google.firebase.internal.Nullable;
 import java.io.IOException;
 import java.security.GeneralSecurityException;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -782,13 +784,21 @@ public class FirebaseAuth {
    * @throws IllegalArgumentException If the user ID string is null or empty, or the claims
    *     payload is invalid or too large.
    */
-  public void setCustomClaims(@NonNull String uid,
+  public void setCustomUserClaims(@NonNull String uid,
       @Nullable Map<String, Object> claims) throws FirebaseAuthException {
     setCustomUserClaimsOp(uid, claims).call();
   }
 
   /**
-   * Similar to {@link #setCustomClaims(String, Map)} but performs the operation asynchronously.
+   * @deprecated Use {@link #setCustomUserClaims(String, Map)} instead.
+   */
+  public void setCustomClaims(@NonNull String uid,
+      @Nullable Map<String, Object> claims) throws FirebaseAuthException {
+    setCustomUserClaims(uid, claims);
+  }
+
+  /**
+   * Similar to {@link #setCustomUserClaims(String, Map)} but performs the operation asynchronously.
    *
    * @param uid A user ID string.
    * @param claims A map of custom claims or null.
@@ -852,6 +862,86 @@ public class FirebaseAuth {
     };
   }
 
+  /**
+   * Imports the provided list of users into Firebase Auth. At most 1000 users can be imported at a
+   * time. This operation is optimized for bulk imports and will ignore checks on identifier
+   * uniqueness which could result in duplications.
+   *
+   * <p>{@link UserImportOptions} is required to import users with passwords. See
+   * {@link #importUsers(List, UserImportOptions)}.
+   *
+   * @param users A non-empty list of users to be imported. Length must not exceed 1000.
+   * @return A {@link UserImportResult} instance.
+   * @throws IllegalArgumentException If the users list is null, empty or has more than 1000
+   *     elements. Or if at least one user specifies a password.
+   * @throws FirebaseAuthException If an error occurs while importing users.
+   */
+  public UserImportResult importUsers(List<ImportUserRecord> users) throws FirebaseAuthException {
+    return importUsers(users, null);
+  }
+
+  /**
+   * Imports the provided list of users into Firebase Auth. At most 1000 users can be imported at a
+   * time. This operation is optimized for bulk imports and will ignore checks on identifier
+   * uniqueness which could result in duplications.
+   *
+   * @param users A non-empty list of users to be imported. Length must not exceed 1000.
+   * @param options a {@link UserImportOptions} instance or null. Required when importing users
+   *     with passwords.
+   * @return A {@link UserImportResult} instance.
+   * @throws IllegalArgumentException If the users list is null, empty or has more than 1000
+   *     elements. Or if at least one user specifies a password, and options is null.
+   * @throws FirebaseAuthException If an error occurs while importing users.
+   */
+  public UserImportResult importUsers(List<ImportUserRecord> users,
+      @Nullable UserImportOptions options) throws FirebaseAuthException {
+    return importUsersOp(users, options).call();
+  }
+
+  /**
+   * Similar to {@link #importUsers(List)} but performs the operation asynchronously.
+   *
+   * @param users A non-empty list of users to be imported. Length must not exceed 1000.
+   * @return An {@code ApiFuture} which will complete successfully when the user accounts are
+   *      imported. If an error occurs while importing the users, the future throws a
+   *     {@link FirebaseAuthException}.
+   * @throws IllegalArgumentException If the users list is null, empty or has more than 1000
+   *     elements. Or if at least one user specifies a password.
+   */
+  public ApiFuture<UserImportResult> importUsersAsync(List<ImportUserRecord> users) {
+    return importUsersAsync(users, null);
+  }
+
+  /**
+   * Similar to {@link #importUsers(List, UserImportOptions)} but performs the operation
+   * asynchronously.
+   *
+   * @param users A non-empty list of users to be imported. Length must not exceed 1000.
+   * @param options a {@link UserImportOptions} instance or null. Required when importing users
+   *     with passwords.
+   * @return An {@code ApiFuture} which will complete successfully when the user accounts are
+   *      imported. If an error occurs while importing the users, the future throws a
+   *     {@link FirebaseAuthException}.
+   * @throws IllegalArgumentException If the users list is null, empty or has more than 1000
+   *     elements. Or if at least one user specifies a password, and options is null.
+   */
+  public ApiFuture<UserImportResult> importUsersAsync(List<ImportUserRecord> users,
+      @Nullable UserImportOptions options) {
+    return importUsersOp(users, options).callAsync(firebaseApp);
+  }
+
+  private CallableOperation<UserImportResult, FirebaseAuthException> importUsersOp(
+      List<ImportUserRecord> users, UserImportOptions options) {
+    checkNotDestroyed();
+    final UserImportRequest request = new UserImportRequest(users, options, jsonFactory);
+    return new CallableOperation<UserImportResult, FirebaseAuthException>() {
+      @Override
+      protected UserImportResult execute() throws FirebaseAuthException {
+        return userManager.importUsers(request);
+      }
+    };
+  }
+
   @VisibleForTesting
   FirebaseUserManager getUserManager() {
     return this.userManager;
diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
index 67720b6..39b3e37 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -30,6 +30,7 @@ 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.api.client.util.Key;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
@@ -41,6 +42,7 @@ import com.google.firebase.auth.internal.DownloadAccountResponse;
 import com.google.firebase.auth.internal.GetAccountInfoResponse;
 
 import com.google.firebase.auth.internal.HttpErrorResponse;
+import com.google.firebase.auth.internal.UploadAccountResponse;
 import com.google.firebase.internal.FirebaseRequestInitializer;
 import com.google.firebase.internal.NonNull;
 import com.google.firebase.internal.SdkUtils;
@@ -82,6 +84,7 @@ class FirebaseUserManager {
       .build();
 
   static final int MAX_LIST_USERS_RESULTS = 1000;
+  static final int MAX_IMPORT_USERS = 1000;
 
   static final List<String> RESERVED_CLAIMS = ImmutableList.of(
       "amr", "at_hash", "aud", "auth_time", "azp", "cnf", "c_hash", "exp", "iat",
@@ -195,6 +198,15 @@ class FirebaseUserManager {
     return response;
   }
 
+  UserImportResult importUsers(UserImportRequest request) throws FirebaseAuthException {
+    checkNotNull(request);
+    UploadAccountResponse response = post("uploadAccount", request, UploadAccountResponse.class);
+    if (response == null) {
+      throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to import users.");
+    }
+    return new UserImportResult(request.getUsersCount(), response);
+  }
+
   String createSessionCookie(String idToken,
       SessionCookieOptions options) throws FirebaseAuthException {
     final Map<String, Object> payload = ImmutableMap.<String, Object>of(
@@ -257,4 +269,38 @@ class FirebaseUserManager {
         "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent());
     throw new FirebaseAuthException(INTERNAL_ERROR, msg, e);
   }
+
+  static class UserImportRequest extends GenericJson {
+
+    @Key("users")
+    private final List<Map<String, Object>> users;
+
+    UserImportRequest(List<ImportUserRecord> users, UserImportOptions options,
+        JsonFactory jsonFactory) {
+      checkArgument(users != null && !users.isEmpty(), "users must not be null or empty");
+      checkArgument(users.size() <= FirebaseUserManager.MAX_IMPORT_USERS,
+          "users list must not contain more than %s items", FirebaseUserManager.MAX_IMPORT_USERS);
+
+      boolean hasPassword = false;
+      ImmutableList.Builder<Map<String, Object>> usersBuilder = ImmutableList.builder();
+      for (ImportUserRecord user : users) {
+        if (user.hasPassword()) {
+          hasPassword = true;
+        }
+        usersBuilder.add(user.getProperties(jsonFactory));
+      }
+      this.users = usersBuilder.build();
+
+      if (hasPassword) {
+        checkArgument(options != null && options.getHash() != null,
+            "UserImportHash option is required when at least one user has a password. Provide "
+                + "a UserImportHash via UserImportOptions.withHash().");
+        this.putAll(options.getProperties());
+      }
+    }
+
+    int getUsersCount() {
+      return users.size();
+    }
+  }
 }
diff --git a/src/main/java/com/google/firebase/auth/ImportUserRecord.java b/src/main/java/com/google/firebase/auth/ImportUserRecord.java
new file mode 100644
index 0000000..5ce57fc
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/ImportUserRecord.java
@@ -0,0 +1,303 @@
+/*
+ * 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.api.client.json.JsonFactory;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.BaseEncoding;
+import com.google.firebase.internal.NonNull;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a user account to be imported to Firebase Auth via the
+ * {@link FirebaseAuth#importUsers(List, UserImportOptions)} API. Must contain at least a
+ * uid string.
+ */
+public final class ImportUserRecord {
+
+  private final Map<String, Object> properties;
+
+  private ImportUserRecord(Map<String, Object> properties) {
+    this.properties = ImmutableMap.copyOf(properties);
+  }
+
+  Map<String, Object> getProperties(JsonFactory jsonFactory) {
+    Map<String, Object> copy = new HashMap<>(properties);
+    // serialize custom claims
+    if (copy.containsKey(UserRecord.CUSTOM_ATTRIBUTES)) {
+      Map customClaims = (Map) copy.remove(UserRecord.CUSTOM_ATTRIBUTES);
+      copy.put(UserRecord.CUSTOM_ATTRIBUTES, UserRecord.serializeCustomClaims(
+          customClaims, jsonFactory));
+    }
+    return ImmutableMap.copyOf(copy);
+  }
+
+  boolean hasPassword() {
+    return this.properties.containsKey("passwordHash");
+  }
+
+  /**
+   * Creates a new {@link ImportUserRecord.Builder}.
+   *
+   * @return A {@link ImportUserRecord.Builder} instance.
+   */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+
+    private String uid;
+    private String email;
+    private Boolean emailVerified;
+    private String displayName;
+    private String phoneNumber;
+    private String photoUrl;
+    private Boolean disabled;
+    private UserMetadata userMetadata;
+    private byte[] passwordHash;
+    private byte[] passwordSalt;
+
+    private final List<UserProvider> userProviders = new ArrayList<>();
+    private final Map<String, Object> customClaims = new HashMap<>();
+
+    private Builder() {}
+
+    /**
+     * Sets a user ID for the user.
+     *
+     * @param uid a non-null, non-empty user ID that uniquely identifies the user. The user ID
+     *     must not be longer than 128 characters.
+     * @return This builder.
+     */
+    public Builder setUid(String uid) {
+      this.uid = uid;
+      return this;
+    }
+
+    /**
+     * Sets an email address for the user.
+     *
+     * @param email a non-null, non-empty email address string.
+     * @return This builder.
+     */
+    public Builder setEmail(String email) {
+      this.email = email;
+      return this;
+    }
+
+    /**
+     * Sets whether the user email address has been verified or not.
+     *
+     * @param emailVerified a boolean indicating the email verification status.
+     * @return This builder.
+     */
+    public Builder setEmailVerified(boolean emailVerified) {
+      this.emailVerified = emailVerified;
+      return this;
+    }
+
+    /**
+     * Sets the display name for the user.
+     *
+     * @param displayName a non-null, non-empty display name string.
+     * @return This builder.
+     */
+    public Builder setDisplayName(String displayName) {
+      this.displayName = displayName;
+      return this;
+    }
+
+    /**
+     * Sets the phone number associated with this user.
+     *
+     * @param phoneNumber a valid phone number string.
+     * @return This builder.
+     */
+    public Builder setPhoneNumber(String phoneNumber) {
+      this.phoneNumber = phoneNumber;
+      return this;
+    }
+
+    /**
+     * Sets the photo URL for the user.
+     *
+     * @param photoUrl a non-null, non-empty URL string.
+     * @return This builder.
+     */
+    public Builder setPhotoUrl(String photoUrl) {
+      this.photoUrl = photoUrl;
+      return this;
+    }
+
+    /**
+     * Sets whether the user account should be disabled by default or not.
+     *
+     * @param disabled a boolean indicating whether the account should be disabled.
+     * @return This builder.
+     */
+    public Builder setDisabled(boolean disabled) {
+      this.disabled = disabled;
+      return this;
+    }
+
+    /**
+     * Sets additional metadata about the user.
+     *
+     * @param userMetadata A {@link UserMetadata} instance.
+     * @return This builder.
+     */
+    public Builder setUserMetadata(UserMetadata userMetadata) {
+      this.userMetadata = userMetadata;
+      return this;
+    }
+
+    /**
+     * Sets a byte array representing the user's hashed password. If at least one user account
+     * carries a password hash, a {@link UserImportHash} must be specified when calling the
+     * {@link FirebaseAuth#importUsersAsync(List, UserImportOptions)} method. See
+     * {@link UserImportOptions.Builder#setHash(UserImportHash)}.
+     *
+     * @param passwordHash A byte array.
+     * @return This builder.
+     */
+    public Builder setPasswordHash(byte[] passwordHash) {
+      this.passwordHash = passwordHash;
+      return this;
+    }
+
+    /**
+     * Sets a byte array representing the user's password salt.
+     *
+     * @param passwordSalt A byte array.
+     * @return This builder.
+     */
+    public Builder setPasswordSalt(byte[] passwordSalt) {
+      this.passwordSalt = passwordSalt;
+      return this;
+    }
+
+    /**
+     * Adds a user provider to be associated with this user.
+     *
+     * <p>A {@link UserProvider} represents the identity of the user as specified by an
+     * identity provider that is linked to this user account. The identity provider can specify
+     * its own values for common user attributes like email, display name and photo URL.
+     *
+     * @param provider A non-null {@link UserProvider}.
+     * @return This builder.
+     */
+    public Builder addUserProvider(@NonNull UserProvider provider) {
+      this.userProviders.add(provider);
+      return this;
+    }
+
+    /**
+     * Associates all user provider's in the given list with this user.
+     *
+     * @param providers A list of {@link UserProvider} instances.
+     * @return This builder.
+     */
+    public Builder addAllUserProviders(List<UserProvider> providers) {
+      this.userProviders.addAll(providers);
+      return this;
+    }
+
+    /**
+     * Sets the specified custom claim on this user account.
+     *
+     * @param key Name of the claim.
+     * @param value Value of the claim.
+     * @return This builder.
+     */
+    public Builder putCustomClaim(String key, Object value) {
+      this.customClaims.put(key, value);
+      return this;
+    }
+
+    /**
+     * Sets the custom claims associated with this user.
+     *
+     * @param customClaims a Map of custom claims
+     */
+    public Builder putAllCustomClaims(Map<String, Object> customClaims) {
+      this.customClaims.putAll(customClaims);
+      return this;
+    }
+
+    /**
+     * Builds a new {@link ImportUserRecord}.
+     *
+     * @return A non-null {@link ImportUserRecord}.
+     */
+    public ImportUserRecord build() {
+      Map<String, Object> properties = new HashMap<>();
+      UserRecord.checkUid(uid);
+      properties.put("localId", uid);
+
+      if (!Strings.isNullOrEmpty(email)) {
+        UserRecord.checkEmail(email);
+        properties.put("email", email);
+      }
+      if (!Strings.isNullOrEmpty(photoUrl)) {
+        UserRecord.checkUrl(photoUrl);
+        properties.put("photoUrl", photoUrl);
+      }
+      if (!Strings.isNullOrEmpty(phoneNumber)) {
+        UserRecord.checkPhoneNumber(phoneNumber);
+        properties.put("phoneNumber", phoneNumber);
+      }
+      if (!Strings.isNullOrEmpty(displayName)) {
+        properties.put("displayName", displayName);
+      }
+      if (userMetadata != null) {
+        if (userMetadata.getCreationTimestamp() > 0) {
+          properties.put("createdAt", userMetadata.getCreationTimestamp());
+        }
+        if (userMetadata.getLastSignInTimestamp() > 0) {
+          properties.put("lastLoginAt", userMetadata.getLastSignInTimestamp());
+        }
+      }
+      if (passwordHash != null) {
+        properties.put("passwordHash", BaseEncoding.base64Url().encode(passwordHash));
+      }
+      if (passwordSalt != null) {
+        properties.put("salt", BaseEncoding.base64Url().encode(passwordSalt));
+      }
+      if (userProviders.size() > 0) {
+        properties.put("providerUserInfo", ImmutableList.copyOf(userProviders));
+      }
+      if (customClaims.size() > 0) {
+        ImmutableMap<String, Object> mergedClaims = ImmutableMap.copyOf(customClaims);
+        UserRecord.checkCustomClaims(mergedClaims);
+        properties.put(UserRecord.CUSTOM_ATTRIBUTES, mergedClaims);
+      }
+      if (emailVerified != null) {
+        properties.put("emailVerified", emailVerified);
+      }
+      if (disabled != null) {
+        properties.put("disabled", disabled);
+      }
+      return new ImportUserRecord(properties);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/UserImportHash.java b/src/main/java/com/google/firebase/auth/UserImportHash.java
new file mode 100644
index 0000000..443c6f8
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/UserImportHash.java
@@ -0,0 +1,52 @@
+/*
+ * 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 com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+
+/**
+ * Represents a hash algorithm and the related configuration parameters used to hash user
+ * passwords. An instance of this class must be specified if importing any users with password
+ * hashes (see {@link UserImportOptions.Builder#setHash(UserImportHash)}.
+ *
+ * <p>This is not expected to be extended in user code. Applications should use one of the provided
+ * concrete implementations in the {@link com.google.firebase.auth.hash} package. See
+ * <a href="https://firebase.google.com/docs/auth/admin/import-users">documentation</a> for more
+ * details on available options.
+ */
+public abstract class UserImportHash {
+
+  private final String name;
+
+  protected UserImportHash(String name) {
+    checkArgument(!Strings.isNullOrEmpty(name));
+    this.name = name;
+  }
+
+  final Map<String, Object> getProperties() {
+    return ImmutableMap.<String, Object>builder()
+        .put("hashAlgorithm", name)
+        .putAll(getOptions())
+        .build();
+  }
+
+  protected abstract Map<String, Object> getOptions();
+}
diff --git a/src/main/java/com/google/firebase/auth/UserImportOptions.java b/src/main/java/com/google/firebase/auth/UserImportOptions.java
new file mode 100644
index 0000000..86accf9
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/UserImportOptions.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2018 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.checkNotNull;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.firebase.internal.NonNull;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A collection of options that can be passed to the
+ * {@link FirebaseAuth#importUsersAsync(List, UserImportOptions)} API.
+ */
+public final class UserImportOptions {
+
+  private final UserImportHash hash;
+
+  private UserImportOptions(Builder builder) {
+    this.hash = checkNotNull(builder.hash);
+  }
+
+  /**
+   * Creates a new {@link UserImportOptions} containing the provided hash algorithm.
+   *
+   * @param hash A non-null {@link UserImportHash}.
+   * @return A new {@link UserImportOptions}.
+   */
+  public static UserImportOptions withHash(@NonNull UserImportHash hash) {
+    return builder().setHash(checkNotNull(hash)).build();
+  }
+
+  /**
+   * Creates a new {@link UserImportOptions.Builder}.
+   *
+   * @return A {@link UserImportOptions.Builder} instance.
+   */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  Map<String, Object> getProperties() {
+    return ImmutableMap.<String, Object>builder()
+        .putAll(hash.getProperties())
+        .build();
+  }
+
+  UserImportHash getHash() {
+    return hash;
+  }
+
+  public static class Builder {
+
+    private UserImportHash hash;
+
+    private Builder() {}
+
+    /**
+     * Sets the algorithm used to hash user passwords. This is required
+     * when at least one of the {@link ImportUserRecord} instances being imported has a password
+     * hash. See {@link ImportUserRecord.Builder#setPasswordHash(byte[])}.
+     *
+     * @param hash A {@link UserImportHash}.
+     * @return This builder.
+     */
+    public Builder setHash(@NonNull UserImportHash hash) {
+      this.hash = hash;
+      return this;
+    }
+
+    /**
+     * Builds a new {@link UserImportOptions}.
+     *
+     * @return A non-null {@link UserImportOptions}.
+     */
+    public UserImportOptions build() {
+      return new UserImportOptions(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/UserImportResult.java b/src/main/java/com/google/firebase/auth/UserImportResult.java
new file mode 100644
index 0000000..143b061
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/UserImportResult.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018 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 com.google.common.collect.ImmutableList;
+import com.google.firebase.auth.internal.UploadAccountResponse;
+import com.google.firebase.internal.NonNull;
+import java.util.List;
+
+/**
+ * Represents the result of the {@link FirebaseAuth#importUsersAsync(List, UserImportOptions)} API.
+ */
+public final class UserImportResult {
+
+  private final int users;
+  private final ImmutableList<ErrorInfo> errors;
+
+  UserImportResult(int users, UploadAccountResponse response) {
+    ImmutableList.Builder<ErrorInfo> errorsBuilder = ImmutableList.builder();
+    List<UploadAccountResponse.ErrorInfo> errors = response.getErrors();
+    if (errors != null) {
+      checkArgument(users >= errors.size());
+      for (UploadAccountResponse.ErrorInfo error : errors) {
+        errorsBuilder.add(new ErrorInfo(error.getIndex(), error.getMessage()));
+      }
+    }
+    this.users = users;
+    this.errors = errorsBuilder.build();
+  }
+
+  /**
+   * Returns the number of users that were imported successfully.
+   *
+   * @return number of users successfully imported (possibly zero).
+   */
+  public int getSuccessCount() {
+    return users - errors.size();
+  }
+
+  /**
+   * Returns the number of users that failed to be imported.
+   *
+   * @return number of users that resulted in import failures (possibly zero).
+   */
+  public int getFailureCount() {
+    return errors.size();
+  }
+
+  /**
+   * A list of {@link ErrorInfo} instances describing the errors that were encountered during
+   * the import. Length of this list is equal to the return value of {@link #getFailureCount()}.
+   *
+   * @return A non-null list (possibly empty).
+   */
+  @NonNull public List<ErrorInfo> getErrors() {
+    return errors;
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/UserMetadata.java b/src/main/java/com/google/firebase/auth/UserMetadata.java
index eb4da93..a287237 100644
--- a/src/main/java/com/google/firebase/auth/UserMetadata.java
+++ b/src/main/java/com/google/firebase/auth/UserMetadata.java
@@ -24,7 +24,11 @@ public class UserMetadata {
   private final long creationTimestamp;
   private final long lastSignInTimestamp;
 
-  UserMetadata(long creationTimestamp, long lastSignInTimestamp) {
+  public UserMetadata(long creationTimestamp) {
+    this(creationTimestamp, 0L);
+  }
+
+  public UserMetadata(long creationTimestamp, long lastSignInTimestamp) {
     this.creationTimestamp = creationTimestamp;
     this.lastSignInTimestamp = lastSignInTimestamp;
   }
diff --git a/src/main/java/com/google/firebase/auth/UserProvider.java b/src/main/java/com/google/firebase/auth/UserProvider.java
new file mode 100644
index 0000000..a13e0ec
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/UserProvider.java
@@ -0,0 +1,139 @@
+/*
+ * 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 com.google.api.client.util.Key;
+import com.google.common.base.Strings;
+
+/**
+ * Represents a user identity provider that can be associated with a Firebase user.
+ */
+public final class UserProvider {
+
+  @Key("rawId")
+  private final String uid;
+
+  @Key("displayName")
+  private final String displayName;
+
+  @Key("email")
+  private final String email;
+
+  @Key("photoUrl")
+  private final String photoUrl;
+
+  @Key("providerId")
+  private final String providerId;
+
+  private UserProvider(Builder builder) {
+    checkArgument(!Strings.isNullOrEmpty(builder.uid), "Uid must not be null or empty");
+    checkArgument(!Strings.isNullOrEmpty(builder.providerId),
+        "ProviderId must not be null or empty");
+    this.uid = builder.uid;
+    this.displayName = builder.displayName;
+    this.email = builder.email;
+    this.photoUrl = builder.photoUrl;
+    this.providerId = builder.providerId;
+  }
+
+  /**
+   * Creates a new {@link UserProvider.Builder}.
+   *
+   * @return A {@link UserProvider.Builder} instance.
+   */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+
+    private String uid;
+    private String displayName;
+    private String email;
+    private String photoUrl;
+    private String providerId;
+
+    private Builder() {}
+
+    /**
+     * Sets the user's unique ID assigned by the identity provider. This field is required.
+     *
+     * @param uid a user ID string.
+     * @return This builder.
+     */
+    public Builder setUid(String uid) {
+      this.uid = uid;
+      return this;
+    }
+
+    /**
+     * Sets the user's display name.
+     *
+     * @param displayName display name of the user.
+     * @return This builder.
+     */
+    public Builder setDisplayName(String displayName) {
+      this.displayName = displayName;
+      return this;
+    }
+
+    /**
+     * Sets the user's email address.
+     *
+     * @param email an email address string.
+     * @return This builder.
+     */
+    public Builder setEmail(String email) {
+      this.email = email;
+      return this;
+    }
+
+    /**
+     * Sets the photo URl of the user.
+     *
+     * @param photoUrl a photo URL string.
+     * @return This builder.
+     */
+    public Builder setPhotoUrl(String photoUrl) {
+      this.photoUrl = photoUrl;
+      return this;
+    }
+
+    /**
+     * Sets 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. This field is required.
+     *
+     * @param providerId an ID string that uniquely identifies the identity provider.
+     * @return This builder.
+     */
+    public Builder setProviderId(String providerId) {
+      this.providerId = providerId;
+      return this;
+    }
+
+    /**
+     * Builds a new {@link UserProvider}.
+     *
+     * @return A non-null {@link UserProvider}.
+     */
+    public UserProvider build() {
+      return new UserProvider(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java
index cc27bd1..e004500 100644
--- a/src/main/java/com/google/firebase/auth/UserRecord.java
+++ b/src/main/java/com/google/firebase/auth/UserRecord.java
@@ -18,10 +18,8 @@ package com.google.firebase.auth;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Preconditions.checkState;
 
 import com.google.api.client.json.JsonFactory;
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -33,7 +31,6 @@ import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.ArrayList;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -48,7 +45,7 @@ public class UserRecord implements UserInfo {
   private static final Map<String, String> REMOVABLE_FIELDS = ImmutableMap.of(
       "displayName", "DISPLAY_NAME",
       "photoUrl", "PHOTO_URL");
-  private static final String CUSTOM_ATTRIBUTES = "customAttributes";
+  static final String CUSTOM_ATTRIBUTES = "customAttributes";
   private static final int MAX_CLAIMS_PAYLOAD_SIZE = 1000;
 
   private final String uid;
@@ -232,29 +229,44 @@ public class UserRecord implements UserInfo {
     return new UpdateRequest(uid);
   }
 
-  private static void checkEmail(String email) {
+  static void checkUid(String uid) {
+    checkArgument(!Strings.isNullOrEmpty(uid), "uid cannot be null or empty");
+    checkArgument(uid.length() <= 128, "UID cannot be longer than 128 characters");
+  }
+
+  static void checkEmail(String email) {
     checkArgument(!Strings.isNullOrEmpty(email), "email cannot be null or empty");
     checkArgument(email.matches("^[^@]+@[^@]+$"));
   }
 
-  private static void checkPhoneNumber(String phoneNumber) {
+  static void checkPhoneNumber(String phoneNumber) {
     // Phone number verification is very lax here. Backend will enforce E.164 spec compliance, and
     // normalize accordingly.
     checkArgument(!Strings.isNullOrEmpty(phoneNumber), "phone number cannot be null or empty");
-    checkState(phoneNumber.startsWith("+"),
+    checkArgument(phoneNumber.startsWith("+"),
         "phone number must be a valid, E.164 compliant identifier starting with a '+' sign");
   }
 
+  static void checkUrl(String photoUrl) {
+    checkArgument(!Strings.isNullOrEmpty(photoUrl), "url cannot be null or empty");
+    try {
+      new URL(photoUrl);
+    } catch (MalformedURLException e) {
+      throw new IllegalArgumentException("malformed url string", e);
+    }
+  }
+
   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");
   }
 
-  private static void checkCustomClaims(Map<String,Object> customClaims) {
+  static void checkCustomClaims(Map<String,Object> customClaims) {
     if (customClaims == null) {
       return;
     }
     for (String key : customClaims.keySet()) {
+      checkArgument(!Strings.isNullOrEmpty(key), "Claim names must not be null or empty");
       checkArgument(!FirebaseUserManager.RESERVED_CLAIMS.contains(key),
           "Claim '" + key + "' is reserved and cannot be set");
     }
@@ -265,7 +277,7 @@ public class UserRecord implements UserInfo {
         + Long.toString(epochSeconds));
   }
 
-  private static String serializeCustomClaims(Map customClaims, JsonFactory jsonFactory) {
+  static String serializeCustomClaims(Map customClaims, JsonFactory jsonFactory) {
     checkNotNull(jsonFactory, "JsonFactory must not be null");
     if (customClaims == null || customClaims.isEmpty()) {
       return "{}";
@@ -305,8 +317,7 @@ public class UserRecord implements UserInfo {
      *     must not be longer than 128 characters.
      */
     public CreateRequest setUid(String uid) {
-      checkArgument(!Strings.isNullOrEmpty(uid), "uid cannot be null or empty");
-      checkArgument(uid.length() <= 128, "UID cannot be longer than 128 characters");
+      checkUid(uid);
       properties.put("localId", uid);
       return this;
     }
@@ -360,12 +371,7 @@ public class UserRecord implements UserInfo {
      * @param photoUrl a non-null, non-empty URL string.
      */
     public CreateRequest 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);
-      }
+      checkUrl(photoUrl);
       properties.put("photoUrl", photoUrl);
       return this;
     }
@@ -475,12 +481,9 @@ public class UserRecord implements UserInfo {
      * @param photoUrl a valid URL string or null
      */
     public UpdateRequest setPhotoUrl(@Nullable String photoUrl) {
+      // This is allowed to be null
       if (photoUrl != null) {
-        try {
-          new URL(photoUrl);
-        } catch (MalformedURLException e) {
-          throw new IllegalArgumentException("malformed photoUrl string", e);
-        }
+        checkUrl(photoUrl);
       }
       properties.put("photoUrl", photoUrl);
       return this;
diff --git a/src/main/java/com/google/firebase/auth/hash/Bcrypt.java b/src/main/java/com/google/firebase/auth/hash/Bcrypt.java
new file mode 100644
index 0000000..2b5f890
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/hash/Bcrypt.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018 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.hash;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.firebase.auth.UserImportHash;
+import java.util.Map;
+
+/**
+ * Represents the Bcrypt password hashing algorithm. Can be used as an instance of
+ * {@link com.google.firebase.auth.UserImportHash} when importing users.
+ */
+public class Bcrypt extends UserImportHash {
+
+  private Bcrypt() {
+    super("BCRYPT");
+  }
+
+  public static Bcrypt getInstance() {
+    return new Bcrypt();
+  }
+
+  @Override
+  protected Map<String, Object> getOptions() {
+    return ImmutableMap.of();
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/hash/Hmac.java b/src/main/java/com/google/firebase/auth/hash/Hmac.java
new file mode 100644
index 0000000..d558793
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/hash/Hmac.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 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.hash;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.BaseEncoding;
+import com.google.firebase.auth.UserImportHash;
+import java.util.Map;
+
+abstract class Hmac extends UserImportHash {
+
+  private final String key;
+
+  Hmac(String name, Builder builder) {
+    super(name);
+    checkArgument(builder.key != null && builder.key.length > 0,
+        "A non-empty key is required for HMAC algorithms");
+    this.key = BaseEncoding.base64().encode(builder.key);
+  }
+
+  @Override
+  protected final Map<String, Object> getOptions() {
+    return ImmutableMap.<String, Object>of("signerKey", key);
+  }
+
+  abstract static class Builder<T extends Builder, U extends UserImportHash> {
+    private byte[] key;
+
+    protected abstract T getInstance();
+
+    /**
+     * Sets the signer key for the HMAC hash algorithm. Required field.
+     *
+     * @param key Signer key as a byte array.
+     * @return This builder.
+     */
+    public T setKey(byte[] key) {
+      this.key = key;
+      return getInstance();
+    }
+
+    public abstract U build();
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/hash/HmacMd5.java b/src/main/java/com/google/firebase/auth/hash/HmacMd5.java
new file mode 100644
index 0000000..b675743
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/hash/HmacMd5.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018 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.hash;
+
+/**
+ * Represents the HMAC MD5 password hashing algorithm. Can be used as an instance of
+ * {@link com.google.firebase.auth.UserImportHash} when importing users.
+ */
+public class HmacMd5 extends Hmac {
+
+  private HmacMd5(Builder builder) {
+    super("HMAC_MD5", builder);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder extends Hmac.Builder<Builder, HmacMd5> {
+
+    private Builder() {}
+
+    @Override
+    protected Builder getInstance() {
+      return this;
+    }
+
+    public HmacMd5 build() {
+      return new HmacMd5(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/hash/HmacSha1.java b/src/main/java/com/google/firebase/auth/hash/HmacSha1.java
new file mode 100644
index 0000000..a9ecefd
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/hash/HmacSha1.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018 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.hash;
+
+/**
+ * Represents the HMAC SHA1 password hashing algorithm. Can be used as an instance of
+ * {@link com.google.firebase.auth.UserImportHash} when importing users.
+ */
+public class HmacSha1 extends Hmac {
+
+  private HmacSha1(Builder builder) {
+    super("HMAC_SHA1", builder);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder extends Hmac.Builder<Builder, HmacSha1> {
+
+    private Builder() {}
+
+    @Override
+    protected Builder getInstance() {
+      return this;
+    }
+
+    public HmacSha1 build() {
+      return new HmacSha1(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/hash/HmacSha256.java b/src/main/java/com/google/firebase/auth/hash/HmacSha256.java
new file mode 100644
index 0000000..78f131c
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/hash/HmacSha256.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018 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.hash;
+
+/**
+ * Represents the HMAC SHA256 password hashing algorithm. Can be used as an instance of
+ * {@link com.google.firebase.auth.UserImportHash} when importing users.
+ */
+public class HmacSha256 extends Hmac {
+
+  private HmacSha256(Builder builder) {
+    super("HMAC_SHA256", builder);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder extends Hmac.Builder<Builder, HmacSha256> {
+
+    private Builder() {}
+
+    @Override
+    protected Builder getInstance() {
+      return this;
+    }
+
+    public HmacSha256 build() {
+      return new HmacSha256(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/hash/HmacSha512.java b/src/main/java/com/google/firebase/auth/hash/HmacSha512.java
new file mode 100644
index 0000000..21e6a2b
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/hash/HmacSha512.java
@@ -0,0 +1,46 @@
+/*
+ * 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.hash;
+
+/**
+ * Represents the HMAC SHA512 password hashing algorithm. Can be used as an instance of
+ * {@link com.google.firebase.auth.UserImportHash} when importing users.
+ */
+public class HmacSha512 extends Hmac {
+
+  private HmacSha512(Builder builder) {
+    super("HMAC_SHA512", builder);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder extends Hmac.Builder<Builder, HmacSha512> {
+
+    private Builder() {}
+
+    @Override
+    protected Builder getInstance() {
+      return this;
+    }
+
+    public HmacSha512 build() {
+      return new HmacSha512(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/hash/Md5.java b/src/main/java/com/google/firebase/auth/hash/Md5.java
new file mode 100644
index 0000000..f45362a
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/hash/Md5.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018 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.hash;
+
+/**
+ * Represents the MD5 password hashing algorithm. Can be used as an instance of
+ * {@link com.google.firebase.auth.UserImportHash} when importing users.
+ */
+public class Md5 extends RepeatableHash {
+
+  private Md5(Builder builder) {
+    super("MD5", 0, 120000, builder);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder extends RepeatableHash.Builder<Builder, Md5> {
+
+    private Builder() {}
+
+    @Override
+    protected Builder getInstance() {
+      return this;
+    }
+
+    public Md5 build() {
+      return new Md5(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/hash/Pbkdf2Sha256.java b/src/main/java/com/google/firebase/auth/hash/Pbkdf2Sha256.java
new file mode 100644
index 0000000..4c5108e
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/hash/Pbkdf2Sha256.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018 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.hash;
+
+/**
+ * Represents the PBKDF2 SHA256 password hashing algorithm. Can be used as an instance of
+ * {@link com.google.firebase.auth.UserImportHash} when importing users.
+ */
+public class Pbkdf2Sha256 extends RepeatableHash {
+
+  private Pbkdf2Sha256(Builder builder) {
+    super("PBKDF2_SHA256", 0, 120000, builder);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder extends RepeatableHash.Builder<Builder, Pbkdf2Sha256> {
+
+    private Builder() {}
+
+    @Override
+    protected Builder getInstance() {
+      return this;
+    }
+
+    public Pbkdf2Sha256 build() {
+      return new Pbkdf2Sha256(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/hash/PbkdfSha1.java b/src/main/java/com/google/firebase/auth/hash/PbkdfSha1.java
new file mode 100644
index 0000000..8afe3f4
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/hash/PbkdfSha1.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018 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.hash;
+
+/**
+ * Represents the PBKDF SHA1 password hashing algorithm. Can be used as an instance of
+ * {@link com.google.firebase.auth.UserImportHash} when importing users.
+ */
+public class PbkdfSha1 extends RepeatableHash {
+
+  private PbkdfSha1(Builder builder) {
+    super("PBKDF_SHA1", 0, 120000, builder);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder extends RepeatableHash.Builder<Builder, PbkdfSha1> {
+
+    private Builder() {}
+
+    @Override
+    protected Builder getInstance() {
+      return this;
+    }
+
+    public PbkdfSha1 build() {
+      return new PbkdfSha1(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/hash/RepeatableHash.java b/src/main/java/com/google/firebase/auth/hash/RepeatableHash.java
new file mode 100644
index 0000000..bcf1a95
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/hash/RepeatableHash.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2018 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.hash;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.firebase.auth.UserImportHash;
+import java.util.Map;
+
+/**
+ * An abstract {@link UserImportHash} implementation that accepts a {@code rounds} parameter in
+ * a given range.
+ */
+abstract class RepeatableHash extends UserImportHash {
+
+  private final int rounds;
+
+  RepeatableHash(String name, int min, int max, Builder builder) {
+    super(name);
+    checkArgument(builder.rounds >= min && builder.rounds <= max,
+        "Rounds value must be between %s and %s (inclusive).", min, max);
+    this.rounds = builder.rounds;
+  }
+
+  @Override
+  protected Map<String, Object> getOptions() {
+    return ImmutableMap.<String, Object>of("rounds", rounds);
+  }
+
+  abstract static class Builder<T extends Builder, U extends UserImportHash> {
+    private int rounds;
+
+    protected abstract T getInstance();
+
+    /**
+     * Sets the number of rounds for the hash algorithm.
+     *
+     * @param rounds an integer between 0 and 120000 (inclusive).
+     * @return this builder.
+     */
+    public T setRounds(int rounds) {
+      this.rounds = rounds;
+      return getInstance();
+    }
+
+    public abstract U build();
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/hash/Scrypt.java b/src/main/java/com/google/firebase/auth/hash/Scrypt.java
new file mode 100644
index 0000000..1c96122
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/hash/Scrypt.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2018 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.hash;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.BaseEncoding;
+import java.util.Map;
+
+/**
+ * Represents the Scrypt password hashing algorithm. This is the
+ * <a href="https://github.com/firebase/scrypt">modified Scrypt algorithm</a> used by
+ * Firebase Auth. See {@link StandardScrypt} for the standard Scrypt algorithm. Can be used as an
+ * instance of {@link com.google.firebase.auth.UserImportHash} when importing users.
+ */
+public final class Scrypt extends RepeatableHash {
+
+  private final String key;
+  private final String saltSeparator;
+  private final int memoryCost;
+
+  private Scrypt(Builder builder) {
+    super("SCRYPT",0, 8, builder);
+    checkArgument(builder.key != null && builder.key.length > 0,
+        "A non-empty key is required for Scrypt");
+    checkArgument(builder.memoryCost > 0 && builder.memoryCost <= 14,
+        "memoryCost must be between 1 and 14");
+    this.key = BaseEncoding.base64Url().encode(builder.key);
+    if (builder.saltSeparator != null) {
+      this.saltSeparator = BaseEncoding.base64Url().encode(builder.saltSeparator);
+    } else {
+      this.saltSeparator = BaseEncoding.base64Url().encode(new byte[0]);
+    }
+    this.memoryCost = builder.memoryCost;
+  }
+
+  @Override
+  protected Map<String, Object> getOptions() {
+    return ImmutableMap.<String, Object>builder()
+        .putAll(super.getOptions())
+        .put("signerKey", key)
+        .put("memoryCost", memoryCost)
+        .put("saltSeparator", saltSeparator)
+        .build();
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder extends RepeatableHash.Builder<Builder, Scrypt> {
+
+    private byte[] key;
+    private byte[] saltSeparator;
+    private int memoryCost;
+
+    private Builder() {}
+
+    /**
+     * Sets the signer key. Required field.
+     *
+     * @param key Signer key as a byte array.
+     * @return This builder.
+     */
+    public Builder setKey(byte[] key) {
+      this.key = key;
+      return this;
+    }
+
+    /**
+     * Sets the salt separator.
+     *
+     * @param saltSeparator Salt separator as a byte array.
+     * @return This builder.
+     */
+    public Builder setSaltSeparator(byte[] saltSeparator) {
+      this.saltSeparator = saltSeparator;
+      return this;
+    }
+
+    /**
+     * Sets the memory cost. Required field.
+     *
+     * @param memoryCost an integer between 1 and 14 (inclusive).
+     * @return this builder.
+     */
+    public Builder setMemoryCost(int memoryCost) {
+      this.memoryCost = memoryCost;
+      return this;
+    }
+
+    @Override
+    protected Builder getInstance() {
+      return this;
+    }
+
+    public Scrypt build() {
+      return new Scrypt(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/hash/Sha1.java b/src/main/java/com/google/firebase/auth/hash/Sha1.java
new file mode 100644
index 0000000..d14975f
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/hash/Sha1.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018 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.hash;
+
+/**
+ * Represents the SHA1 password hashing algorithm. Can be used as an instance of
+ * {@link com.google.firebase.auth.UserImportHash} when importing users.
+ */
+public class Sha1 extends RepeatableHash {
+
+  private Sha1(Builder builder) {
+    super("SHA1", 0, 120000, builder);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder extends RepeatableHash.Builder<Builder, Sha1> {
+
+    private Builder() {}
+
+    @Override
+    protected Builder getInstance() {
+      return this;
+    }
+
+    public Sha1 build() {
+      return new Sha1(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/hash/Sha256.java b/src/main/java/com/google/firebase/auth/hash/Sha256.java
new file mode 100644
index 0000000..ecc0e72
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/hash/Sha256.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018 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.hash;
+
+/**
+ * Represents the SHA256 password hashing algorithm. Can be used as an instance of
+ * {@link com.google.firebase.auth.UserImportHash} when importing users.
+ */
+public class Sha256 extends RepeatableHash {
+
+  private Sha256(Builder builder) {
+    super("SHA256", 0, 120000, builder);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder extends RepeatableHash.Builder<Builder, Sha256> {
+
+    private Builder() {}
+
+    @Override
+    protected Builder getInstance() {
+      return this;
+    }
+
+    public Sha256 build() {
+      return new Sha256(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/hash/Sha512.java b/src/main/java/com/google/firebase/auth/hash/Sha512.java
new file mode 100644
index 0000000..858d16e
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/hash/Sha512.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018 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.hash;
+
+/**
+ * Represents the SHA512 password hashing algorithm. Can be used as an instance of
+ * {@link com.google.firebase.auth.UserImportHash} when importing users.
+ */
+public class Sha512 extends RepeatableHash {
+
+  private Sha512(Builder builder) {
+    super("SHA512", 0, 120000, builder);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder extends RepeatableHash.Builder<Builder, Sha512> {
+
+    private Builder() {}
+
+    @Override
+    protected Builder getInstance() {
+      return this;
+    }
+
+    public Sha512 build() {
+      return new Sha512(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/hash/StandardScrypt.java b/src/main/java/com/google/firebase/auth/hash/StandardScrypt.java
new file mode 100644
index 0000000..49f7d72
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/hash/StandardScrypt.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2018 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.hash;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.firebase.auth.UserImportHash;
+import java.util.Map;
+
+/**
+ * Represents the Standard Scrypt password hashing algorithm. Can be used as an instance of
+ * {@link com.google.firebase.auth.UserImportHash} when importing users.
+ */
+public class StandardScrypt extends UserImportHash {
+
+  private final int derivedKeyLength;
+  private final int blockSize;
+  private final int parallelization;
+  private final int memoryCost;
+
+  private StandardScrypt(Builder builder) {
+    super("STANDARD_SCRYPT");
+    this.derivedKeyLength = builder.derivedKeyLength;
+    this.blockSize = builder.blockSize;
+    this.parallelization = builder.parallelization;
+    this.memoryCost = builder.memoryCost;
+  }
+
+  @Override
+  protected Map<String, Object> getOptions() {
+    return ImmutableMap.<String, Object>of(
+        "dkLen", derivedKeyLength,
+        "blockSize", blockSize,
+        "parallelization", parallelization,
+        "memoryCost", memoryCost
+    );
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+
+    private int derivedKeyLength;
+    private int blockSize;
+    private int parallelization;
+    private int memoryCost;
+
+    private Builder() {}
+
+    public Builder setDerivedKeyLength(int derivedKeyLength) {
+      this.derivedKeyLength = derivedKeyLength;
+      return this;
+    }
+
+    public Builder setBlockSize(int blockSize) {
+      this.blockSize = blockSize;
+      return this;
+    }
+
+    public Builder setParallelization(int parallelization) {
+      this.parallelization = parallelization;
+      return this;
+    }
+
+    public Builder setMemoryCost(int memoryCost) {
+      this.memoryCost = memoryCost;
+      return this;
+    }
+
+    public StandardScrypt build() {
+      return new StandardScrypt(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/internal/UploadAccountResponse.java b/src/main/java/com/google/firebase/auth/internal/UploadAccountResponse.java
new file mode 100644
index 0000000..abf2853
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/internal/UploadAccountResponse.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018 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;
+
+/**
+ * Represents the response from identity toolkit for a user import request.
+ */
+public class UploadAccountResponse {
+
+  @Key("error")
+  private List<ErrorInfo> errors;
+
+  public List<ErrorInfo> getErrors() {
+    return errors;
+  }
+
+  public static class ErrorInfo {
+    @Key("index")
+    private int index;
+
+    @Key("message")
+    private String message;
+
+    public int getIndex() {
+      return index;
+    }
+
+    public String getMessage() {
+      return message;
+    }
+  }
+}


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 4e71d7f..99171d8 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
@@ -36,10 +36,13 @@ import com.google.api.core.ApiFuture;
 import com.google.api.core.ApiFutureCallback;
 import com.google.api.core.ApiFutures;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.io.BaseEncoding;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.auth.UserRecord.CreateRequest;
 import com.google.firebase.auth.UserRecord.UpdateRequest;
+import com.google.firebase.auth.hash.Scrypt;
 import com.google.firebase.testing.IntegrationTestUtils;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -60,6 +63,8 @@ public class FirebaseAuthIT {
 
   private static final String ID_TOOLKIT_URL =
       "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken";
+  private static final String ID_TOOLKIT_URL2 =
+      "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword";
   private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
   private static final HttpTransport transport = Utils.getDefaultTransport();
 
@@ -433,6 +438,66 @@ public class FirebaseAuthIT {
     assertEquals("silver", decoded.getClaims().get("subscription"));
   }
 
+  @Test
+  public void testImportUsers() throws Exception {
+    final String randomId = UUID.randomUUID().toString().replaceAll("-", "");
+    final String userEmail = ("test" + randomId.substring(0, 12) + "@example."
+        + randomId.substring(12) + ".com").toLowerCase();
+    ImportUserRecord user = ImportUserRecord.builder()
+        .setUid(randomId)
+        .setEmail(userEmail)
+        .build();
+
+    UserImportResult result = auth.importUsersAsync(ImmutableList.of(user)).get();
+    assertEquals(1, result.getSuccessCount());
+    assertEquals(0, result.getFailureCount());
+
+    try {
+      UserRecord savedUser = auth.getUserAsync(randomId).get();
+      assertEquals(userEmail, savedUser.getEmail());
+    } finally {
+      auth.deleteUserAsync(randomId).get();
+    }
+  }
+
+  @Test
+  public void testImportUsersWithPassword() throws Exception {
+    final String randomId = UUID.randomUUID().toString().replaceAll("-", "");
+    final String userEmail = ("test" + randomId.substring(0, 12) + "@example."
+        + randomId.substring(12) + ".com").toLowerCase();
+    final byte[] passwordHash = BaseEncoding.base64().decode(
+        "V358E8LdWJXAO7muq0CufVpEOXaj8aFiC7T/rcaGieN04q/ZPJ08WhJEHGjj9lz/2TT+/86N5VjVoc5DdBhBiw==");
+    ImportUserRecord user = ImportUserRecord.builder()
+        .setUid(randomId)
+        .setEmail(userEmail)
+        .setPasswordHash(passwordHash)
+        .setPasswordSalt("NaCl".getBytes())
+        .build();
+
+    final byte[] scryptKey = BaseEncoding.base64().decode(
+        "jxspr8Ki0RYycVU8zykbdLGjFQ3McFUH0uiiTvC8pVMXAn210wjLNmdZJzxUECKbm0QsEmYUSDzZvpjeJ9WmXA==");
+    final byte[] saltSeparator = BaseEncoding.base64().decode("Bw==");
+    UserImportResult result = auth.importUsersAsync(
+        ImmutableList.of(user),
+        UserImportOptions.withHash(Scrypt.builder()
+            .setKey(scryptKey)
+            .setSaltSeparator(saltSeparator)
+            .setRounds(8)
+            .setMemoryCost(14)
+            .build())).get();
+    assertEquals(1, result.getSuccessCount());
+    assertEquals(0, result.getFailureCount());
+
+    try {
+      UserRecord savedUser = auth.getUserAsync(randomId).get();
+      assertEquals(userEmail, savedUser.getEmail());
+      String idToken = signInWithPassword(userEmail, "password");
+      assertFalse(Strings.isNullOrEmpty(idToken));
+    } finally {
+      auth.deleteUserAsync(randomId).get();
+    }
+  }
+
   private String signInWithCustomToken(String customToken) throws IOException {
     GenericUrl url = new GenericUrl(ID_TOOLKIT_URL + "?key="
         + IntegrationTestUtils.getApiKey());
@@ -450,6 +515,23 @@ public class FirebaseAuthIT {
     }
   }
 
+  private String signInWithPassword(String email, String password) throws IOException {
+    GenericUrl url = new GenericUrl(ID_TOOLKIT_URL2 + "?key="
+        + IntegrationTestUtils.getApiKey());
+    Map<String, Object> content = ImmutableMap.<String, Object>of(
+        "email", email, "password", password);
+    HttpRequest request = transport.createRequestFactory().buildPostRequest(url,
+        new JsonHttpContent(jsonFactory, content));
+    request.setParser(new JsonObjectParser(jsonFactory));
+    HttpResponse response = request.execute();
+    try {
+      GenericJson json = response.parseAs(GenericJson.class);
+      return json.get("idToken").toString();
+    } finally {
+      response.disconnect();
+    }
+  }
+
   private void checkRecreate(String uid) throws Exception {
     try {
       auth.createUserAsync(new CreateRequest().setUid(uid)).get();
diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
index b100584..df5a34e 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
@@ -252,6 +252,159 @@ public class FirebaseUserManagerTest {
     checkRequestHeaders(interceptor);
   }
 
+  @Test
+  public void testImportUsers() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
+    ImportUserRecord user1 = ImportUserRecord.builder().setUid("user1").build();
+    ImportUserRecord user2 = ImportUserRecord.builder().setUid("user2").build();
+
+    List<ImportUserRecord> users = ImmutableList.of(user1, user2);
+    UserImportResult result = FirebaseAuth.getInstance().importUsersAsync(users, null).get();
+    checkRequestHeaders(interceptor);
+    assertEquals(2, result.getSuccessCount());
+    assertEquals(0, result.getFailureCount());
+    assertTrue(result.getErrors().isEmpty());
+
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    interceptor.getResponse().getRequest().getContent().writeTo(out);
+    JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
+    GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class);
+    assertEquals(1, parsed.size());
+    List<Map<String, Object>> expected = ImmutableList.of(
+        user1.getProperties(jsonFactory),
+        user2.getProperties(jsonFactory)
+    );
+    assertEquals(expected, parsed.get("users"));
+  }
+
+  @Test
+  public void testImportUsersError() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("importUsersError.json"));
+    ImportUserRecord user1 = ImportUserRecord.builder()
+        .setUid("user1")
+        .build();
+    ImportUserRecord user2 = ImportUserRecord.builder()
+        .setUid("user2")
+        .build();
+    ImportUserRecord user3 = ImportUserRecord.builder()
+        .setUid("user3")
+        .build();
+
+    List<ImportUserRecord> users = ImmutableList.of(user1, user2, user3);
+    UserImportResult result = FirebaseAuth.getInstance().importUsersAsync(users, null).get();
+    checkRequestHeaders(interceptor);
+    assertEquals(1, result.getSuccessCount());
+    assertEquals(2, result.getFailureCount());
+    assertEquals(2, result.getErrors().size());
+
+    ErrorInfo error = result.getErrors().get(0);
+    assertEquals(0, error.getIndex());
+    assertEquals("Some error occurred in user1", error.getReason());
+    error = result.getErrors().get(1);
+    assertEquals(2, error.getIndex());
+    assertEquals("Another error occurred in user3", error.getReason());
+
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    interceptor.getResponse().getRequest().getContent().writeTo(out);
+    JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
+    GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class);
+    assertEquals(1, parsed.size());
+    List<Map<String, Object>> expected = ImmutableList.of(
+        user1.getProperties(jsonFactory),
+        user2.getProperties(jsonFactory),
+        user3.getProperties(jsonFactory)
+    );
+    assertEquals(expected, parsed.get("users"));
+  }
+
+  @Test
+  public void testImportUsersWithHash() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
+    ImportUserRecord user1 = ImportUserRecord.builder()
+        .setUid("user1")
+        .build();
+    ImportUserRecord user2 = ImportUserRecord.builder()
+        .setUid("user2")
+        .setPasswordHash("password".getBytes())
+        .build();
+
+    List<ImportUserRecord> users = ImmutableList.of(user1, user2);
+    UserImportHash hash = new UserImportHash("MOCK_HASH") {
+      @Override
+      protected Map<String, Object> getOptions() {
+        return ImmutableMap.<String, Object>of("key1", "value1", "key2", true);
+      }
+    };
+    UserImportResult result = FirebaseAuth.getInstance().importUsersAsync(users,
+        UserImportOptions.withHash(hash)).get();
+    checkRequestHeaders(interceptor);
+    assertEquals(2, result.getSuccessCount());
+    assertEquals(0, result.getFailureCount());
+    assertTrue(result.getErrors().isEmpty());
+
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    interceptor.getResponse().getRequest().getContent().writeTo(out);
+    JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
+    GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class);
+    assertEquals(4, parsed.size());
+    List<Map<String, Object>> expected = ImmutableList.of(
+        user1.getProperties(jsonFactory),
+        user2.getProperties(jsonFactory)
+    );
+    assertEquals(expected, parsed.get("users"));
+    assertEquals("MOCK_HASH", parsed.get("hashAlgorithm"));
+    assertEquals("value1", parsed.get("key1"));
+    assertEquals(Boolean.TRUE, parsed.get("key2"));
+  }
+
+  @Test
+  public void testImportUsersMissingHash() {
+    initializeAppForUserManagement();
+    ImportUserRecord user1 = ImportUserRecord.builder()
+        .setUid("user1")
+        .build();
+    ImportUserRecord user2 = ImportUserRecord.builder()
+        .setUid("user2")
+        .setPasswordHash("password".getBytes())
+        .build();
+
+    List<ImportUserRecord> users = ImmutableList.of(user1, user2);
+    try {
+      FirebaseAuth.getInstance().importUsersAsync(users);
+      fail("No error thrown for missing hash option");
+    } catch (IllegalArgumentException expected) {
+      assertEquals("UserImportHash option is required when at least one user has a password. "
+          + "Provide a UserImportHash via UserImportOptions.withHash().", expected.getMessage());
+    }
+  }
+
+  @Test
+  public void testImportUsersEmptyList() {
+    initializeAppForUserManagement();
+    try {
+      FirebaseAuth.getInstance().importUsersAsync(ImmutableList.<ImportUserRecord>of());
+      fail("No error thrown for empty user list");
+    } catch (IllegalArgumentException expected) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testImportUsersLargeList() {
+    initializeAppForUserManagement();
+    ImmutableList.Builder<ImportUserRecord> users = ImmutableList.builder();
+    for (int i = 0; i < 1001; i++) {
+      users.add(ImportUserRecord.builder().setUid("test" + i).build());
+    }
+    try {
+      FirebaseAuth.getInstance().importUsersAsync(users.build());
+      fail("No error thrown for large list");
+    } catch (IllegalArgumentException expected) {
+      // expected
+    }
+  }
+
   @Test
   public void testCreateSessionCookie() throws Exception {
     TestResponseInterceptor interceptor = initializeAppForUserManagement(
@@ -267,12 +420,13 @@ public class FirebaseUserManagerTest {
     interceptor.getResponse().getRequest().getContent().writeTo(out);
     JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
     GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class);
+    assertEquals(2, parsed.size());
     assertEquals("testToken", parsed.get("idToken"));
     assertEquals(new BigDecimal(3600), parsed.get("validDuration"));
   }
 
   @Test
-  public void testCreateSessionInvalidArguments() {
+  public void testCreateSessionCookieInvalidArguments() {
     FirebaseApp.initializeApp(new FirebaseOptions.Builder()
         .setCredentials(credentials)
         .build());
diff --git a/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java b/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java
new file mode 100644
index 0000000..011a5cc
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2018 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.fail;
+
+import com.google.api.client.googleapis.util.Utils;
+import com.google.api.client.json.JsonFactory;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.BaseEncoding;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+
+public class ImportUserRecordTest {
+
+  private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory();
+
+  @Test
+  public void testUidOnlyRecord() {
+    ImportUserRecord record = ImportUserRecord.builder()
+        .setUid("testuid")
+        .build();
+    assertEquals(ImmutableMap.of("localId", "testuid"), record.getProperties(JSON_FACTORY));
+  }
+
+  @Test
+  public void testAllProperties() throws IOException {
+    Date date = new Date();
+    UserProvider provider1 = UserProvider.builder()
+        .setUid("testuid")
+        .setProviderId("google.com")
+        .build();
+    UserProvider provider2 = UserProvider.builder()
+        .setUid("testuid")
+        .setProviderId("test.com")
+        .build();
+    ImportUserRecord record = ImportUserRecord.builder()
+        .setUid("testuid")
+        .setEmail("test@example.com")
+        .setDisplayName("Test User")
+        .setPhotoUrl("https://test.com/user.png")
+        .setPhoneNumber("+1234567890")
+        .setUserMetadata(new UserMetadata(date.getTime(), date.getTime()))
+        .setDisabled(false)
+        .setEmailVerified(true)
+        .setPasswordHash("password".getBytes())
+        .setPasswordSalt("salt".getBytes())
+        .addUserProvider(provider1)
+        .addAllUserProviders(ImmutableList.of(provider2))
+        .putCustomClaim("admin", true)
+        .putAllCustomClaims(ImmutableMap.<String, Object>of("package", "gold"))
+        .build();
+
+    Map<String, Object> properties = record.getProperties(JSON_FACTORY);
+
+    Map<String, Object> customClaims = new HashMap<>();
+    String customAttributes = (String) properties.get("customAttributes");
+    JSON_FACTORY.createJsonParser(customAttributes).parse(customClaims);
+    assertEquals(ImmutableMap.of("admin", true, "package", "gold"), customClaims);
+
+    Map<String, Object> expected = ImmutableMap.<String, Object>builder()
+        .put("localId", "testuid")
+        .put("email", "test@example.com")
+        .put("displayName", "Test User")
+        .put("photoUrl", "https://test.com/user.png")
+        .put("phoneNumber", "+1234567890")
+        .put("createdAt", date.getTime())
+        .put("lastLoginAt", date.getTime())
+        .put("disabled", false)
+        .put("emailVerified", true)
+        .put("passwordHash", BaseEncoding.base64Url().encode("password".getBytes()))
+        .put("salt", BaseEncoding.base64Url().encode("salt".getBytes()))
+        .put("providerUserInfo", ImmutableList.of(provider1, provider2))
+        .put("customAttributes", customAttributes)
+        .build();
+    assertEquals(expected, properties);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testInvalidUid() {
+    ImportUserRecord.builder()
+        .setUid(Strings.repeat("a", 129))
+        .build();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testInvalidEmail() {
+    ImportUserRecord.builder()
+        .setUid("test")
+        .setEmail("not-an-email")
+        .build();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testInvalidPhotoUrl() {
+    ImportUserRecord.builder()
+        .setUid("test")
+        .setPhotoUrl("not a url")
+        .build();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testInvalidPhoneNumber() {
+    ImportUserRecord.builder()
+        .setUid("test")
+        .setPhoneNumber("not a phone number")
+        .build();
+  }
+
+  @Test
+  public void testNullUserProvider() {
+    try {
+      ImportUserRecord.builder()
+          .setUid("test")
+          .addUserProvider(null).build();
+      fail("No error thrown for null provider");
+    } catch (NullPointerException expected) {
+      // expected
+    }
+
+    try {
+      List<UserProvider> providers = new ArrayList<>();
+      providers.add(null);
+      ImportUserRecord.builder()
+          .setUid("test")
+          .addAllUserProviders(providers).build();
+      fail("No error thrown for null provider");
+    } catch (NullPointerException expected) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testNullOrEmptyCustomClaims() {
+    try {
+      ImportUserRecord.builder()
+          .setUid("test")
+          .putCustomClaim("foo", null).build();
+      fail("No error thrown for null claim value");
+    } catch (NullPointerException expected) {
+      // expected
+    }
+
+    try {
+      ImportUserRecord.builder()
+          .setUid("test")
+          .putCustomClaim(null, "foo").build();
+      fail("No error thrown for null claim name");
+    } catch (NullPointerException expected) {
+      // expected
+    }
+
+    try {
+      ImportUserRecord.builder()
+          .setUid("test")
+          .putCustomClaim("", "foo").build();
+      fail("No error thrown for empty claim name");
+    } catch (IllegalArgumentException expected) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testReservedClaims() {
+    for (String key : FirebaseUserManager.RESERVED_CLAIMS) {
+      try {
+        ImportUserRecord.builder()
+            .setUid("test")
+            .putCustomClaim(key, "foo").build();
+        fail("No error thrown for reserved claim");
+      } catch (IllegalArgumentException expected) {
+        // expected
+      }
+    }
+  }
+
+  @Test
+  public void testLargeCustomClaims() {
+    ImportUserRecord user = ImportUserRecord.builder()
+        .setUid("test")
+        .putCustomClaim("foo", Strings.repeat("a", 1000))
+        .build();
+    try {
+      user.getProperties(JSON_FACTORY);
+      fail("No error thrown for large claim value");
+    } catch (IllegalArgumentException expected) {
+      // expected
+    }
+  }
+}
diff --git a/src/test/java/com/google/firebase/auth/UserImportHashTest.java b/src/test/java/com/google/firebase/auth/UserImportHashTest.java
new file mode 100644
index 0000000..e4a3af1
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/UserImportHashTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2018 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 com.google.common.collect.ImmutableMap;
+import com.google.common.io.BaseEncoding;
+import com.google.firebase.auth.hash.Bcrypt;
+import com.google.firebase.auth.hash.HmacMd5;
+import com.google.firebase.auth.hash.HmacSha1;
+import com.google.firebase.auth.hash.HmacSha256;
+import com.google.firebase.auth.hash.HmacSha512;
+import com.google.firebase.auth.hash.Md5;
+import com.google.firebase.auth.hash.Pbkdf2Sha256;
+import com.google.firebase.auth.hash.PbkdfSha1;
+import com.google.firebase.auth.hash.Scrypt;
+import com.google.firebase.auth.hash.Sha1;
+import com.google.firebase.auth.hash.Sha256;
+import com.google.firebase.auth.hash.Sha512;
+import com.google.firebase.auth.hash.StandardScrypt;
+import java.util.Map;
+import org.junit.Test;
+
+public class UserImportHashTest {
+
+  private static final byte[] SIGNER_KEY = "key".getBytes();
+  private static final byte[] SALT_SEPARATOR = "separator".getBytes();
+
+  private static class MockHash extends UserImportHash {
+    MockHash() {
+      super("MOCK_HASH");
+    }
+
+    @Override
+    protected Map<String, Object> getOptions() {
+      return ImmutableMap.<String, Object>of("key", "value");
+    }
+  }
+
+  @Test
+  public void testBase() {
+    UserImportHash hash = new MockHash();
+    assertEquals(ImmutableMap.of("hashAlgorithm", "MOCK_HASH", "key", "value"),
+        hash.getProperties());
+  }
+
+  @Test
+  public void testScryptHash() {
+    UserImportHash scrypt = Scrypt.builder()
+        .setKey(SIGNER_KEY)
+        .setSaltSeparator(SALT_SEPARATOR)
+        .setRounds(8)
+        .setMemoryCost(14)
+        .build();
+    Map<String, Object> properties = ImmutableMap.<String, Object>of(
+        "hashAlgorithm", "SCRYPT",
+        "signerKey", BaseEncoding.base64Url().encode(SIGNER_KEY),
+        "saltSeparator", BaseEncoding.base64Url().encode(SALT_SEPARATOR),
+        "rounds", 8,
+        "memoryCost", 14
+    );
+    assertEquals(properties, scrypt.getProperties());
+
+    scrypt = Scrypt.builder()
+        .setKey(SIGNER_KEY)
+        .setRounds(8)
+        .setMemoryCost(14)
+        .build();
+    properties = ImmutableMap.<String, Object>of(
+        "hashAlgorithm", "SCRYPT",
+        "signerKey", BaseEncoding.base64Url().encode(SIGNER_KEY),
+        "saltSeparator", "",
+        "rounds", 8,
+        "memoryCost", 14
+    );
+    assertEquals(properties, scrypt.getProperties());
+  }
+
+  @Test
+  public void testStandardScryptHash() {
+    UserImportHash scrypt = StandardScrypt.builder()
+        .setBlockSize(1)
+        .setParallelization(2)
+        .setDerivedKeyLength(3)
+        .setMemoryCost(4)
+        .build();
+    Map<String, Object> properties = ImmutableMap.<String, Object>of(
+        "hashAlgorithm", "STANDARD_SCRYPT",
+        "blockSize", 1,
+        "parallelization", 2,
+        "dkLen", 3,
+        "memoryCost", 4
+    );
+    assertEquals(properties, scrypt.getProperties());
+  }
+
+  @Test
+  public void testHmacHash() {
+    Map<String, UserImportHash> hashes = ImmutableMap.<String, UserImportHash>of(
+        "HMAC_SHA512", HmacSha512.builder().setKey(SIGNER_KEY).build(),
+        "HMAC_SHA256", HmacSha256.builder().setKey(SIGNER_KEY).build(),
+        "HMAC_SHA1", HmacSha1.builder().setKey(SIGNER_KEY).build(),
+        "HMAC_MD5", HmacMd5.builder().setKey(SIGNER_KEY).build()
+    );
+    for (Map.Entry<String, UserImportHash> entry : hashes.entrySet()) {
+      Map<String, Object> properties = ImmutableMap.<String, Object>of(
+          "hashAlgorithm", entry.getKey(),
+          "signerKey", BaseEncoding.base64Url().encode(SIGNER_KEY)
+      );
+      assertEquals(properties, entry.getValue().getProperties());
+    }
+  }
+
+  @Test
+  public void testBasicHash() {
+    Map<String, UserImportHash> hashes = ImmutableMap.<String, UserImportHash>builder()
+        .put("SHA512", Sha512.builder().setRounds(42).build())
+        .put("SHA256", Sha256.builder().setRounds(42).build())
+        .put("SHA1", Sha1.builder().setRounds(42).build())
+        .put("MD5", Md5.builder().setRounds(42).build())
+        .put("PBKDF2_SHA256", Pbkdf2Sha256.builder().setRounds(42).build())
+        .put("PBKDF_SHA1", PbkdfSha1.builder().setRounds(42).build())
+        .build();
+    for (Map.Entry<String, UserImportHash> entry : hashes.entrySet()) {
+      Map<String, Object> properties = ImmutableMap.<String, Object>of(
+          "hashAlgorithm", entry.getKey(),
+          "rounds", 42
+      );
+      assertEquals(properties, entry.getValue().getProperties());
+    }
+  }
+
+  @Test
+  public void testBcryptHash() {
+    UserImportHash bcrypt = Bcrypt.getInstance();
+    Map<String, Object> properties = ImmutableMap.<String, Object>of("hashAlgorithm", "BCRYPT");
+    assertEquals(properties, bcrypt.getProperties());
+  }
+}
diff --git a/src/test/java/com/google/firebase/auth/UserImportOptionsTest.java b/src/test/java/com/google/firebase/auth/UserImportOptionsTest.java
new file mode 100644
index 0000000..7913997
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/UserImportOptionsTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018 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.assertSame;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.BaseEncoding;
+import com.google.firebase.auth.hash.HmacSha512;
+import java.util.Map;
+import org.junit.Test;
+
+public class UserImportOptionsTest {
+
+  @Test(expected = NullPointerException.class)
+  public void testEmptyOptions() {
+    UserImportOptions.builder().build();
+  }
+
+  @Test
+  public void testHash() {
+    HmacSha512 hash = HmacSha512.builder()
+        .setKey("key".getBytes())
+        .build();
+    UserImportOptions options = UserImportOptions.builder()
+        .setHash(hash)
+        .build();
+    Map<String, Object> expected = ImmutableMap.<String, Object>of(
+        "hashAlgorithm", "HMAC_SHA512",
+        "signerKey", BaseEncoding.base64Url().encode("key".getBytes())
+    );
+    assertEquals(expected, options.getProperties());
+    assertSame(hash, options.getHash());
+  }
+}
diff --git a/src/test/java/com/google/firebase/auth/UserProviderTest.java b/src/test/java/com/google/firebase/auth/UserProviderTest.java
new file mode 100644
index 0000000..69806dc
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/UserProviderTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018 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 com.google.api.client.googleapis.util.Utils;
+import com.google.api.client.json.JsonFactory;
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+
+public class UserProviderTest {
+
+  private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory();
+
+  @Test
+  public void testAllProperties() throws IOException {
+    UserProvider provider = UserProvider.builder()
+        .setUid("testuid")
+        .setProviderId("google.com")
+        .setEmail("test@example.com")
+        .setDisplayName("Test User")
+        .setPhotoUrl("https://test.com/user.png")
+        .build();
+    String json = JSON_FACTORY.toString(provider);
+    Map<String, Object> parsed = new HashMap<>();
+    JSON_FACTORY.createJsonParser(json).parse(parsed);
+    Map<String, Object> expected = ImmutableMap.<String, Object>of(
+        "rawId", "testuid",
+        "providerId", "google.com",
+        "email", "test@example.com",
+        "displayName", "Test User",
+        "photoUrl", "https://test.com/user.png"
+    );
+    assertEquals(expected, parsed);
+  }
+
+  @Test
+  public void testRequiredProperties() throws IOException {
+    UserProvider provider = UserProvider.builder()
+        .setUid("testuid")
+        .setProviderId("google.com")
+        .build();
+    String json = JSON_FACTORY.toString(provider);
+    Map<String, Object> parsed = new HashMap<>();
+    JSON_FACTORY.createJsonParser(json).parse(parsed);
+    Map<String, Object> expected = ImmutableMap.<String, Object>of(
+        "rawId", "testuid",
+        "providerId", "google.com"
+    );
+    assertEquals(expected, parsed);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testNoUid() {
+    UserProvider.builder()
+        .setProviderId("google.com")
+        .build();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testNoProviderId() {
+    UserProvider.builder()
+        .setUid("testuid")
+        .build();
+  }
+}
diff --git a/src/test/java/com/google/firebase/auth/hash/InvalidHashTest.java b/src/test/java/com/google/firebase/auth/hash/InvalidHashTest.java
new file mode 100644
index 0000000..aabe444
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/hash/InvalidHashTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2018 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.hash;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+
+public class InvalidHashTest {
+
+  private static final byte[] SIGNER_KEY = "key".getBytes();
+  private static final byte[] SALT_SEPARATOR = "separator".getBytes();
+
+  @Test
+  public void testInvalidHmac() {
+    List<Hmac.Builder> builders = ImmutableList.<Hmac.Builder>of(
+        HmacSha512.builder(),
+        HmacSha256.builder(),
+        HmacSha1.builder(),
+        HmacMd5.builder()
+    );
+    for (Hmac.Builder builder : builders) {
+      try {
+        builder.build();
+        fail("No error thrown for missing key");
+      } catch (Exception expected) {
+        assertTrue(expected instanceof IllegalArgumentException);
+      }
+    }
+  }
+
+  @Test
+  public void testInvalidRepeatableHash() {
+    List<RepeatableHash.Builder> builders = ImmutableList.<RepeatableHash.Builder>builder()
+        .add(Sha512.builder().setRounds(-1))
+        .add(Sha256.builder().setRounds(-1))
+        .add(Sha1.builder().setRounds(-1))
+        .add(Md5.builder().setRounds(-1))
+        .add(Pbkdf2Sha256.builder().setRounds(-1))
+        .add(PbkdfSha1.builder().setRounds(-1))
+        .add(Sha512.builder().setRounds(120001))
+        .add(Sha256.builder().setRounds(120001))
+        .add(Sha1.builder().setRounds(120001))
+        .add(Md5.builder().setRounds(120001))
+        .add(Pbkdf2Sha256.builder().setRounds(120001))
+        .add(PbkdfSha1.builder().setRounds(120001))
+        .build();
+    for (RepeatableHash.Builder builder : builders) {
+      try {
+        builder.build();
+        fail("No error thrown for invalid rounds");
+      } catch (Exception expected) {
+        assertTrue(expected instanceof IllegalArgumentException);
+      }
+    }
+  }
+
+  @Test
+  public void testInvalidScrypt() {
+    List<Scrypt.Builder> builders = ImmutableList.of(
+        Scrypt.builder() // missing signer key
+            .setSaltSeparator(SALT_SEPARATOR)
+            .setRounds(5)
+            .setMemoryCost(12),
+        Scrypt.builder() // invalid rounds (> 8)
+            .setKey(SIGNER_KEY)
+            .setSaltSeparator(SALT_SEPARATOR)
+            .setRounds(9)
+            .setMemoryCost(14),
+        Scrypt.builder() // invalid rounds (< 0)
+            .setKey(SIGNER_KEY)
+            .setSaltSeparator(SALT_SEPARATOR)
+            .setRounds(-1)
+            .setMemoryCost(14),
+        Scrypt.builder() // invalid memory cost (> 15)
+            .setKey(SIGNER_KEY)
+            .setSaltSeparator(SALT_SEPARATOR)
+            .setRounds(8)
+            .setMemoryCost(15)
+    );
+    for (Scrypt.Builder builder : builders) {
+      try {
+        builder.build();
+        fail("No error thrown for invalid configuration");
+      } catch (IllegalArgumentException expected) {
+        // expected
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java
index ed219bd..52c78de 100644
--- a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java
+++ b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java
@@ -30,7 +30,6 @@ import com.google.firebase.database.FirebaseDatabase;
 import java.net.URI;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.CookieParam;
@@ -46,32 +45,32 @@ import javax.ws.rs.core.Response.Status;
  */
 public class FirebaseAuthSnippets {
 
-  public static void getUserById(String uid) throws InterruptedException, ExecutionException {
+  public static void getUserById(String uid) throws FirebaseAuthException {
     // [START get_user_by_id]
-    UserRecord userRecord = FirebaseAuth.getInstance().getUserAsync(uid).get();
+    UserRecord userRecord = FirebaseAuth.getInstance().getUser(uid);
     // See the UserRecord reference doc for the contents of userRecord.
     System.out.println("Successfully fetched user data: " + userRecord.getUid());
     // [END get_user_by_id]
   }
 
-  public static void getUserByEmail(String email) throws InterruptedException, ExecutionException {
+  public static void getUserByEmail(String email) throws FirebaseAuthException {
     // [START get_user_by_email]
-    UserRecord userRecord = FirebaseAuth.getInstance().getUserByEmailAsync(email).get();
+    UserRecord userRecord = FirebaseAuth.getInstance().getUserByEmail(email);
     // See the UserRecord reference doc for the contents of userRecord.
     System.out.println("Successfully fetched user data: " + userRecord.getEmail());
     // [END get_user_by_email]
   }
 
   public static void getUserByPhoneNumber(
-      String phoneNumber) throws InterruptedException, ExecutionException {
+      String phoneNumber) throws FirebaseAuthException {
     // [START get_user_by_phone]
-    UserRecord userRecord = FirebaseAuth.getInstance().getUserByPhoneNumberAsync(phoneNumber).get();
+    UserRecord userRecord = FirebaseAuth.getInstance().getUserByPhoneNumber(phoneNumber);
     // See the UserRecord reference doc for the contents of userRecord.
     System.out.println("Successfully fetched user data: " + userRecord.getPhoneNumber());
     // [END get_user_by_phone]
   }
 
-  public static void createUser() throws InterruptedException, ExecutionException {
+  public static void createUser() throws FirebaseAuthException {
     // [START create_user]
     CreateRequest request = new CreateRequest()
         .setEmail("user@example.com")
@@ -82,24 +81,24 @@ public class FirebaseAuthSnippets {
         .setPhotoUrl("http://www.example.com/12345678/photo.png")
         .setDisabled(false);
 
-    UserRecord userRecord = FirebaseAuth.getInstance().createUserAsync(request).get();
+    UserRecord userRecord = FirebaseAuth.getInstance().createUser(request);
     System.out.println("Successfully created new user: " + userRecord.getUid());
     // [END create_user]
   }
 
-  public static void createUserWithUid() throws InterruptedException, ExecutionException {
+  public static void createUserWithUid() throws FirebaseAuthException {
     // [START create_user_with_uid]
     CreateRequest request = new CreateRequest()
         .setUid("some-uid")
         .setEmail("user@example.com")
         .setPhoneNumber("+11234567890");
 
-    UserRecord userRecord = FirebaseAuth.getInstance().createUserAsync(request).get();
+    UserRecord userRecord = FirebaseAuth.getInstance().createUser(request);
     System.out.println("Successfully created new user: " + userRecord.getUid());
     // [END create_user_with_uid]
   }
 
-  public static void updateUser(String uid) throws InterruptedException, ExecutionException {
+  public static void updateUser(String uid) throws FirebaseAuthException {
     // [START update_user]
     UpdateRequest request = new UpdateRequest(uid)
         .setEmail("user@example.com")
@@ -110,18 +109,18 @@ public class FirebaseAuthSnippets {
         .setPhotoUrl("http://www.example.com/12345678/photo.png")
         .setDisabled(true);
 
-    UserRecord userRecord = FirebaseAuth.getInstance().updateUserAsync(request).get();
+    UserRecord userRecord = FirebaseAuth.getInstance().updateUser(request);
     System.out.println("Successfully updated user: " + userRecord.getUid());
     // [END update_user]
   }
 
   public static void setCustomUserClaims(
-      String uid) throws InterruptedException, ExecutionException {
+      String uid) throws FirebaseAuthException {
     // [START set_custom_user_claims]
     // Set admin privilege on the user corresponding to uid.
     Map<String, Object> claims = new HashMap<>();
     claims.put("admin", true);
-    FirebaseAuth.getInstance().setCustomUserClaimsAsync(uid, claims).get();
+    FirebaseAuth.getInstance().setCustomUserClaims(uid, claims);
     // The new custom claims will propagate to the user's ID token the
     // next time a new one is issued.
     // [END set_custom_user_claims]
@@ -129,7 +128,7 @@ public class FirebaseAuthSnippets {
     String idToken = "id_token";
     // [START verify_custom_claims]
     // Verify the ID token first.
-    FirebaseToken decoded = FirebaseAuth.getInstance().verifyIdTokenAsync(idToken).get();
+    FirebaseToken decoded = FirebaseAuth.getInstance().verifyIdToken(idToken);
     if (Boolean.TRUE.equals(decoded.getClaims().get("admin"))) {
       // Allow access to requested admin resource.
     }
@@ -137,43 +136,43 @@ public class FirebaseAuthSnippets {
 
     // [START read_custom_user_claims]
     // Lookup the user associated with the specified uid.
-    UserRecord user = FirebaseAuth.getInstance().getUserAsync(uid).get();
+    UserRecord user = FirebaseAuth.getInstance().getUser(uid);
     System.out.println(user.getCustomClaims().get("admin"));
     // [END read_custom_user_claims]
   }
 
-  public static void setCustomUserClaimsScript() throws InterruptedException, ExecutionException {
+  public static void setCustomUserClaimsScript() throws FirebaseAuthException {
     // [START set_custom_user_claims_script]
     UserRecord user = FirebaseAuth.getInstance()
-        .getUserByEmailAsync("user@admin.example.com").get();
+        .getUserByEmail("user@admin.example.com");
     // Confirm user is verified.
     if (user.isEmailVerified()) {
       Map<String, Object> claims = new HashMap<>();
       claims.put("admin", true);
-      FirebaseAuth.getInstance().setCustomUserClaimsAsync(user.getUid(), claims).get();
+      FirebaseAuth.getInstance().setCustomUserClaims(user.getUid(), claims);
     }
     // [END set_custom_user_claims_script]
   }
 
-  public static void setCustomUserClaimsInc() throws InterruptedException, ExecutionException {
+  public static void setCustomUserClaimsInc() throws FirebaseAuthException {
     // [START set_custom_user_claims_incremental]
     UserRecord user = FirebaseAuth.getInstance()
-        .getUserByEmailAsync("user@admin.example.com").get();
+        .getUserByEmail("user@admin.example.com");
     // Add incremental custom claim without overwriting the existing claims.
     Map<String, Object> currentClaims = user.getCustomClaims();
     if (Boolean.TRUE.equals(currentClaims.get("admin"))) {
       // Add level.
       currentClaims.put("level", 10);
       // Add custom claims for additional privileges.
-      FirebaseAuth.getInstance().setCustomUserClaimsAsync(user.getUid(), currentClaims).get();
+      FirebaseAuth.getInstance().setCustomUserClaims(user.getUid(), currentClaims);
     }
     // [END set_custom_user_claims_incremental]
   }
 
-  public static void listAllUsers() throws InterruptedException, ExecutionException  {
+  public static void listAllUsers() throws FirebaseAuthException {
     // [START list_all_users]
     // Start listing users from the beginning, 1000 at a time.
-    ListUsersPage page = FirebaseAuth.getInstance().listUsersAsync(null).get();
+    ListUsersPage page = FirebaseAuth.getInstance().listUsers(null);
     while (page != null) {
       for (ExportedUserRecord user : page.getValues()) {
         System.out.println("User: " + user.getUid());
@@ -183,82 +182,78 @@ public class FirebaseAuthSnippets {
 
     // Iterate through all users. This will still retrieve users in batches,
     // buffering no more than 1000 users in memory at a time.
-    page = FirebaseAuth.getInstance().listUsersAsync(null).get();
+    page = FirebaseAuth.getInstance().listUsers(null);
     for (ExportedUserRecord user : page.iterateAll()) {
       System.out.println("User: " + user.getUid());
     }
     // [END list_all_users]
   }
 
-  public static void deleteUser(String uid) throws InterruptedException, ExecutionException {
+  public static void deleteUser(String uid) throws FirebaseAuthException {
     // [START delete_user]
-    FirebaseAuth.getInstance().deleteUserAsync(uid).get();
+    FirebaseAuth.getInstance().deleteUser(uid);
     System.out.println("Successfully deleted user.");
     // [END delete_user]
   }
 
-  public static void createCustomToken() throws InterruptedException, ExecutionException {
+  public static void createCustomToken() throws FirebaseAuthException {
     // [START custom_token]
     String uid = "some-uid";
 
-    String customToken = FirebaseAuth.getInstance().createCustomTokenAsync(uid).get();
+    String customToken = FirebaseAuth.getInstance().createCustomToken(uid);
     // Send token back to client
     // [END custom_token]
     System.out.println("Created custom token: " + customToken);
   }
 
-  public static void createCustomTokenWithClaims() throws InterruptedException, ExecutionException {
+  public static void createCustomTokenWithClaims() throws FirebaseAuthException {
     // [START custom_token_with_claims]
     String uid = "some-uid";
     Map<String, Object> additionalClaims = new HashMap<String, Object>();
     additionalClaims.put("premiumAccount", true);
 
     String customToken = FirebaseAuth.getInstance()
-        .createCustomTokenAsync(uid, additionalClaims).get();
+        .createCustomToken(uid, additionalClaims);
     // Send token back to client
     // [END custom_token_with_claims]
     System.out.println("Created custom token: " + customToken);
   }
 
-  public static void verifyIdToken(
-      String idToken) throws InterruptedException, ExecutionException {
+  public static void verifyIdToken(String idToken) throws FirebaseAuthException {
     // [START verify_id_token]
     // idToken comes from the client app (shown above)
-    FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdTokenAsync(idToken).get();
+    FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdToken(idToken);
     String uid = decodedToken.getUid();
     // [END verify_id_token]
     System.out.println("Decoded ID token from user: " + uid);
   }
 
-  public static void verifyIdTokenCheckRevoked(String idToken) throws InterruptedException {
+  public static void verifyIdTokenCheckRevoked(String idToken) {
     // [START verify_id_token_check_revoked]
     try {
       // Verify the ID token while checking if the token is revoked by passing checkRevoked
       // as true.
       boolean checkRevoked = true;
       FirebaseToken decodedToken = FirebaseAuth.getInstance()
-          .verifyIdTokenAsync(idToken, checkRevoked).get();
+          .verifyIdToken(idToken, checkRevoked);
       // Token is valid and not revoked.
       String uid = decodedToken.getUid();
-    } catch (ExecutionException e) {
-      if (e.getCause() instanceof FirebaseAuthException) {
-        FirebaseAuthException authError = (FirebaseAuthException) e.getCause();
-        if (authError.getErrorCode().equals("id-token-revoked")) {
-          // Token has been revoked. Inform the user to reauthenticate or signOut() the user.
-        } else {
-          // Token is invalid.
-        }
+    } catch (FirebaseAuthException e) {
+      if (e.getErrorCode().equals("id-token-revoked")) {
+        // Token has been revoked. Inform the user to re-authenticate or signOut() the user.
+      } else {
+        // Token is invalid.
       }
     }
     // [END verify_id_token_check_revoked]
   }
 
   public static void revokeIdTokens(
-      String idToken) throws InterruptedException, ExecutionException {
+      String idToken) throws FirebaseAuthException {
     String uid = "someUid";
     // [START revoke_tokens]
-    FirebaseAuth.getInstance().revokeRefreshTokensAsync(uid).get();
-    UserRecord user = FirebaseAuth.getInstance().getUserAsync(uid).get();
+    FirebaseAuth.getInstance().revokeRefreshTokens(uid);
+    UserRecord user = FirebaseAuth.getInstance().getUser(uid);
     // Convert to seconds as the auth_time in the token claims is in seconds too.
     long revocationSecond = user.getTokensValidAfterTimestamp() / 1000;
     System.out.println("Tokens revoked at: " + revocationSecond);
@@ -268,7 +263,7 @@ public class FirebaseAuthSnippets {
     DatabaseReference ref = FirebaseDatabase.getInstance().getReference("metadata/" + uid);
     Map<String, Object> userData = new HashMap<>();
     userData.put("revokeTime", revocationSecond);
-    ref.setValueAsync(userData).get();
+    ref.setValueAsync(userData);
     // [END save_revocation_in_db]
   }
 
@@ -295,23 +290,22 @@ public class FirebaseAuthSnippets {
     try {
       // Create the session cookie. This will also verify the ID token in the process.
       // The session cookie will have the same claims as the ID token.
-      String sessionCookie = FirebaseAuth.getInstance().createSessionCookieAsync(
-          idToken, options).get();
+      String sessionCookie = FirebaseAuth.getInstance().createSessionCookie(idToken, options);
       // Set cookie policy parameters as required.
       NewCookie cookie = new NewCookie("session", sessionCookie /* ... other parameters */);
       return Response.ok().cookie(cookie).build();
-    } catch (Exception e) {
+    } catch (FirebaseAuthException e) {
       return Response.status(Status.UNAUTHORIZED).entity("Failed to create a session cookie")
           .build();
     }
   }
   // [END session_login]
 
-  public Response checkAuthTime(String idToken) throws Exception {
+  public Response checkAuthTime(String idToken) throws FirebaseAuthException {
     // [START check_auth_time]
     // To ensure that cookies are set only on recently signed in users, check auth_time in
     // ID token before creating a cookie.
-    FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdTokenAsync(idToken).get();
+    FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdToken(idToken);
     long authTimeMillis = TimeUnit.SECONDS.toMillis(
         (long) decodedToken.getClaims().get("auth_time"));
 
@@ -321,8 +315,7 @@ public class FirebaseAuthSnippets {
       SessionCookieOptions options = SessionCookieOptions.builder()
           .setExpiresIn(expiresIn)
           .build();
-      String sessionCookie = FirebaseAuth.getInstance().createSessionCookieAsync(
-          idToken, options).get();
+      String sessionCookie = FirebaseAuth.getInstance().createSessionCookie(idToken, options);
       // Set cookie policy parameters as required.
       NewCookie cookie = new NewCookie("session", sessionCookie);
       return Response.ok().cookie(cookie).build();
@@ -350,10 +343,10 @@ public class FirebaseAuthSnippets {
       // Verify the session cookie. In this case an additional check is added to detect
       // if the user's Firebase session was revoked, user deleted/disabled, etc.
       final boolean checkRevoked = true;
-      FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookieAsync(
-          sessionCookie, checkRevoked).get();
+      FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookie(
+          sessionCookie, checkRevoked);
       return serveContentForUser(decodedToken);
-    } catch (Exception e) {
+    } catch (FirebaseAuthException e) {
       // Session cookie is unavailable, invalid or revoked. Force user to login.
       return Response.temporaryRedirect(URI.create("/login")).build();
     }
@@ -364,13 +357,13 @@ public class FirebaseAuthSnippets {
     // [START session_verify_with_permission_check]
     try {
       final boolean checkRevoked = true;
-      FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookieAsync(
-          sessionCookie, checkRevoked).get();
+      FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookie(
+          sessionCookie, checkRevoked);
       if (Boolean.TRUE.equals(decodedToken.getClaims().get("admin"))) {
         return serveContentForAdmin(decodedToken);
       }
       return Response.status(Status.UNAUTHORIZED).entity("Insufficient permissions").build();
-    } catch (Exception e) {
+    } catch (FirebaseAuthException e) {
       // Session cookie is unavailable, invalid or revoked. Force user to login.
       return Response.temporaryRedirect(URI.create("/login")).build();
     }
@@ -393,13 +386,12 @@ public class FirebaseAuthSnippets {
   public Response clearSessionCookieAndRevoke(@CookieParam("session") Cookie cookie) {
     String sessionCookie = cookie.getValue();
     try {
-      FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookieAsync(
-          sessionCookie).get();
-      FirebaseAuth.getInstance().revokeRefreshTokensAsync(decodedToken.getUid()).get();
+      FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookie(sessionCookie);
+      FirebaseAuth.getInstance().revokeRefreshTokens(decodedToken.getUid());
       final int maxAge = 0;
       NewCookie newCookie = new NewCookie(cookie, null, maxAge, true);
       return Response.temporaryRedirect(URI.create("/login")).cookie(newCookie).build();
-    } catch (Exception e) {
+    } catch (FirebaseAuthException e) {
       return Response.temporaryRedirect(URI.create("/login")).build();
     }
   }
diff --git a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java
index 67bb62f..316e206 100644
--- a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java
+++ b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java
@@ -22,6 +22,7 @@ import com.google.firebase.messaging.ApnsConfig;
 import com.google.firebase.messaging.Aps;
 import com.google.firebase.messaging.ApsAlert;
 import com.google.firebase.messaging.FirebaseMessaging;
+import com.google.firebase.messaging.FirebaseMessagingException;
 import com.google.firebase.messaging.Message;
 import com.google.firebase.messaging.Notification;
 import com.google.firebase.messaging.TopicManagementResponse;
@@ -35,7 +36,7 @@ import java.util.List;
  */
 public class FirebaseMessagingSnippets {
 
-  public void sendToToken() throws Exception {
+  public void sendToToken() throws FirebaseMessagingException {
     // [START send_to_token]
     // This registration token comes from the client FCM SDKs.
     String registrationToken = "YOUR_REGISTRATION_TOKEN";
@@ -49,13 +50,13 @@ public class FirebaseMessagingSnippets {
 
     // Send a message to the device corresponding to the provided
     // registration token.
-    String response = FirebaseMessaging.getInstance().sendAsync(message).get();
+    String response = FirebaseMessaging.getInstance().send(message);
     // Response is a message ID string.
     System.out.println("Successfully sent message: " + response);
     // [END send_to_token]
   }
 
-  public void sendToTopic() throws Exception {
+  public void sendToTopic() throws FirebaseMessagingException {
     // [START send_to_topic]
     // The topic name can be optionally prefixed with "/topics/".
     String topic = "highScores";
@@ -68,13 +69,13 @@ public class FirebaseMessagingSnippets {
         .build();
 
     // Send a message to the devices subscribed to the provided topic.
-    String response = FirebaseMessaging.getInstance().sendAsync(message).get();
+    String response = FirebaseMessaging.getInstance().send(message);
     // Response is a message ID string.
     System.out.println("Successfully sent message: " + response);
     // [END send_to_topic]
   }
 
-  public void sendToCondition() throws Exception {
+  public void sendToCondition() throws FirebaseMessagingException {
     // [START send_to_condition]
     // Define a condition which will send to devices which are subscribed
     // to either the Google stock or the tech industry topics.
@@ -90,13 +91,13 @@ public class FirebaseMessagingSnippets {
 
     // Send a message to devices subscribed to the combination of topics
     // specified by the provided condition.
-    String response = FirebaseMessaging.getInstance().sendAsync(message).get();
+    String response = FirebaseMessaging.getInstance().send(message);
     // Response is a message ID string.
     System.out.println("Successfully sent message: " + response);
     // [END send_to_condition]
   }
 
-  public void sendDryRun() throws Exception {
+  public void sendDryRun() throws FirebaseMessagingException {
     Message message = Message.builder()
         .putData("score", "850")
         .putData("time", "2:45")
@@ -106,7 +107,7 @@ public class FirebaseMessagingSnippets {
     // [START send_dry_run]
     // Send a message in the dry run mode.
     boolean dryRun = true;
-    String response = FirebaseMessaging.getInstance().sendAsync(message, dryRun).get();
+    String response = FirebaseMessaging.getInstance().send(message, dryRun);
     // Response is a message ID string.
     System.out.println("Dry run successful: " + response);
     // [END send_dry_run]
@@ -189,7 +190,7 @@ public class FirebaseMessagingSnippets {
     return message;
   }
 
-  public void subscribeToTopic() throws Exception {
+  public void subscribeToTopic() throws FirebaseMessagingException {
     String topic = "highScores";
     // [START subscribe]
     // These registration tokens come from the client FCM SDKs.
@@ -201,15 +202,15 @@ public class FirebaseMessagingSnippets {
 
     // Subscribe the devices corresponding to the registration tokens to the
     // topic.
-    TopicManagementResponse response = FirebaseMessaging.getInstance().subscribeToTopicAsync(
-        registrationTokens, topic).get();
+    TopicManagementResponse response = FirebaseMessaging.getInstance().subscribeToTopic(
+        registrationTokens, topic);
     // See the TopicManagementResponse reference documentation
     // for the contents of response.
     System.out.println(response.getSuccessCount() + " tokens were subscribed successfully");
     // [END subscribe]
   }
 
-  public void unsubscribeFromTopic() throws Exception {
+  public void unsubscribeFromTopic() throws FirebaseMessagingException {
     String topic = "highScores";
     // [START unsubscribe]
     // These registration tokens come from the client FCM SDKs.
@@ -221,8 +222,8 @@ public class FirebaseMessagingSnippets {
 
     // Unsubscribe the devices corresponding to the registration tokens from
     // the topic.
-    TopicManagementResponse response = FirebaseMessaging.getInstance().unsubscribeFromTopicAsync(
-        registrationTokens, topic).get();
+    TopicManagementResponse response = FirebaseMessaging.getInstance().unsubscribeFromTopic(
+        registrationTokens, topic);
     // See the TopicManagementResponse reference documentation
     // for the contents of response.
     System.out.println(response.getSuccessCount() + " tokens were unsubscribed successfully");
diff --git a/src/test/resources/importUsersError.json b/src/test/resources/importUsersError.json
new file mode 100644
index 0000000..9233f8a
--- /dev/null
+++ b/src/test/resources/importUsersError.json
@@ -0,0 +1,6 @@
+{
+  "error": [
+    {"index": 0, "message": "Some error occurred in user1"},
+    {"index": 2, "message": "Another error occurred in user3"}
+  ]
+}