Repository: firebase/firebase-admin-java
Pull request #449 (2020-07-07 21:51:28)

Moved tenant management code into a new package
* Moved `TenantManager` and related classes into a new `multitenancy` package.
* Added `AuthHttpClient` and `FirebaseTenantClient` internal APIs for making REST calls.


Patch:
diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
index a864c4f..35f2227 100644
--- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
@@ -21,6 +21,7 @@ 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.api.client.util.Clock;
 import com.google.api.core.ApiFuture;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
@@ -64,7 +65,7 @@ public abstract class AbstractFirebaseAuth {
   private final Supplier<? extends FirebaseUserManager> userManager;
   private final JsonFactory jsonFactory;
 
-  AbstractFirebaseAuth(Builder builder) {
+  protected AbstractFirebaseAuth(Builder builder) {
     this.firebaseApp = checkNotNull(builder.firebaseApp);
     this.tokenFactory = threadSafeMemoize(builder.tokenFactory);
     this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier);
@@ -73,6 +74,43 @@ public abstract class AbstractFirebaseAuth {
     this.jsonFactory = builder.firebaseApp.getOptions().getJsonFactory();
   }
 
+  protected static Builder builderFromAppAndTenantId(final FirebaseApp app, final String tenantId) {
+    return AbstractFirebaseAuth.builder()
+        .setFirebaseApp(app)
+        .setTokenFactory(
+            new Supplier<FirebaseTokenFactory>() {
+              @Override
+              public FirebaseTokenFactory get() {
+                return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM, tenantId);
+              }
+            })
+        .setIdTokenVerifier(
+            new Supplier<FirebaseTokenVerifier>() {
+              @Override
+              public FirebaseTokenVerifier get() {
+                return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM, tenantId);
+              }
+            })
+        .setCookieVerifier(
+            new Supplier<FirebaseTokenVerifier>() {
+              @Override
+              public FirebaseTokenVerifier get() {
+                return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM);
+              }
+            })
+        .setUserManager(
+            new Supplier<FirebaseUserManager>() {
+              @Override
+              public FirebaseUserManager get() {
+                return FirebaseUserManager
+                    .builder()
+                    .setFirebaseApp(app)
+                    .setTenantId(tenantId)
+                    .build();
+              }
+            });
+  }
+
   /**
    * Creates a Firebase custom token for the given UID. This token can then be sent back to a client
    * application to be used with the <a
diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
index ff52aca..f44bd23 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
@@ -27,10 +27,10 @@ import com.google.common.base.Supplier;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.ImplFirebaseTrampolines;
 import com.google.firebase.auth.internal.FirebaseTokenFactory;
+import com.google.firebase.auth.multitenancy.TenantManager;
 import com.google.firebase.internal.CallableOperation;
 import com.google.firebase.internal.FirebaseService;
 import com.google.firebase.internal.NonNull;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * This class is the entry point for all server-side Firebase Authentication actions.
@@ -45,15 +45,13 @@ public final class FirebaseAuth extends AbstractFirebaseAuth {
   private static final String SERVICE_ID = FirebaseAuth.class.getName();
 
   private final Supplier<TenantManager> tenantManager;
-  private final AtomicBoolean tenantManagerCreated = new AtomicBoolean(false);
 
   FirebaseAuth(final Builder builder) {
     super(builder);
     tenantManager = threadSafeMemoize(new Supplier<TenantManager>() {
       @Override
       public TenantManager get() {
-        tenantManagerCreated.set(true);
-        return new TenantManager(builder.firebaseApp, getUserManager());
+        return new TenantManager(builder.firebaseApp);
       }
     });
   }
@@ -213,12 +211,7 @@ public final class FirebaseAuth extends AbstractFirebaseAuth {
   }
 
   @Override
-  protected void doDestroy() {
-    // Only destroy the tenant manager if it has been created.
-    if (tenantManagerCreated.get()) {
-      getTenantManager().destroy();
-    }
-  }
+  protected void doDestroy() { }
 
   private static FirebaseAuth fromApp(final FirebaseApp app) {
     return new FirebaseAuth(
diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java
index a1c1f61..e1a5a9a 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java
@@ -46,6 +46,7 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
       "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit";
   private static final String ERROR_INVALID_CREDENTIAL = "ERROR_INVALID_CREDENTIAL";
   private static final String ERROR_RUNTIME_EXCEPTION = "ERROR_RUNTIME_EXCEPTION";
+  static final String TENANT_ID_MISMATCH_ERROR = "tenant-id-mismatch";
 
   private final JsonFactory jsonFactory;
   private final GooglePublicKeysManager publicKeysManager;
@@ -287,7 +288,7 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
     String tokenTenantId = Strings.nullToEmpty(firebaseToken.getTenantId());
     if (!this.tenantId.equals(tokenTenantId)) {
       throw new FirebaseAuthException(
-          FirebaseUserManager.TENANT_ID_MISMATCH_ERROR,
+          TENANT_ID_MISMATCH_ERROR,
           String.format(
             "The tenant ID ('%s') of the token did not match the expected value ('%s')",
             tokenTenantId,
diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
index c65e44d..a2c17aa 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -20,40 +20,30 @@ import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.api.client.http.GenericUrl;
-import com.google.api.client.http.HttpContent;
-import com.google.api.client.http.HttpRequest;
 import com.google.api.client.http.HttpRequestFactory;
-import com.google.api.client.http.HttpResponse;
-import com.google.api.client.http.HttpResponseException;
 import com.google.api.client.http.HttpResponseInterceptor;
-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.Joiner;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSortedSet;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.ImplFirebaseTrampolines;
+import com.google.firebase.auth.internal.AuthHttpClient;
 import com.google.firebase.auth.internal.BatchDeleteResponse;
 import com.google.firebase.auth.internal.DownloadAccountResponse;
 import com.google.firebase.auth.internal.GetAccountInfoRequest;
 import com.google.firebase.auth.internal.GetAccountInfoResponse;
-import com.google.firebase.auth.internal.HttpErrorResponse;
 import com.google.firebase.auth.internal.ListOidcProviderConfigsResponse;
 import com.google.firebase.auth.internal.ListSamlProviderConfigsResponse;
-import com.google.firebase.auth.internal.ListTenantsResponse;
 import com.google.firebase.auth.internal.UploadAccountResponse;
 import com.google.firebase.internal.ApiClientUtils;
 import com.google.firebase.internal.NonNull;
 import com.google.firebase.internal.Nullable;
-import com.google.firebase.internal.SdkUtils;
 
-import java.io.IOException;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
@@ -71,37 +61,7 @@ import java.util.Set;
  */
 class FirebaseUserManager {
 
-  static final String CONFIGURATION_NOT_FOUND_ERROR = "configuration-not-found";
-  static final String TENANT_ID_MISMATCH_ERROR = "tenant-id-mismatch";
-  static final String TENANT_NOT_FOUND_ERROR = "tenant-not-found";
-  static final String USER_NOT_FOUND_ERROR = "user-not-found";
-  static final String INTERNAL_ERROR = "internal-error";
-
-  // Map of server-side error codes to SDK error codes.
-  // SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors
-  private static final Map<String, String> ERROR_CODES = ImmutableMap.<String, String>builder()
-      .put("CLAIMS_TOO_LARGE", "claims-too-large")
-      .put("CONFIGURATION_NOT_FOUND", CONFIGURATION_NOT_FOUND_ERROR)
-      .put("INSUFFICIENT_PERMISSION", "insufficient-permission")
-      .put("DUPLICATE_EMAIL", "email-already-exists")
-      .put("DUPLICATE_LOCAL_ID", "uid-already-exists")
-      .put("EMAIL_EXISTS", "email-already-exists")
-      .put("INVALID_CLAIMS", "invalid-claims")
-      .put("INVALID_EMAIL", "invalid-email")
-      .put("INVALID_PAGE_SELECTION", "invalid-page-token")
-      .put("INVALID_PHONE_NUMBER", "invalid-phone-number")
-      .put("PHONE_NUMBER_EXISTS", "phone-number-already-exists")
-      .put("PROJECT_NOT_FOUND", "project-not-found")
-      .put("TENANT_ID_MISMATCH", TENANT_ID_MISMATCH_ERROR)
-      .put("TENANT_NOT_FOUND", TENANT_NOT_FOUND_ERROR)
-      .put("USER_NOT_FOUND", USER_NOT_FOUND_ERROR)
-      .put("WEAK_PASSWORD", "invalid-password")
-      .put("UNAUTHORIZED_DOMAIN", "unauthorized-continue-uri")
-      .put("INVALID_DYNAMIC_LINK_DOMAIN", "invalid-dynamic-link-domain")
-      .build();
-
   static final int MAX_LIST_PROVIDER_CONFIGS_RESULTS = 100;
-  static final int MAX_LIST_TENANTS_RESULTS = 1000;
   static final int MAX_GET_ACCOUNTS_BATCH_SIZE = 100;
   static final int MAX_DELETE_ACCOUNTS_BATCH_SIZE = 1000;
   static final int MAX_LIST_USERS_RESULTS = 1000;
@@ -113,18 +73,13 @@ class FirebaseUserManager {
 
   private static final String ID_TOOLKIT_URL =
       "https://identitytoolkit.googleapis.com/%s/projects/%s";
-  private static final String CLIENT_VERSION_HEADER = "X-Client-Version";
 
   private final String userMgtBaseUrl;
   private final String idpConfigMgtBaseUrl;
-  private final String tenantMgtBaseUrl;
   private final JsonFactory jsonFactory;
-  private final HttpRequestFactory requestFactory;
-  private final String clientVersion = "Java/Admin/" + SdkUtils.getVersion();
-
-  private HttpResponseInterceptor interceptor;
+  private final AuthHttpClient httpClient;
 
-  FirebaseUserManager(Builder builder) {
+  private FirebaseUserManager(Builder builder) {
     FirebaseApp app = checkNotNull(builder.app, "FirebaseApp must not be null");
     String projectId = ImplFirebaseTrampolines.getProjectId(app);
     checkArgument(!Strings.isNullOrEmpty(projectId),
@@ -139,18 +94,19 @@ class FirebaseUserManager {
       this.idpConfigMgtBaseUrl = idToolkitUrlV2;
     } else {
       checkArgument(!tenantId.isEmpty(), "Tenant ID must not be empty.");
-      this.userMgtBaseUrl = idToolkitUrlV1 + getTenantUrlSuffix(tenantId);
-      this.idpConfigMgtBaseUrl = idToolkitUrlV2 + getTenantUrlSuffix(tenantId);
+      this.userMgtBaseUrl = idToolkitUrlV1 + "/tenants/" + tenantId;
+      this.idpConfigMgtBaseUrl = idToolkitUrlV2 + "/tenants/" + tenantId;
     }
-    this.tenantMgtBaseUrl = idToolkitUrlV2;
+
     this.jsonFactory = app.getOptions().getJsonFactory();
-    this.requestFactory = builder.requestFactory == null
-      ? ApiClientUtils.newAuthorizedRequestFactory(app) : builder.requestFactory;
+    HttpRequestFactory requestFactory = builder.requestFactory == null
+        ? ApiClientUtils.newAuthorizedRequestFactory(app) : builder.requestFactory;
+    this.httpClient = new AuthHttpClient(jsonFactory, requestFactory);
   }
 
   @VisibleForTesting
   void setInterceptor(HttpResponseInterceptor interceptor) {
-    this.interceptor = interceptor;
+    httpClient.setInterceptor(interceptor);
   }
 
   UserRecord getUserById(String uid) throws FirebaseAuthException {
@@ -159,7 +115,8 @@ class FirebaseUserManager {
     GetAccountInfoResponse response = post(
         "/accounts:lookup", payload, GetAccountInfoResponse.class);
     if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) {
-      throw new FirebaseAuthException(USER_NOT_FOUND_ERROR,
+      throw new FirebaseAuthException(
+          AuthHttpClient.USER_NOT_FOUND_ERROR,
           "No user record found for the provided user ID: " + uid);
     }
     return new UserRecord(response.getUsers().get(0), jsonFactory);
@@ -171,7 +128,8 @@ class FirebaseUserManager {
     GetAccountInfoResponse response = post(
         "/accounts:lookup", payload, GetAccountInfoResponse.class);
     if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) {
-      throw new FirebaseAuthException(USER_NOT_FOUND_ERROR,
+      throw new FirebaseAuthException(
+          AuthHttpClient.USER_NOT_FOUND_ERROR,
           "No user record found for the provided email: " + email);
     }
     return new UserRecord(response.getUsers().get(0), jsonFactory);
@@ -183,7 +141,8 @@ class FirebaseUserManager {
     GetAccountInfoResponse response = post(
         "/accounts:lookup", payload, GetAccountInfoResponse.class);
     if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) {
-      throw new FirebaseAuthException(USER_NOT_FOUND_ERROR,
+      throw new FirebaseAuthException(
+          AuthHttpClient.USER_NOT_FOUND_ERROR,
           "No user record found for the provided phone number: " + phoneNumber);
     }
     return new UserRecord(response.getUsers().get(0), jsonFactory);
@@ -204,7 +163,8 @@ class FirebaseUserManager {
         "/accounts:lookup", payload, GetAccountInfoResponse.class);
 
     if (response == null) {
-      throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to parse server response");
+      throw new FirebaseAuthException(
+          AuthHttpClient.INTERNAL_ERROR, "Failed to parse server response");
     }
 
     Set<UserRecord> results = new HashSet<>();
@@ -225,7 +185,7 @@ class FirebaseUserManager {
         return uid;
       }
     }
-    throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create new user");
+    throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to create new user");
   }
 
   void updateUser(UserRecord.UpdateRequest request, JsonFactory jsonFactory)
@@ -233,7 +193,8 @@ class FirebaseUserManager {
     GenericJson response = post(
         "/accounts:update", request.getProperties(jsonFactory), GenericJson.class);
     if (response == null || !request.getUid().equals(response.get("localId"))) {
-      throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to update user: " + request.getUid());
+      throw new FirebaseAuthException(
+          AuthHttpClient.INTERNAL_ERROR, "Failed to update user: " + request.getUid());
     }
   }
 
@@ -242,7 +203,8 @@ class FirebaseUserManager {
     GenericJson response = post(
         "/accounts:delete", payload, GenericJson.class);
     if (response == null || !response.containsKey("kind")) {
-      throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to delete user: " + uid);
+      throw new FirebaseAuthException(
+          AuthHttpClient.INTERNAL_ERROR, "Failed to delete user: " + uid);
     }
   }
 
@@ -257,7 +219,7 @@ class FirebaseUserManager {
     BatchDeleteResponse response = post(
         "/accounts:batchDelete", payload, BatchDeleteResponse.class);
     if (response == null) {
-      throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to delete users");
+      throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to delete users");
     }
 
     return new DeleteUsersResult(uids.size(), response);
@@ -273,10 +235,10 @@ class FirebaseUserManager {
 
     GenericUrl url = new GenericUrl(userMgtBaseUrl + "/accounts:batchGet");
     url.putAll(builder.build());
-    DownloadAccountResponse response = sendRequest(
+    DownloadAccountResponse response = httpClient.sendRequest(
             "GET", url, null, DownloadAccountResponse.class);
     if (response == null) {
-      throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve users.");
+      throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve users.");
     }
     return response;
   }
@@ -286,52 +248,11 @@ class FirebaseUserManager {
     UploadAccountResponse response = post(
             "/accounts:batchCreate", request, UploadAccountResponse.class);
     if (response == null) {
-      throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to import users.");
+      throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to import users.");
     }
     return new UserImportResult(request.getUsersCount(), response);
   }
 
-  Tenant getTenant(String tenantId) throws FirebaseAuthException {
-    GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId));
-    return sendRequest("GET", url, null, Tenant.class);
-  }
-
-  Tenant createTenant(Tenant.CreateRequest request) throws FirebaseAuthException {
-    GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants");
-    return sendRequest("POST", url, request.getProperties(), Tenant.class);
-  }
-
-  Tenant updateTenant(Tenant.UpdateRequest request) throws FirebaseAuthException {
-    Map<String, Object> properties = request.getProperties();
-    GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(request.getTenantId()));
-    url.put("updateMask", Joiner.on(",").join(generateMask(properties)));
-    return sendRequest("PATCH", url, properties, Tenant.class);
-  }
-
-  void deleteTenant(String tenantId) throws FirebaseAuthException {
-    GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId));
-    sendRequest("DELETE", url, null, GenericJson.class);
-  }
-
-  ListTenantsResponse listTenants(int maxResults, String pageToken)
-      throws FirebaseAuthException {
-    ImmutableMap.Builder<String, Object> builder =
-        ImmutableMap.<String, Object>builder().put("pageSize", maxResults);
-    if (pageToken != null) {
-      checkArgument(!pageToken.equals(
-          ListTenantsPage.END_OF_LIST), "Invalid end of list page token");
-      builder.put("pageToken", pageToken);
-    }
-
-    GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants");
-    url.putAll(builder.build());
-    ListTenantsResponse response = sendRequest("GET", url, null, ListTenantsResponse.class);
-    if (response == null) {
-      throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve tenants.");
-    }
-    return response;
-  }
-
   String createSessionCookie(String idToken,
       SessionCookieOptions options) throws FirebaseAuthException {
     final Map<String, Object> payload = ImmutableMap.<String, Object>of(
@@ -343,7 +264,8 @@ class FirebaseUserManager {
         return cookie;
       }
     }
-    throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create session cookie");
+    throw new FirebaseAuthException(
+        AuthHttpClient.INTERNAL_ERROR, "Failed to create session cookie");
   }
 
   String getEmailActionLink(EmailLinkType type, String email,
@@ -362,21 +284,22 @@ class FirebaseUserManager {
         return link;
       }
     }
-    throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create email action link");
+    throw new FirebaseAuthException(
+        AuthHttpClient.INTERNAL_ERROR, "Failed to create email action link");
   }
 
   OidcProviderConfig createOidcProviderConfig(
       OidcProviderConfig.CreateRequest request) throws FirebaseAuthException {
     GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs");
     url.set("oauthIdpConfigId", request.getProviderId());
-    return sendRequest("POST", url, request.getProperties(), OidcProviderConfig.class);
+    return httpClient.sendRequest("POST", url, request.getProperties(), OidcProviderConfig.class);
   }
 
   SamlProviderConfig createSamlProviderConfig(
       SamlProviderConfig.CreateRequest request) throws FirebaseAuthException {
     GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/inboundSamlConfigs");
     url.set("inboundSamlConfigId", request.getProviderId());
-    return sendRequest("POST", url, request.getProperties(), SamlProviderConfig.class);
+    return httpClient.sendRequest("POST", url, request.getProperties(), SamlProviderConfig.class);
   }
 
   OidcProviderConfig updateOidcProviderConfig(OidcProviderConfig.UpdateRequest request)
@@ -384,8 +307,8 @@ class FirebaseUserManager {
     Map<String, Object> properties = request.getProperties();
     GenericUrl url =
         new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(request.getProviderId()));
-    url.put("updateMask", Joiner.on(",").join(generateMask(properties)));
-    return sendRequest("PATCH", url, properties, OidcProviderConfig.class);
+    url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties)));
+    return httpClient.sendRequest("PATCH", url, properties, OidcProviderConfig.class);
   }
 
   SamlProviderConfig updateSamlProviderConfig(SamlProviderConfig.UpdateRequest request)
@@ -393,18 +316,18 @@ class FirebaseUserManager {
     Map<String, Object> properties = request.getProperties();
     GenericUrl url =
         new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(request.getProviderId()));
-    url.put("updateMask", Joiner.on(",").join(generateMask(properties)));
-    return sendRequest("PATCH", url, properties, SamlProviderConfig.class);
+    url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties)));
+    return httpClient.sendRequest("PATCH", url, properties, SamlProviderConfig.class);
   }
 
   OidcProviderConfig getOidcProviderConfig(String providerId) throws FirebaseAuthException {
     GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId));
-    return sendRequest("GET", url, null, OidcProviderConfig.class);
+    return httpClient.sendRequest("GET", url, null, OidcProviderConfig.class);
   }
 
   SamlProviderConfig getSamlProviderConfig(String providerId) throws FirebaseAuthException {
     GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId));
-    return sendRequest("GET", url, null, SamlProviderConfig.class);
+    return httpClient.sendRequest("GET", url, null, SamlProviderConfig.class);
   }
 
   ListOidcProviderConfigsResponse listOidcProviderConfigs(int maxResults, String pageToken)
@@ -420,9 +343,10 @@ class FirebaseUserManager {
     GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs");
     url.putAll(builder.build());
     ListOidcProviderConfigsResponse response =
-        sendRequest("GET", url, null, ListOidcProviderConfigsResponse.class);
+        httpClient.sendRequest("GET", url, null, ListOidcProviderConfigsResponse.class);
     if (response == null) {
-      throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve provider configs.");
+      throw new FirebaseAuthException(
+          AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve provider configs.");
     }
     return response;
   }
@@ -440,41 +364,22 @@ class FirebaseUserManager {
     GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/inboundSamlConfigs");
     url.putAll(builder.build());
     ListSamlProviderConfigsResponse response =
-        sendRequest("GET", url, null, ListSamlProviderConfigsResponse.class);
+        httpClient.sendRequest("GET", url, null, ListSamlProviderConfigsResponse.class);
     if (response == null) {
-      throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve provider configs.");
+      throw new FirebaseAuthException(
+          AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve provider configs.");
     }
     return response;
   }
 
   void deleteOidcProviderConfig(String providerId) throws FirebaseAuthException {
     GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId));
-    sendRequest("DELETE", url, null, GenericJson.class);
+    httpClient.sendRequest("DELETE", url, null, GenericJson.class);
   }
 
   void deleteSamlProviderConfig(String providerId) throws FirebaseAuthException {
     GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId));
-    sendRequest("DELETE", url, null, GenericJson.class);
-  }
-
-  private static Set<String> generateMask(Map<String, Object> properties) {
-    ImmutableSortedSet.Builder<String> maskBuilder = ImmutableSortedSet.naturalOrder();
-    for (Map.Entry<String, Object> entry : properties.entrySet()) {
-      if (entry.getValue() instanceof Map) {
-        Set<String> childMask = generateMask((Map<String, Object>) entry.getValue());
-        for (String childProperty : childMask) {
-          maskBuilder.add(entry.getKey() + "." + childProperty);
-        }
-      } else {
-        maskBuilder.add(entry.getKey());
-      }
-    }
-    return maskBuilder.build();
-  }
-
-  private static String getTenantUrlSuffix(String tenantId) {
-    checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty.");
-    return "/tenants/" + tenantId;
+    httpClient.sendRequest("DELETE", url, null, GenericJson.class);
   }
 
   private static String getOidcUrlSuffix(String providerId) {
@@ -491,61 +396,7 @@ class FirebaseUserManager {
     checkArgument(!Strings.isNullOrEmpty(path), "path must not be null or empty");
     checkNotNull(content, "content must not be null for POST requests");
     GenericUrl url = new GenericUrl(userMgtBaseUrl + path);
-    return sendRequest("POST", url, content, clazz);
-  }
-
-  private <T> T sendRequest(
-          String method, GenericUrl url,
-          @Nullable Object content, Class<T> clazz) throws FirebaseAuthException {
-
-    checkArgument(!Strings.isNullOrEmpty(method), "method must not be null or empty");
-    checkNotNull(url, "url must not be null");
-    checkNotNull(clazz, "response class must not be null");
-    HttpResponse response = null;
-    try {
-      HttpContent httpContent = content != null ? new JsonHttpContent(jsonFactory, content) : null;
-      HttpRequest request =
-          requestFactory.buildRequest(method.equals("PATCH") ? "POST" : method, url, httpContent);
-      request.setParser(new JsonObjectParser(jsonFactory));
-      request.getHeaders().set(CLIENT_VERSION_HEADER, clientVersion);
-      if (method.equals("PATCH")) {
-        request.getHeaders().set("X-HTTP-Method-Override", "PATCH");
-      }
-      request.setResponseInterceptor(interceptor);
-      response = request.execute();
-      return response.parseAs(clazz);
-    } catch (HttpResponseException e) {
-      // Server responded with an HTTP error
-      handleHttpError(e);
-      return null;
-    } catch (IOException e) {
-      // All other IO errors (Connection refused, reset, parse error etc.)
-      throw new FirebaseAuthException(
-          INTERNAL_ERROR, "Error while calling user management backend service", e);
-    } finally {
-      if (response != null) {
-        try {
-          response.disconnect();
-        } catch (IOException ignored) {
-          // Ignored
-        }
-      }
-    }
-  }
-
-  private void handleHttpError(HttpResponseException e) throws FirebaseAuthException {
-    try {
-      HttpErrorResponse response = jsonFactory.fromString(e.getContent(), HttpErrorResponse.class);
-      String code = ERROR_CODES.get(response.getErrorCode());
-      if (code != null) {
-        throw new FirebaseAuthException(code, "User management service responded with an error", e);
-      }
-    } catch (IOException ignored) {
-      // Ignored
-    }
-    String msg = String.format(
-        "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent());
-    throw new FirebaseAuthException(INTERNAL_ERROR, msg, e);
+    return httpClient.sendRequest("POST", url, content, clazz);
   }
 
   static class UserImportRequest extends GenericJson {
diff --git a/src/main/java/com/google/firebase/auth/TenantAwareFirebaseAuth.java b/src/main/java/com/google/firebase/auth/TenantAwareFirebaseAuth.java
deleted file mode 100644
index f16586a..0000000
--- a/src/main/java/com/google/firebase/auth/TenantAwareFirebaseAuth.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright 2020 Google LLC
- *
- * 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.Clock;
-import com.google.common.base.Strings;
-import com.google.common.base.Supplier;
-import com.google.firebase.FirebaseApp;
-import com.google.firebase.auth.internal.FirebaseTokenFactory;
-
-/**
- * The tenant-aware Firebase client.
- *
- * <p>This can be used to perform a variety of authentication-related operations, scoped to a
- * particular tenant.
- */
-public final class TenantAwareFirebaseAuth extends AbstractFirebaseAuth {
-
-  private final String tenantId;
-
-  TenantAwareFirebaseAuth(final FirebaseApp firebaseApp, final String tenantId) {
-    super(builderFromAppAndTenantId(firebaseApp, tenantId));
-    checkArgument(!Strings.isNullOrEmpty(tenantId));
-    this.tenantId = tenantId;
-  }
-
-  private static Builder builderFromAppAndTenantId(final FirebaseApp app, final String tenantId) {
-    return AbstractFirebaseAuth.builder()
-        .setFirebaseApp(app)
-        .setTokenFactory(
-            new Supplier<FirebaseTokenFactory>() {
-              @Override
-              public FirebaseTokenFactory get() {
-                return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM, tenantId);
-              }
-            })
-        .setIdTokenVerifier(
-            new Supplier<FirebaseTokenVerifier>() {
-              @Override
-              public FirebaseTokenVerifier get() {
-                return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM, tenantId);
-              }
-            })
-        .setCookieVerifier(
-            new Supplier<FirebaseTokenVerifier>() {
-              @Override
-              public FirebaseTokenVerifier get() {
-                return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM);
-              }
-            })
-        .setUserManager(
-            new Supplier<FirebaseUserManager>() {
-              @Override
-              public FirebaseUserManager get() {
-                return FirebaseUserManager
-                  .builder()
-                  .setFirebaseApp(app)
-                  .setTenantId(tenantId)
-                  .build();
-              }
-            });
-  }
-
-  /** Returns the client's tenant ID. */
-  public String getTenantId() {
-    return tenantId;
-  }
-
-  @Override
-  protected void doDestroy() {
-    // Nothing extra needs to be destroyed.
-  }
-}
diff --git a/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java b/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java
new file mode 100644
index 0000000..ad77236
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2020 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 static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpContent;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.HttpResponseException;
+import com.google.api.client.http.HttpResponseInterceptor;
+import com.google.api.client.http.json.JsonHttpContent;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.JsonObjectParser;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.firebase.auth.FirebaseAuthException;
+import com.google.firebase.internal.Nullable;
+import com.google.firebase.internal.SdkUtils;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Provides a convenient API for making REST calls to the Firebase Auth backend servers.
+ */
+public final class AuthHttpClient {
+
+  public static final String CONFIGURATION_NOT_FOUND_ERROR = "configuration-not-found";
+  public static final String INTERNAL_ERROR = "internal-error";
+  public static final String TENANT_NOT_FOUND_ERROR = "tenant-not-found";
+  public static final String USER_NOT_FOUND_ERROR = "user-not-found";
+
+  private static final String CLIENT_VERSION_HEADER = "X-Client-Version";
+
+  private static final String CLIENT_VERSION = "Java/Admin/" + SdkUtils.getVersion();
+
+  // Map of server-side error codes to SDK error codes.
+  // SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors
+  private static final Map<String, String> ERROR_CODES = ImmutableMap.<String, String>builder()
+      .put("CLAIMS_TOO_LARGE", "claims-too-large")
+      .put("CONFIGURATION_NOT_FOUND", CONFIGURATION_NOT_FOUND_ERROR)
+      .put("INSUFFICIENT_PERMISSION", "insufficient-permission")
+      .put("DUPLICATE_EMAIL", "email-already-exists")
+      .put("DUPLICATE_LOCAL_ID", "uid-already-exists")
+      .put("EMAIL_EXISTS", "email-already-exists")
+      .put("INVALID_CLAIMS", "invalid-claims")
+      .put("INVALID_EMAIL", "invalid-email")
+      .put("INVALID_PAGE_SELECTION", "invalid-page-token")
+      .put("INVALID_PHONE_NUMBER", "invalid-phone-number")
+      .put("PHONE_NUMBER_EXISTS", "phone-number-already-exists")
+      .put("PROJECT_NOT_FOUND", "project-not-found")
+      .put("USER_NOT_FOUND", USER_NOT_FOUND_ERROR)
+      .put("WEAK_PASSWORD", "invalid-password")
+      .put("UNAUTHORIZED_DOMAIN", "unauthorized-continue-uri")
+      .put("INVALID_DYNAMIC_LINK_DOMAIN", "invalid-dynamic-link-domain")
+      .put("TENANT_NOT_FOUND", TENANT_NOT_FOUND_ERROR)
+      .build();
+
+  private final JsonFactory jsonFactory;
+  private final HttpRequestFactory requestFactory;
+
+  private HttpResponseInterceptor interceptor;
+
+  public AuthHttpClient(JsonFactory jsonFactory, HttpRequestFactory requestFactory) {
+    this.jsonFactory = jsonFactory;
+    this.requestFactory = requestFactory;
+  }
+
+  public static Set<String> generateMask(Map<String, Object> properties) {
+    ImmutableSortedSet.Builder<String> maskBuilder = ImmutableSortedSet.naturalOrder();
+    for (Map.Entry<String, Object> entry : properties.entrySet()) {
+      if (entry.getValue() instanceof Map) {
+        Set<String> childMask = generateMask((Map<String, Object>) entry.getValue());
+        for (String childProperty : childMask) {
+          maskBuilder.add(entry.getKey() + "." + childProperty);
+        }
+      } else {
+        maskBuilder.add(entry.getKey());
+      }
+    }
+    return maskBuilder.build();
+  }
+
+  public void setInterceptor(HttpResponseInterceptor interceptor) {
+    this.interceptor = interceptor;
+  }
+
+  public <T> T sendRequest(
+      String method, GenericUrl url,
+      @Nullable Object content, Class<T> clazz) throws FirebaseAuthException {
+
+    checkArgument(!Strings.isNullOrEmpty(method), "method must not be null or empty");
+    checkNotNull(url, "url must not be null");
+    checkNotNull(clazz, "response class must not be null");
+    HttpResponse response = null;
+    try {
+      HttpContent httpContent = content != null ? new JsonHttpContent(jsonFactory, content) : null;
+      HttpRequest request =
+          requestFactory.buildRequest(method.equals("PATCH") ? "POST" : method, url, httpContent);
+      request.setParser(new JsonObjectParser(jsonFactory));
+      request.getHeaders().set(CLIENT_VERSION_HEADER, CLIENT_VERSION);
+      if (method.equals("PATCH")) {
+        request.getHeaders().set("X-HTTP-Method-Override", "PATCH");
+      }
+      request.setResponseInterceptor(interceptor);
+      response = request.execute();
+      return response.parseAs(clazz);
+    } catch (HttpResponseException e) {
+      // Server responded with an HTTP error
+      handleHttpError(e);
+      return null;
+    } catch (IOException e) {
+      // All other IO errors (Connection refused, reset, parse error etc.)
+      throw new FirebaseAuthException(
+          INTERNAL_ERROR, "Error while calling the Firebase Auth backend service", e);
+    } finally {
+      if (response != null) {
+        try {
+          response.disconnect();
+        } catch (IOException ignored) {
+          // Ignored
+        }
+      }
+    }
+  }
+
+  private void handleHttpError(HttpResponseException e) throws FirebaseAuthException {
+    try {
+      HttpErrorResponse response = jsonFactory.fromString(e.getContent(), HttpErrorResponse.class);
+      String code = ERROR_CODES.get(response.getErrorCode());
+      if (code != null) {
+        throw new FirebaseAuthException(code, "Firebase Auth service responded with an error", e);
+      }
+    } catch (IOException ignored) {
+      // Ignored
+    }
+    String msg = String.format(
+        "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent());
+    throw new FirebaseAuthException(INTERNAL_ERROR, msg, e);
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java
index c81205a..2f25ae6 100644
--- a/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java
+++ b/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java
@@ -16,12 +16,7 @@
 
 package com.google.firebase.auth.internal;
 
-import com.google.api.client.util.Key;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableList;
-import com.google.firebase.auth.ListProviderConfigsPage;
 import com.google.firebase.auth.ProviderConfig;
-import com.google.firebase.auth.Tenant;
 import java.util.List;
 
 /**
diff --git a/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java
index b612793..f108692 100644
--- a/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java
+++ b/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java
@@ -19,7 +19,7 @@ package com.google.firebase.auth.internal;
 import com.google.api.client.util.Key;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
-import com.google.firebase.auth.Tenant;
+import com.google.firebase.auth.multitenancy.Tenant;
 import java.util.List;
 
 /**
diff --git a/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java
new file mode 100644
index 0000000..1278e63
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2020 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.multitenancy;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpResponseInterceptor;
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.json.JsonFactory;
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.ImplFirebaseTrampolines;
+import com.google.firebase.auth.FirebaseAuthException;
+import com.google.firebase.auth.internal.AuthHttpClient;
+import com.google.firebase.auth.internal.ListTenantsResponse;
+import com.google.firebase.internal.ApiClientUtils;
+import java.util.Map;
+
+final class FirebaseTenantClient {
+
+  static final int MAX_LIST_TENANTS_RESULTS = 100;
+
+  private static final String ID_TOOLKIT_URL =
+      "https://identitytoolkit.googleapis.com/%s/projects/%s";
+
+  private final String tenantMgtBaseUrl;
+  private final AuthHttpClient httpClient;
+
+  FirebaseTenantClient(FirebaseApp app) {
+    checkNotNull(app, "FirebaseApp must not be null");
+    String projectId = ImplFirebaseTrampolines.getProjectId(app);
+    checkArgument(!Strings.isNullOrEmpty(projectId),
+        "Project ID is required to access the auth service. Use a service account credential or "
+            + "set the project ID explicitly via FirebaseOptions. Alternatively you can also "
+            + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.");
+    this.tenantMgtBaseUrl = String.format(ID_TOOLKIT_URL, "v2", projectId);
+    JsonFactory jsonFactory = app.getOptions().getJsonFactory();
+    HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app);
+    this.httpClient = new AuthHttpClient(jsonFactory, requestFactory);
+  }
+
+  void setInterceptor(HttpResponseInterceptor interceptor) {
+    httpClient.setInterceptor(interceptor);
+  }
+
+  Tenant getTenant(String tenantId) throws FirebaseAuthException {
+    GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId));
+    return httpClient.sendRequest("GET", url, null, Tenant.class);
+  }
+
+  Tenant createTenant(Tenant.CreateRequest request) throws FirebaseAuthException {
+    GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants");
+    return httpClient.sendRequest("POST", url, request.getProperties(), Tenant.class);
+  }
+
+  Tenant updateTenant(Tenant.UpdateRequest request) throws FirebaseAuthException {
+    Map<String, Object> properties = request.getProperties();
+    GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(request.getTenantId()));
+    url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties)));
+    return httpClient.sendRequest("PATCH", url, properties, Tenant.class);
+  }
+
+  void deleteTenant(String tenantId) throws FirebaseAuthException {
+    GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId));
+    httpClient.sendRequest("DELETE", url, null, GenericJson.class);
+  }
+
+  ListTenantsResponse listTenants(int maxResults, String pageToken)
+      throws FirebaseAuthException {
+    ImmutableMap.Builder<String, Object> builder =
+        ImmutableMap.<String, Object>builder().put("pageSize", maxResults);
+    if (pageToken != null) {
+      checkArgument(!pageToken.equals(
+          ListTenantsPage.END_OF_LIST), "Invalid end of list page token.");
+      builder.put("pageToken", pageToken);
+    }
+
+    GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants");
+    url.putAll(builder.build());
+    ListTenantsResponse response = httpClient.sendRequest(
+        "GET", url, null, ListTenantsResponse.class);
+    if (response == null) {
+      throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve tenants.");
+    }
+    return response;
+  }
+
+  private static String getTenantUrlSuffix(String tenantId) {
+    checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty.");
+    return "/tenants/" + tenantId;
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/ListTenantsPage.java b/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java
similarity index 91%
rename from src/main/java/com/google/firebase/auth/ListTenantsPage.java
rename to src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java
index d637174..c1f393d 100644
--- a/src/main/java/com/google/firebase/auth/ListTenantsPage.java
+++ b/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java
@@ -14,15 +14,14 @@
  * limitations under the License.
  */
 
-package com.google.firebase.auth;
+package com.google.firebase.auth.multitenancy;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 
-import com.google.api.client.json.JsonFactory;
 import com.google.api.gax.paging.Page;
 import com.google.common.collect.ImmutableList;
-import com.google.firebase.auth.internal.DownloadAccountResponse;
+import com.google.firebase.auth.FirebaseAuthException;
 import com.google.firebase.auth.internal.ListTenantsResponse;
 import com.google.firebase.internal.NonNull;
 import com.google.firebase.internal.Nullable;
@@ -198,16 +197,16 @@ public class ListTenantsPage implements Page<Tenant> {
 
   static class DefaultTenantSource implements TenantSource {
 
-    private final FirebaseUserManager userManager;
+    private final FirebaseTenantClient tenantClient;
 
-    DefaultTenantSource(FirebaseUserManager userManager) {
-      this.userManager = checkNotNull(userManager, "User manager must not be null.");
+    DefaultTenantSource(FirebaseTenantClient tenantClient) {
+      this.tenantClient = checkNotNull(tenantClient, "Tenant client must not be null.");
     }
 
     @Override
     public ListTenantsResponse fetch(int maxResults, String pageToken)
         throws FirebaseAuthException {
-      return userManager.listTenants(maxResults, pageToken);
+      return tenantClient.listTenants(maxResults, pageToken);
     }
   }
 
@@ -224,13 +223,13 @@ public class ListTenantsPage implements Page<Tenant> {
     private final String pageToken;
 
     PageFactory(@NonNull TenantSource source) {
-      this(source, FirebaseUserManager.MAX_LIST_TENANTS_RESULTS, null);
+      this(source, FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS, null);
     }
 
     PageFactory(@NonNull TenantSource source, int maxResults, @Nullable String pageToken) {
-      checkArgument(maxResults > 0 && maxResults <= FirebaseUserManager.MAX_LIST_TENANTS_RESULTS,
+      checkArgument(maxResults > 0 && maxResults <= FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS,
           "maxResults must be a positive integer that does not exceed %s",
-          FirebaseUserManager.MAX_LIST_TENANTS_RESULTS);
+          FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS);
       checkArgument(!END_OF_LIST.equals(pageToken), "Invalid end of list page token.");
       this.source = checkNotNull(source, "Source must not be null.");
       this.maxResults = maxResults;
diff --git a/src/main/java/com/google/firebase/auth/Tenant.java b/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java
similarity index 99%
rename from src/main/java/com/google/firebase/auth/Tenant.java
rename to src/main/java/com/google/firebase/auth/multitenancy/Tenant.java
index 61f5257..57d215e 100644
--- a/src/main/java/com/google/firebase/auth/Tenant.java
+++ b/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.google.firebase.auth;
+package com.google.firebase.auth.multitenancy;
 
 import static com.google.common.base.Preconditions.checkArgument;
 
diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java
new file mode 100644
index 0000000..5404374
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.multitenancy;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.Strings;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.auth.AbstractFirebaseAuth;
+
+/**
+ * The tenant-aware Firebase client.
+ *
+ * <p>This can be used to perform a variety of authentication-related operations, scoped to a
+ * particular tenant.
+ */
+public final class TenantAwareFirebaseAuth extends AbstractFirebaseAuth {
+
+  private final String tenantId;
+
+  TenantAwareFirebaseAuth(final FirebaseApp firebaseApp, final String tenantId) {
+    super(builderFromAppAndTenantId(firebaseApp, tenantId));
+    checkArgument(!Strings.isNullOrEmpty(tenantId));
+    this.tenantId = tenantId;
+  }
+
+  /** Returns the client's tenant ID. */
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  @Override
+  protected void doDestroy() {
+    // Nothing extra needs to be destroyed.
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/TenantManager.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java
similarity index 86%
rename from src/main/java/com/google/firebase/auth/TenantManager.java
rename to src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java
index 4b3bbb9..11f26b0 100644
--- a/src/main/java/com/google/firebase/auth/TenantManager.java
+++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java
@@ -14,27 +14,28 @@
  * limitations under the License.
  */
 
-package com.google.firebase.auth;
+package com.google.firebase.auth.multitenancy;
 
 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.api.client.http.HttpResponseInterceptor;
 import com.google.api.core.ApiFuture;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.firebase.FirebaseApp;
-import com.google.firebase.auth.ListTenantsPage.DefaultTenantSource;
-import com.google.firebase.auth.ListTenantsPage.PageFactory;
-import com.google.firebase.auth.ListTenantsPage.TenantSource;
-import com.google.firebase.auth.Tenant.CreateRequest;
-import com.google.firebase.auth.Tenant.UpdateRequest;
+import com.google.firebase.auth.FirebaseAuth;
+import com.google.firebase.auth.FirebaseAuthException;
+import com.google.firebase.auth.multitenancy.ListTenantsPage.DefaultTenantSource;
+import com.google.firebase.auth.multitenancy.ListTenantsPage.PageFactory;
+import com.google.firebase.auth.multitenancy.ListTenantsPage.TenantSource;
+import com.google.firebase.auth.multitenancy.Tenant.CreateRequest;
+import com.google.firebase.auth.multitenancy.Tenant.UpdateRequest;
 import com.google.firebase.internal.CallableOperation;
 import com.google.firebase.internal.NonNull;
 import com.google.firebase.internal.Nullable;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * This class can be used to perform a variety of tenant-related operations, including creating,
@@ -42,17 +43,25 @@ import java.util.concurrent.atomic.AtomicBoolean;
  */
 public final class TenantManager {
 
-  private final Object lock = new Object();
-  private final AtomicBoolean destroyed = new AtomicBoolean(false);
-
   private final FirebaseApp firebaseApp;
-  private final FirebaseUserManager userManager;
+  private final FirebaseTenantClient tenantClient;
   private final Map<String, TenantAwareFirebaseAuth> tenantAwareAuths;
 
-  TenantManager(FirebaseApp firebaseApp, FirebaseUserManager userManager) {
+  /**
+   * Creates a new {@link TenantManager} instance. For internal use only. Use
+   * {@link FirebaseAuth#getTenantManager()} to obtain an instance for regular use.
+   *
+   * @hide
+   */
+  public TenantManager(FirebaseApp firebaseApp) {
     this.firebaseApp = firebaseApp;
-    this.userManager = userManager;
-    tenantAwareAuths = new HashMap<String, TenantAwareFirebaseAuth>();
+    this.tenantClient = new FirebaseTenantClient(firebaseApp);
+    this.tenantAwareAuths = new HashMap<>();
+  }
+
+  @VisibleForTesting
+  void setInterceptor(HttpResponseInterceptor interceptor) {
+    this.tenantClient.setInterceptor(interceptor);
   }
 
   /**
@@ -89,12 +98,11 @@ public final class TenantManager {
   }
 
   private CallableOperation<Tenant, FirebaseAuthException> getTenantOp(final String tenantId) {
-    checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty.");
     return new CallableOperation<Tenant, FirebaseAuthException>() {
       @Override
       protected Tenant execute() throws FirebaseAuthException {
-        return userManager.getTenant(tenantId);
+        return tenantClient.getTenant(tenantId);
       }
     };
   }
@@ -109,7 +117,7 @@ public final class TenantManager {
    * @throws FirebaseAuthException If an error occurs while retrieving tenant data.
    */
   public ListTenantsPage listTenants(@Nullable String pageToken) throws FirebaseAuthException {
-    return listTenants(pageToken, FirebaseUserManager.MAX_LIST_TENANTS_RESULTS);
+    return listTenants(pageToken, FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS);
   }
 
   /**
@@ -137,7 +145,7 @@ public final class TenantManager {
    * @throws IllegalArgumentException If the specified page token is empty.
    */
   public ApiFuture<ListTenantsPage> listTenantsAsync(@Nullable String pageToken) {
-    return listTenantsAsync(pageToken, FirebaseUserManager.MAX_LIST_TENANTS_RESULTS);
+    return listTenantsAsync(pageToken, FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS);
   }
 
   /**
@@ -157,8 +165,7 @@ public final class TenantManager {
 
   private CallableOperation<ListTenantsPage, FirebaseAuthException> listTenantsOp(
       @Nullable final String pageToken, final int maxResults) {
-    checkNotDestroyed();
-    final TenantSource tenantSource = new DefaultTenantSource(userManager);
+    final TenantSource tenantSource = new DefaultTenantSource(tenantClient);
     final PageFactory factory = new PageFactory(tenantSource, maxResults, pageToken);
     return new CallableOperation<ListTenantsPage, FirebaseAuthException>() {
       @Override
@@ -195,12 +202,11 @@ public final class TenantManager {
 
   private CallableOperation<Tenant, FirebaseAuthException> createTenantOp(
       final CreateRequest request) {
-    checkNotDestroyed();
     checkNotNull(request, "Create request must not be null.");
     return new CallableOperation<Tenant, FirebaseAuthException>() {
       @Override
       protected Tenant execute() throws FirebaseAuthException {
-        return userManager.createTenant(request);
+        return tenantClient.createTenant(request);
       }
     };
   }
@@ -233,14 +239,13 @@ public final class TenantManager {
 
   private CallableOperation<Tenant, FirebaseAuthException> updateTenantOp(
       final UpdateRequest request) {
-    checkNotDestroyed();
     checkNotNull(request, "Update request must not be null.");
     checkArgument(!request.getProperties().isEmpty(),
         "Tenant update must have at least one property set.");
     return new CallableOperation<Tenant, FirebaseAuthException>() {
       @Override
       protected Tenant execute() throws FirebaseAuthException {
-        return userManager.updateTenant(request);
+        return tenantClient.updateTenant(request);
       }
     };
   }
@@ -270,27 +275,13 @@ public final class TenantManager {
   }
 
   private CallableOperation<Void, FirebaseAuthException> deleteTenantOp(final String tenantId) {
-    checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty.");
     return new CallableOperation<Void, FirebaseAuthException>() {
       @Override
       protected Void execute() throws FirebaseAuthException {
-        userManager.deleteTenant(tenantId);
+        tenantClient.deleteTenant(tenantId);
         return null;
       }
     };
   }
-
-  void checkNotDestroyed() {
-    synchronized (lock) {
-      checkState(
-          !destroyed.get(),
-          "TenantManager instance is no longer alive. This happens when "
-              + "the parent FirebaseApp instance has been deleted.");
-    }
-  }
-
-  protected void destroy() {
-    destroyed.set(true);
-  }
 }


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 f651e3d..d5bfbae 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
@@ -50,6 +50,7 @@ import com.google.firebase.auth.ProviderConfigTestUtils.TemporaryProviderConfig;
 import com.google.firebase.auth.UserTestUtils.RandomUser;
 import com.google.firebase.auth.UserTestUtils.TemporaryUser;
 import com.google.firebase.auth.hash.Scrypt;
+import com.google.firebase.auth.internal.AuthHttpClient;
 import com.google.firebase.internal.Nullable;
 import com.google.firebase.testing.IntegrationTestUtils;
 import java.io.IOException;
@@ -66,7 +67,6 @@ import java.util.concurrent.atomic.AtomicReference;
 
 import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExternalResource;
 
 public class FirebaseAuthIT {
 
@@ -96,7 +96,7 @@ public class FirebaseAuthIT {
       fail("No error thrown for non existing uid");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
+      assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR,
           ((FirebaseAuthException) e.getCause()).getErrorCode());
     }
   }
@@ -108,7 +108,7 @@ public class FirebaseAuthIT {
       fail("No error thrown for non existing email");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
+      assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR,
           ((FirebaseAuthException) e.getCause()).getErrorCode());
     }
   }
@@ -120,7 +120,7 @@ public class FirebaseAuthIT {
       fail("No error thrown for non existing uid");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
+      assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR,
           ((FirebaseAuthException) e.getCause()).getErrorCode());
     }
   }
@@ -132,7 +132,7 @@ public class FirebaseAuthIT {
       fail("No error thrown for non existing uid");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
+      assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR,
           ((FirebaseAuthException) e.getCause()).getErrorCode());
     }
   }
@@ -1021,18 +1021,6 @@ public class FirebaseAuthIT {
     return false;
   }
 
-  private static void assertUserDoesNotExist(AbstractFirebaseAuth firebaseAuth, String uid)
-      throws Exception {
-    try {
-      firebaseAuth.getUserAsync(uid).get();
-      fail("No error thrown for getting a user which was expected to be absent.");
-    } catch (ExecutionException e) {
-      assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
-          ((FirebaseAuthException) e.getCause()).getErrorCode());
-    }
-  }
-
   static UserRecord newUserWithParams() throws Exception {
     return newUserWithParams(auth);
   }
diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java
index a7c1d4d..5dd1e9c 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java
@@ -30,7 +30,6 @@ import com.google.api.client.testing.http.MockLowLevelHttpResponse;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.firebase.auth.internal.FirebaseCustomAuthToken;
 import com.google.firebase.testing.ServiceAccount;
 import java.io.IOException;
 import java.util.concurrent.TimeUnit;
@@ -219,14 +218,14 @@ public class FirebaseTokenVerifierImplTest {
   }
 
   @Test
-  public void testVerifyTokenDifferentTenantIds() throws Exception {
+  public void testVerifyTokenDifferentTenantIds() {
     try {
       fullyPopulatedBuilder()
         .setTenantId("TENANT_1")
         .build()
         .verifyToken(createTokenWithTenantId("TENANT_2"));
     } catch (FirebaseAuthException e) {
-      assertEquals(FirebaseUserManager.TENANT_ID_MISMATCH_ERROR, e.getErrorCode());
+      assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode());
       assertEquals(
           "The tenant ID ('TENANT_2') of the token did not match the expected value ('TENANT_1')",
           e.getMessage());
@@ -234,14 +233,14 @@ public class FirebaseTokenVerifierImplTest {
   }
 
   @Test
-  public void testVerifyTokenMissingTenantId() throws Exception {
+  public void testVerifyTokenMissingTenantId() {
     try {
       fullyPopulatedBuilder()
         .setTenantId("TENANT_ID")
         .build()
         .verifyToken(tokenFactory.createToken());
     } catch (FirebaseAuthException e) {
-      assertEquals(FirebaseUserManager.TENANT_ID_MISMATCH_ERROR, e.getErrorCode());
+      assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode());
       assertEquals(
           "The tenant ID ('') of the token did not match the expected value ('TENANT_ID')",
           e.getMessage());
@@ -249,13 +248,13 @@ public class FirebaseTokenVerifierImplTest {
   }
 
   @Test
-  public void testVerifyTokenUnexpectedTenantId() throws Exception {
+  public void testVerifyTokenUnexpectedTenantId() {
     try {
       fullyPopulatedBuilder()
         .build()
         .verifyToken(createTokenWithTenantId("TENANT_ID"));
     } catch (FirebaseAuthException e) {
-      assertEquals(FirebaseUserManager.TENANT_ID_MISMATCH_ERROR, e.getErrorCode());
+      assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode());
       assertEquals(
           "The tenant ID ('TENANT_ID') of the token did not match the expected value ('')",
           e.getMessage());
diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
index 74b36fa..2702a1c 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
@@ -44,6 +44,9 @@ import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.TestOnlyImplFirebaseTrampolines;
 import com.google.firebase.auth.FirebaseUserManager.EmailLinkType;
+import com.google.firebase.auth.internal.AuthHttpClient;
+import com.google.firebase.auth.multitenancy.TenantAwareFirebaseAuth;
+import com.google.firebase.auth.multitenancy.TenantManager;
 import com.google.firebase.internal.SdkUtils;
 import com.google.firebase.testing.MultiRequestMockHttpTransport;
 import com.google.firebase.testing.TestResponseInterceptor;
@@ -66,7 +69,9 @@ import org.junit.Test;
 public class FirebaseUserManagerTest {
 
   private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory();
+
   private static final String TEST_TOKEN = "token";
+
   private static final GoogleCredentials credentials = new MockGoogleCredentials(TEST_TOKEN);
 
   private static final ActionCodeSettings ACTION_CODE_SETTINGS = ActionCodeSettings.builder()
@@ -78,11 +83,13 @@ public class FirebaseUserManagerTest {
           .setAndroidInstallApp(true)
           .setAndroidMinimumVersion("6")
           .build();
+
   private static final Map<String, Object> ACTION_CODE_SETTINGS_MAP =
           ACTION_CODE_SETTINGS.getProperties();
 
   private static final String PROJECT_BASE_URL =
       "https://identitytoolkit.googleapis.com/v2/projects/test-project-id";
+
   private static final String TENANTS_BASE_URL = PROJECT_BASE_URL + "/tenants";
 
   @After
@@ -126,7 +133,7 @@ public class FirebaseUserManagerTest {
     } catch (ExecutionException e) {
       assertThat(e.getCause(), instanceOf(FirebaseAuthException.class));
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode());
+      assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode());
     }
   }
 
@@ -149,7 +156,7 @@ public class FirebaseUserManagerTest {
     } catch (ExecutionException e) {
       assertThat(e.getCause(), instanceOf(FirebaseAuthException.class));
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode());
+      assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode());
     }
   }
 
@@ -172,7 +179,7 @@ public class FirebaseUserManagerTest {
     } catch (ExecutionException e) {
       assertThat(e.getCause(), instanceOf(FirebaseAuthException.class));
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode());
+      assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode());
     }
   }
 
@@ -648,228 +655,6 @@ public class FirebaseUserManagerTest {
     }
   }
 
-  @Test
-  public void testGetTenant() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("tenant.json"));
-
-    Tenant tenant = FirebaseAuth.getInstance().getTenantManager().getTenant("TENANT_1");
-
-    checkTenant(tenant, "TENANT_1");
-    checkRequestHeaders(interceptor);
-    checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_1");
-  }
-
-  @Test
-  public void testGetTenantWithNotFoundError() throws Exception {
-    TestResponseInterceptor interceptor =
-        initializeAppForUserManagementWithStatusCode(404,
-            "{\"error\": {\"message\": \"TENANT_NOT_FOUND\"}}");
-    try {
-      FirebaseAuth.getInstance().getTenantManager().getTenant("UNKNOWN");
-      fail("No error thrown for invalid response");
-    } catch (FirebaseAuthException e) {
-      assertEquals(FirebaseUserManager.TENANT_NOT_FOUND_ERROR, e.getErrorCode());
-    }
-    checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/UNKNOWN");
-  }
-
-  @Test
-  public void testListTenants() throws Exception {
-    final TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("listTenants.json"));
-
-    ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants(null, 999);
-
-    ImmutableList<Tenant> tenants = ImmutableList.copyOf(page.getValues());
-    assertEquals(2, tenants.size());
-    checkTenant(tenants.get(0), "TENANT_1");
-    checkTenant(tenants.get(1), "TENANT_2");
-    assertEquals("", page.getNextPageToken());
-    checkRequestHeaders(interceptor);
-    checkUrl(interceptor, "GET", TENANTS_BASE_URL);
-    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
-    assertEquals(999, url.getFirst("pageSize"));
-    assertNull(url.getFirst("pageToken"));
-  }
-
-  @Test
-  public void testListTenantsWithPageToken() throws Exception {
-    final TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("listTenants.json"));
-
-    ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants("token", 999);
-
-    ImmutableList<Tenant> tenants = ImmutableList.copyOf(page.getValues());
-    assertEquals(2, tenants.size());
-    checkTenant(tenants.get(0), "TENANT_1");
-    checkTenant(tenants.get(1), "TENANT_2");
-    assertEquals("", page.getNextPageToken());
-    checkRequestHeaders(interceptor);
-    checkUrl(interceptor, "GET", TENANTS_BASE_URL);
-    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
-    assertEquals(999, url.getFirst("pageSize"));
-    assertEquals("token", url.getFirst("pageToken"));
-  }
-
-  @Test
-  public void testListZeroTenants() throws Exception {
-    final TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
-
-    ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants(null);
-
-    assertTrue(Iterables.isEmpty(page.getValues()));
-    assertEquals("", page.getNextPageToken());
-    checkRequestHeaders(interceptor);
-  }
-
-  @Test
-  public void testCreateTenant() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("tenant.json"));
-    Tenant.CreateRequest request = new Tenant.CreateRequest()
-        .setDisplayName("DISPLAY_NAME")
-        .setPasswordSignInAllowed(true)
-        .setEmailLinkSignInEnabled(false);
-
-    Tenant tenant = FirebaseAuth.getInstance().getTenantManager().createTenant(request);
-
-    checkTenant(tenant, "TENANT_1");
-    checkRequestHeaders(interceptor);
-    checkUrl(interceptor, "POST", TENANTS_BASE_URL);
-    GenericJson parsed = parseRequestContent(interceptor);
-    assertEquals("DISPLAY_NAME", parsed.get("displayName"));
-    assertEquals(true, parsed.get("allowPasswordSignup"));
-    assertEquals(false, parsed.get("enableEmailLinkSignin"));
-  }
-
-  @Test
-  public void testCreateTenantMinimal() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("tenant.json"));
-    Tenant.CreateRequest request = new Tenant.CreateRequest();
-
-    Tenant tenant = FirebaseAuth.getInstance().getTenantManager().createTenant(request);
-
-    checkTenant(tenant, "TENANT_1");
-    checkRequestHeaders(interceptor);
-    checkUrl(interceptor, "POST", TENANTS_BASE_URL);
-    GenericJson parsed = parseRequestContent(interceptor);
-    assertNull(parsed.get("displayName"));
-    assertNull(parsed.get("allowPasswordSignup"));
-    assertNull(parsed.get("enableEmailLinkSignin"));
-  }
-
-  @Test
-  public void testCreateTenantError() throws Exception {
-    TestResponseInterceptor interceptor =
-        initializeAppForUserManagementWithStatusCode(404,
-            "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
-    try {
-      FirebaseAuth.getInstance().getTenantManager().createTenant(new Tenant.CreateRequest());
-      fail("No error thrown for invalid response");
-    } catch (FirebaseAuthException e) {
-      assertEquals(FirebaseUserManager.INTERNAL_ERROR, e.getErrorCode());
-    }
-    checkUrl(interceptor, "POST", TENANTS_BASE_URL);
-  }
-
-  @Test
-  public void testUpdateTenant() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("tenant.json"));
-    Tenant.UpdateRequest request = new Tenant.UpdateRequest("TENANT_1")
-        .setDisplayName("DISPLAY_NAME")
-        .setPasswordSignInAllowed(true)
-        .setEmailLinkSignInEnabled(false);
-
-    Tenant tenant = FirebaseAuth.getInstance().getTenantManager().updateTenant(request);
-
-    checkTenant(tenant, "TENANT_1");
-    checkRequestHeaders(interceptor);
-    checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1");
-    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
-    assertEquals("allowPasswordSignup,displayName,enableEmailLinkSignin",
-        url.getFirst("updateMask"));
-    GenericJson parsed = parseRequestContent(interceptor);
-    assertEquals("DISPLAY_NAME", parsed.get("displayName"));
-    assertEquals(true, parsed.get("allowPasswordSignup"));
-    assertEquals(false, parsed.get("enableEmailLinkSignin"));
-  }
-
-  @Test
-  public void testUpdateTenantMinimal() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("tenant.json"));
-    Tenant.UpdateRequest request =
-        new Tenant.UpdateRequest("TENANT_1").setDisplayName("DISPLAY_NAME");
-
-    Tenant tenant = FirebaseAuth.getInstance().getTenantManager().updateTenant(request);
-
-    checkTenant(tenant, "TENANT_1");
-    checkRequestHeaders(interceptor);
-    checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1");
-    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
-    assertEquals("displayName", url.getFirst("updateMask"));
-    GenericJson parsed = parseRequestContent(interceptor);
-    assertEquals("DISPLAY_NAME", parsed.get("displayName"));
-    assertNull(parsed.get("allowPasswordSignup"));
-    assertNull(parsed.get("enableEmailLinkSignin"));
-  }
-
-  @Test
-  public void testUpdateTenantNoValues() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("tenant.json"));
-    TenantManager tenantManager = FirebaseAuth.getInstance().getTenantManager();
-    try {
-      tenantManager.updateTenant(new Tenant.UpdateRequest("TENANT_1"));
-      fail("No error thrown for empty tenant update");
-    } catch (IllegalArgumentException e) {
-      // expected
-    }
-  }
-
-  @Test
-  public void testUpdateTenantError() throws Exception {
-    TestResponseInterceptor interceptor =
-        initializeAppForUserManagementWithStatusCode(404,
-            "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
-    Tenant.UpdateRequest request =
-        new Tenant.UpdateRequest("TENANT_1").setDisplayName("DISPLAY_NAME");
-    try {
-      FirebaseAuth.getInstance().getTenantManager().updateTenant(request);
-      fail("No error thrown for invalid response");
-    } catch (FirebaseAuthException e) {
-      assertEquals(FirebaseUserManager.INTERNAL_ERROR, e.getErrorCode());
-    }
-    checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1");
-  }
-
-  @Test
-  public void testDeleteTenant() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
-
-    FirebaseAuth.getInstance().getTenantManager().deleteTenant("TENANT_1");
-
-    checkRequestHeaders(interceptor);
-    checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/TENANT_1");
-  }
-
-  @Test
-  public void testDeleteTenantWithNotFoundError() throws Exception {
-    TestResponseInterceptor interceptor =
-        initializeAppForUserManagementWithStatusCode(404,
-            "{\"error\": {\"message\": \"TENANT_NOT_FOUND\"}}");
-    try {
-      FirebaseAuth.getInstance().getTenantManager().deleteTenant("UNKNOWN");
-      fail("No error thrown for invalid response");
-    } catch (FirebaseAuthException e) {
-      assertEquals(FirebaseUserManager.TENANT_NOT_FOUND_ERROR, e.getErrorCode());
-    }
-    checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/UNKNOWN");
-  }
-
   @Test
   public void testCreateSessionCookie() throws Exception {
     TestResponseInterceptor interceptor = initializeAppForUserManagement(
@@ -1004,7 +789,7 @@ public class FirebaseUserManagerTest {
           String msg = String.format("Unexpected HTTP response with status: %d; body: {}", code);
           assertEquals(msg, authException.getMessage());
           assertThat(authException.getCause(), instanceOf(HttpResponseException.class));
-          assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode());
+          assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode());
         }
       }
     }
@@ -1019,9 +804,9 @@ public class FirebaseUserManagerTest {
       }  catch (ExecutionException e) {
         assertThat(e.getCause().toString(), e.getCause(), instanceOf(FirebaseAuthException.class));
         FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-        assertEquals("User management service responded with an error", authException.getMessage());
+        assertEquals("Firebase Auth service responded with an error", authException.getMessage());
         assertThat(authException.getCause(), instanceOf(HttpResponseException.class));
-        assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode());
+        assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode());
       }
     }
   }
@@ -1036,7 +821,7 @@ public class FirebaseUserManagerTest {
       assertThat(e.getCause(), instanceOf(FirebaseAuthException.class));
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
       assertThat(authException.getCause(), instanceOf(IOException.class));
-      assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode());
+      assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode());
     }
   }
 
@@ -1055,7 +840,7 @@ public class FirebaseUserManagerTest {
       assertThat(authException.getCause(), instanceOf(HttpResponseException.class));
       assertEquals("Unexpected HTTP response with status: 500; body: {\"not\" json}",
           authException.getMessage());
-      assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode());
+      assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode());
     }
   }
 
@@ -1672,15 +1457,14 @@ public class FirebaseUserManagerTest {
       FirebaseAuth.getInstance().createOidcProviderConfig(createRequest);
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
-      assertEquals(FirebaseUserManager.INTERNAL_ERROR, e.getErrorCode());
+      assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode());
     }
     checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs");
   }
 
   @Test
   public void testCreateOidcProviderMissingId() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("oidc.json"));
+    initializeAppForUserManagement(TestUtils.loadResource("oidc.json"));
     OidcProviderConfig.CreateRequest createRequest =
         new OidcProviderConfig.CreateRequest()
             .setDisplayName("DISPLAY_NAME")
@@ -1762,8 +1546,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testUpdateOidcProviderConfigNoValues() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("oidc.json"));
+    initializeAppForUserManagement(TestUtils.loadResource("oidc.json"));
     try {
       FirebaseAuth.getInstance().updateOidcProviderConfig(
           new OidcProviderConfig.UpdateRequest("oidc.provider-id"));
@@ -1774,7 +1557,7 @@ public class FirebaseUserManagerTest {
   }
 
   @Test
-  public void testUpdateOidcProviderConfigError() throws Exception {
+  public void testUpdateOidcProviderConfigError() {
     TestResponseInterceptor interceptor =
         initializeAppForUserManagementWithStatusCode(404,
             "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
@@ -1784,7 +1567,7 @@ public class FirebaseUserManagerTest {
       FirebaseAuth.getInstance().updateOidcProviderConfig(request);
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
-      assertEquals(FirebaseUserManager.INTERNAL_ERROR, e.getErrorCode());
+      assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode());
     }
     checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id");
   }
@@ -1833,8 +1616,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testGetOidcProviderConfigMissingId() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("oidc.json"));
+    initializeAppForUserManagement(TestUtils.loadResource("oidc.json"));
 
     try {
       FirebaseAuth.getInstance().getOidcProviderConfig(null);
@@ -1846,8 +1628,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testGetOidcProviderConfigInvalidId() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("oidc.json"));
+    initializeAppForUserManagement(TestUtils.loadResource("oidc.json"));
 
     try {
       FirebaseAuth.getInstance().getOidcProviderConfig("saml.invalid-oidc-provider-id");
@@ -1866,7 +1647,7 @@ public class FirebaseUserManagerTest {
       FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id");
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
-      assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode());
+      assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode());
     }
     checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id");
   }
@@ -1968,7 +1749,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testDeleteOidcProviderMissingId() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
+    initializeAppForUserManagement("{}");
 
     try {
       FirebaseAuth.getInstance().deleteOidcProviderConfig(null);
@@ -1980,7 +1761,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testDeleteOidcProviderInvalidId() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
+    initializeAppForUserManagement("{}");
 
     try {
       FirebaseAuth.getInstance().deleteOidcProviderConfig("saml.invalid-oidc-provider-id");
@@ -1991,7 +1772,7 @@ public class FirebaseUserManagerTest {
   }
 
   @Test
-  public void testDeleteOidcProviderConfigWithNotFoundError() throws Exception {
+  public void testDeleteOidcProviderConfigWithNotFoundError() {
     TestResponseInterceptor interceptor =
         initializeAppForUserManagementWithStatusCode(404,
             "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}");
@@ -1999,7 +1780,7 @@ public class FirebaseUserManagerTest {
       FirebaseAuth.getInstance().deleteOidcProviderConfig("oidc.UNKNOWN");
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
-      assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode());
+      assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode());
     }
     checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.UNKNOWN");
   }
@@ -2107,7 +1888,7 @@ public class FirebaseUserManagerTest {
   }
 
   @Test
-  public void testCreateSamlProviderError() throws Exception {
+  public void testCreateSamlProviderError() {
     TestResponseInterceptor interceptor =
         initializeAppForUserManagementWithStatusCode(404,
             "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
@@ -2117,15 +1898,14 @@ public class FirebaseUserManagerTest {
       FirebaseAuth.getInstance().createSamlProviderConfig(createRequest);
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
-      assertEquals(FirebaseUserManager.INTERNAL_ERROR, e.getErrorCode());
+      assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode());
     }
     checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs");
   }
 
   @Test
   public void testCreateSamlProviderMissingId() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("saml.json"));
+    initializeAppForUserManagement(TestUtils.loadResource("saml.json"));
     SamlProviderConfig.CreateRequest createRequest =
         new SamlProviderConfig.CreateRequest()
           .setDisplayName("DISPLAY_NAME")
@@ -2260,7 +2040,7 @@ public class FirebaseUserManagerTest {
       FirebaseAuth.getInstance().updateSamlProviderConfig(request);
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
-      assertEquals(FirebaseUserManager.INTERNAL_ERROR, e.getErrorCode());
+      assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode());
     }
     checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id");
   }
@@ -2314,8 +2094,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testGetSamlProviderConfigMissingId() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("saml.json"));
+    initializeAppForUserManagement(TestUtils.loadResource("saml.json"));
 
     try {
       FirebaseAuth.getInstance().getSamlProviderConfig(null);
@@ -2327,8 +2106,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testGetSamlProviderConfigInvalidId() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement(
-        TestUtils.loadResource("saml.json"));
+    initializeAppForUserManagement(TestUtils.loadResource("saml.json"));
 
     try {
       FirebaseAuth.getInstance().getSamlProviderConfig("oidc.invalid-saml-provider-id");
@@ -2339,7 +2117,7 @@ public class FirebaseUserManagerTest {
   }
 
   @Test
-  public void testGetSamlProviderConfigWithNotFoundError() throws Exception {
+  public void testGetSamlProviderConfigWithNotFoundError() {
     TestResponseInterceptor interceptor =
         initializeAppForUserManagementWithStatusCode(404,
             "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}");
@@ -2347,7 +2125,7 @@ public class FirebaseUserManagerTest {
       FirebaseAuth.getInstance().getSamlProviderConfig("saml.provider-id");
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
-      assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode());
+      assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode());
     }
     checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id");
   }
@@ -2450,7 +2228,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testDeleteSamlProviderMissingId() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
+    initializeAppForUserManagement("{}");
 
     try {
       FirebaseAuth.getInstance().deleteSamlProviderConfig(null);
@@ -2462,7 +2240,7 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testDeleteSamlProviderInvalidId() throws Exception {
-    TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
+    initializeAppForUserManagement("{}");
 
     try {
       FirebaseAuth.getInstance().deleteSamlProviderConfig("oidc.invalid-saml-provider-id");
@@ -2473,7 +2251,7 @@ public class FirebaseUserManagerTest {
   }
 
   @Test
-  public void testDeleteSamlProviderConfigWithNotFoundError() throws Exception {
+  public void testDeleteSamlProviderConfigWithNotFoundError() {
     TestResponseInterceptor interceptor =
         initializeAppForUserManagementWithStatusCode(404,
             "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}");
@@ -2481,7 +2259,7 @@ public class FirebaseUserManagerTest {
       FirebaseAuth.getInstance().deleteSamlProviderConfig("saml.UNKNOWN");
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
-      assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode());
+      assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode());
     }
     checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.UNKNOWN");
   }
@@ -2521,7 +2299,8 @@ public class FirebaseUserManagerTest {
     initializeAppWithResponses(responses);
     TestResponseInterceptor interceptor = new TestResponseInterceptor();
     TenantManager tenantManager =  FirebaseAuth.getInstance().getTenantManager();
-    tenantManager.getAuthForTenant(tenantId).getUserManager().setInterceptor(interceptor);
+    AbstractFirebaseAuth auth = tenantManager.getAuthForTenant(tenantId);
+    auth.getUserManager().setInterceptor(interceptor);
     return interceptor;
   }
 
@@ -2610,13 +2389,6 @@ public class FirebaseUserManagerTest {
     assertEquals("gold", claims.get("package"));
   }
 
-  private static void checkTenant(Tenant tenant, String tenantId) {
-    assertEquals(tenantId, tenant.getTenantId());
-    assertEquals("DISPLAY_NAME", tenant.getDisplayName());
-    assertTrue(tenant.isPasswordSignInAllowed());
-    assertFalse(tenant.isEmailLinkSignInEnabled());
-  }
-
   private static void checkOidcProviderConfig(OidcProviderConfig config, String providerId) {
     assertEquals(providerId, config.getProviderId());
     assertEquals("DISPLAY_NAME", config.getDisplayName());
diff --git a/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java
index 262c48f..c01ac65 100644
--- a/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java
+++ b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java
@@ -21,33 +21,34 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import com.google.firebase.auth.internal.AuthHttpClient;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 import org.junit.rules.ExternalResource;
 
-class ProviderConfigTestUtils {
+public class ProviderConfigTestUtils {
 
-  static void assertOidcProviderConfigDoesNotExist(
+  public static void assertOidcProviderConfigDoesNotExist(
       AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception {
     try {
       firebaseAuth.getOidcProviderConfigAsync(providerId).get();
       fail("No error thrown for getting a deleted OIDC provider config.");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR,
+      assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR,
           ((FirebaseAuthException) e.getCause()).getErrorCode());
     }
   }
 
-  static void assertSamlProviderConfigDoesNotExist(
+  public static void assertSamlProviderConfigDoesNotExist(
       AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception {
     try {
       firebaseAuth.getSamlProviderConfigAsync(providerId).get();
       fail("No error thrown for getting a deleted SAML provider config.");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR,
+      assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR,
           ((FirebaseAuthException) e.getCause()).getErrorCode());
     }
   }
@@ -55,24 +56,25 @@ class ProviderConfigTestUtils {
   /**
    * Creates temporary provider configs for testing, and deletes them at the end of each test case.
    */
-  static final class TemporaryProviderConfig extends ExternalResource {
+  public static final class TemporaryProviderConfig extends ExternalResource {
 
     private final AbstractFirebaseAuth auth;
     private final List<String> oidcIds = new ArrayList<>();
     private final List<String> samlIds = new ArrayList<>();
 
-    TemporaryProviderConfig(AbstractFirebaseAuth auth) {
+    public TemporaryProviderConfig(AbstractFirebaseAuth auth) {
       this.auth = auth;
     }
 
-    synchronized OidcProviderConfig createOidcProviderConfig(
+    public synchronized OidcProviderConfig createOidcProviderConfig(
         OidcProviderConfig.CreateRequest request) throws FirebaseAuthException {
       OidcProviderConfig config = auth.createOidcProviderConfig(request);
       oidcIds.add(config.getProviderId());
       return config;
     }
 
-    synchronized void deleteOidcProviderConfig(String providerId) throws FirebaseAuthException {
+    public synchronized void deleteOidcProviderConfig(
+        String providerId) throws FirebaseAuthException {
       checkArgument(oidcIds.contains(providerId),
           "Provider ID is not currently associated with a temporary OIDC provider config: "
           + providerId);
@@ -80,14 +82,15 @@ class ProviderConfigTestUtils {
       oidcIds.remove(providerId);
     }
 
-    synchronized SamlProviderConfig createSamlProviderConfig(
+    public synchronized SamlProviderConfig createSamlProviderConfig(
         SamlProviderConfig.CreateRequest request) throws FirebaseAuthException {
       SamlProviderConfig config = auth.createSamlProviderConfig(request);
       samlIds.add(config.getProviderId());
       return config;
     }
 
-    synchronized void deleteSamlProviderConfig(String providerId) throws FirebaseAuthException {
+    public synchronized void deleteSamlProviderConfig(
+        String providerId) throws FirebaseAuthException {
       checkArgument(samlIds.contains(providerId),
           "Provider ID is not currently associated with a temporary SAML provider config: "
           + providerId);
diff --git a/src/test/java/com/google/firebase/auth/UserTestUtils.java b/src/test/java/com/google/firebase/auth/UserTestUtils.java
index 8f16114..aa86e6e 100644
--- a/src/test/java/com/google/firebase/auth/UserTestUtils.java
+++ b/src/test/java/com/google/firebase/auth/UserTestUtils.java
@@ -16,11 +16,11 @@
 
 package com.google.firebase.auth;
 
-import static com.google.common.base.Preconditions.checkArgument;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import com.google.firebase.auth.internal.AuthHttpClient;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Random;
@@ -28,21 +28,21 @@ import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 import org.junit.rules.ExternalResource;
 
-final class UserTestUtils {
+public final class UserTestUtils {
 
-  static void assertUserDoesNotExist(AbstractFirebaseAuth firebaseAuth, String uid)
+  public static void assertUserDoesNotExist(AbstractFirebaseAuth firebaseAuth, String uid)
       throws Exception {
     try {
       firebaseAuth.getUserAsync(uid).get();
       fail("No error thrown for getting a user which was expected to be absent.");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
+      assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR,
           ((FirebaseAuthException) e.getCause()).getErrorCode());
     }
   }
 
-  static RandomUser generateRandomUserInfo() {
+  public static RandomUser generateRandomUserInfo() {
     String uid = UUID.randomUUID().toString().replaceAll("-", "");
     String email = String.format(
         "test%s@example.%s.com",
@@ -60,7 +60,7 @@ final class UserTestUtils {
     return builder.toString();
   }
 
-  static class RandomUser {
+  public static class RandomUser {
     private final String uid;
     private final String email;
     private final String phoneNumber;
@@ -71,15 +71,15 @@ final class UserTestUtils {
       this.phoneNumber = phoneNumber;
     }
 
-    String getUid() {
+    public String getUid() {
       return uid;
     }
 
-    String getEmail() {
+    public String getEmail() {
       return email;
     }
 
-    String getPhoneNumber() {
+    public String getPhoneNumber() {
       return phoneNumber;
     }
   }
@@ -88,12 +88,12 @@ final class UserTestUtils {
    * Creates temporary Firebase user accounts for testing, and deletes them at the end of each
    * test case.
    */
-  static final class TemporaryUser extends ExternalResource {
+  public static final class TemporaryUser extends ExternalResource {
 
     private final AbstractFirebaseAuth auth;
     private final List<String> users = new ArrayList<>();
 
-    TemporaryUser(AbstractFirebaseAuth auth) {
+    public TemporaryUser(AbstractFirebaseAuth auth) {
       this.auth = auth;
     }
 
diff --git a/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java
new file mode 100644
index 0000000..36660dc
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.multitenancy;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.api.client.googleapis.util.Utils;
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpHeaders;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import com.google.firebase.TestOnlyImplFirebaseTrampolines;
+import com.google.firebase.auth.FirebaseAuth;
+import com.google.firebase.auth.FirebaseAuthException;
+import com.google.firebase.auth.MockGoogleCredentials;
+import com.google.firebase.auth.internal.AuthHttpClient;
+import com.google.firebase.internal.SdkUtils;
+import com.google.firebase.testing.MultiRequestMockHttpTransport;
+import com.google.firebase.testing.TestResponseInterceptor;
+import com.google.firebase.testing.TestUtils;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.After;
+import org.junit.Test;
+
+public class FirebaseTenantClientTest {
+
+  private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory();
+
+  private static final String TEST_TOKEN = "token";
+
+  private static final GoogleCredentials credentials = new MockGoogleCredentials(TEST_TOKEN);
+
+  private static final String PROJECT_BASE_URL =
+      "https://identitytoolkit.googleapis.com/v2/projects/test-project-id";
+
+  private static final String TENANTS_BASE_URL = PROJECT_BASE_URL + "/tenants";
+
+  @After
+  public void tearDown() {
+    TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
+  }
+
+  @Test
+  public void testGetTenant() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForTenantManagement(
+        TestUtils.loadResource("tenant.json"));
+
+    Tenant tenant = FirebaseAuth.getInstance().getTenantManager().getTenant("TENANT_1");
+
+    checkTenant(tenant, "TENANT_1");
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_1");
+  }
+
+  @Test
+  public void testGetTenantWithNotFoundError() {
+    TestResponseInterceptor interceptor =
+        initializeAppForTenantManagementWithStatusCode(404,
+            "{\"error\": {\"message\": \"TENANT_NOT_FOUND\"}}");
+    try {
+      FirebaseAuth.getInstance().getTenantManager().getTenant("UNKNOWN");
+      fail("No error thrown for invalid response");
+    } catch (FirebaseAuthException e) {
+      assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, e.getErrorCode());
+    }
+    checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/UNKNOWN");
+  }
+
+  @Test
+  public void testListTenants() throws Exception {
+    final TestResponseInterceptor interceptor = initializeAppForTenantManagement(
+        TestUtils.loadResource("listTenants.json"));
+
+    ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants(null, 99);
+
+    ImmutableList<Tenant> tenants = ImmutableList.copyOf(page.getValues());
+    assertEquals(2, tenants.size());
+    checkTenant(tenants.get(0), "TENANT_1");
+    checkTenant(tenants.get(1), "TENANT_2");
+    assertEquals("", page.getNextPageToken());
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "GET", TENANTS_BASE_URL);
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals(99, url.getFirst("pageSize"));
+    assertNull(url.getFirst("pageToken"));
+  }
+
+  @Test
+  public void testListTenantsWithPageToken() throws Exception {
+    final TestResponseInterceptor interceptor = initializeAppForTenantManagement(
+        TestUtils.loadResource("listTenants.json"));
+
+    ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants("token", 99);
+
+    ImmutableList<Tenant> tenants = ImmutableList.copyOf(page.getValues());
+    assertEquals(2, tenants.size());
+    checkTenant(tenants.get(0), "TENANT_1");
+    checkTenant(tenants.get(1), "TENANT_2");
+    assertEquals("", page.getNextPageToken());
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "GET", TENANTS_BASE_URL);
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals(99, url.getFirst("pageSize"));
+    assertEquals("token", url.getFirst("pageToken"));
+  }
+
+  @Test
+  public void testListZeroTenants() throws Exception {
+    final TestResponseInterceptor interceptor = initializeAppForTenantManagement("{}");
+
+    ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants(null);
+
+    assertTrue(Iterables.isEmpty(page.getValues()));
+    assertEquals("", page.getNextPageToken());
+    checkRequestHeaders(interceptor);
+  }
+
+  @Test
+  public void testCreateTenant() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForTenantManagement(
+        TestUtils.loadResource("tenant.json"));
+    Tenant.CreateRequest request = new Tenant.CreateRequest()
+        .setDisplayName("DISPLAY_NAME")
+        .setPasswordSignInAllowed(true)
+        .setEmailLinkSignInEnabled(false);
+
+    Tenant tenant = FirebaseAuth.getInstance().getTenantManager().createTenant(request);
+
+    checkTenant(tenant, "TENANT_1");
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "POST", TENANTS_BASE_URL);
+    GenericJson parsed = parseRequestContent(interceptor);
+    assertEquals("DISPLAY_NAME", parsed.get("displayName"));
+    assertEquals(true, parsed.get("allowPasswordSignup"));
+    assertEquals(false, parsed.get("enableEmailLinkSignin"));
+  }
+
+  @Test
+  public void testCreateTenantMinimal() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForTenantManagement(
+        TestUtils.loadResource("tenant.json"));
+    Tenant.CreateRequest request = new Tenant.CreateRequest();
+
+    Tenant tenant = FirebaseAuth.getInstance().getTenantManager().createTenant(request);
+
+    checkTenant(tenant, "TENANT_1");
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "POST", TENANTS_BASE_URL);
+    GenericJson parsed = parseRequestContent(interceptor);
+    assertNull(parsed.get("displayName"));
+    assertNull(parsed.get("allowPasswordSignup"));
+    assertNull(parsed.get("enableEmailLinkSignin"));
+  }
+
+  @Test
+  public void testCreateTenantError() {
+    TestResponseInterceptor interceptor =
+        initializeAppForTenantManagementWithStatusCode(404,
+            "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
+    try {
+      FirebaseAuth.getInstance().getTenantManager().createTenant(new Tenant.CreateRequest());
+      fail("No error thrown for invalid response");
+    } catch (FirebaseAuthException e) {
+      assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode());
+    }
+    checkUrl(interceptor, "POST", TENANTS_BASE_URL);
+  }
+
+  @Test
+  public void testUpdateTenant() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForTenantManagement(
+        TestUtils.loadResource("tenant.json"));
+    Tenant.UpdateRequest request = new Tenant.UpdateRequest("TENANT_1")
+        .setDisplayName("DISPLAY_NAME")
+        .setPasswordSignInAllowed(true)
+        .setEmailLinkSignInEnabled(false);
+
+    Tenant tenant = FirebaseAuth.getInstance().getTenantManager().updateTenant(request);
+
+    checkTenant(tenant, "TENANT_1");
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1");
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals("allowPasswordSignup,displayName,enableEmailLinkSignin",
+        url.getFirst("updateMask"));
+    GenericJson parsed = parseRequestContent(interceptor);
+    assertEquals("DISPLAY_NAME", parsed.get("displayName"));
+    assertEquals(true, parsed.get("allowPasswordSignup"));
+    assertEquals(false, parsed.get("enableEmailLinkSignin"));
+  }
+
+  @Test
+  public void testUpdateTenantMinimal() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForTenantManagement(
+        TestUtils.loadResource("tenant.json"));
+    Tenant.UpdateRequest request =
+        new Tenant.UpdateRequest("TENANT_1").setDisplayName("DISPLAY_NAME");
+
+    Tenant tenant = FirebaseAuth.getInstance().getTenantManager().updateTenant(request);
+
+    checkTenant(tenant, "TENANT_1");
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1");
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals("displayName", url.getFirst("updateMask"));
+    GenericJson parsed = parseRequestContent(interceptor);
+    assertEquals("DISPLAY_NAME", parsed.get("displayName"));
+    assertNull(parsed.get("allowPasswordSignup"));
+    assertNull(parsed.get("enableEmailLinkSignin"));
+  }
+
+  @Test
+  public void testUpdateTenantNoValues() throws Exception {
+    initializeAppForTenantManagement(TestUtils.loadResource("tenant.json"));
+    TenantManager tenantManager = FirebaseAuth.getInstance().getTenantManager();
+    try {
+      tenantManager.updateTenant(new Tenant.UpdateRequest("TENANT_1"));
+      fail("No error thrown for empty tenant update");
+    } catch (IllegalArgumentException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testUpdateTenantError() {
+    TestResponseInterceptor interceptor =
+        initializeAppForTenantManagementWithStatusCode(404,
+            "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
+    Tenant.UpdateRequest request =
+        new Tenant.UpdateRequest("TENANT_1").setDisplayName("DISPLAY_NAME");
+    try {
+      FirebaseAuth.getInstance().getTenantManager().updateTenant(request);
+      fail("No error thrown for invalid response");
+    } catch (FirebaseAuthException e) {
+      assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode());
+    }
+    checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1");
+  }
+
+  @Test
+  public void testDeleteTenant() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForTenantManagement("{}");
+
+    FirebaseAuth.getInstance().getTenantManager().deleteTenant("TENANT_1");
+
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/TENANT_1");
+  }
+
+  @Test
+  public void testDeleteTenantWithNotFoundError() {
+    TestResponseInterceptor interceptor =
+        initializeAppForTenantManagementWithStatusCode(404,
+            "{\"error\": {\"message\": \"TENANT_NOT_FOUND\"}}");
+    try {
+      FirebaseAuth.getInstance().getTenantManager().deleteTenant("UNKNOWN");
+      fail("No error thrown for invalid response");
+    } catch (FirebaseAuthException e) {
+      assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, e.getErrorCode());
+    }
+    checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/UNKNOWN");
+  }
+
+  private static void checkTenant(Tenant tenant, String tenantId) {
+    assertEquals(tenantId, tenant.getTenantId());
+    assertEquals("DISPLAY_NAME", tenant.getDisplayName());
+    assertTrue(tenant.isPasswordSignInAllowed());
+    assertFalse(tenant.isEmailLinkSignInEnabled());
+  }
+
+  private static void checkRequestHeaders(TestResponseInterceptor interceptor) {
+    HttpHeaders headers = interceptor.getResponse().getRequest().getHeaders();
+    String auth = "Bearer " + TEST_TOKEN;
+    assertEquals(auth, headers.getFirstHeaderStringValue("Authorization"));
+
+    String clientVersion = "Java/Admin/" + SdkUtils.getVersion();
+    assertEquals(clientVersion, headers.getFirstHeaderStringValue("X-Client-Version"));
+  }
+
+  private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) {
+    HttpRequest request = interceptor.getResponse().getRequest();
+    if (method.equals("PATCH")) {
+      assertEquals("PATCH",
+          request.getHeaders().getFirstHeaderStringValue("X-HTTP-Method-Override"));
+      assertEquals("POST", request.getRequestMethod());
+    } else {
+      assertEquals(method, request.getRequestMethod());
+    }
+    assertEquals(url, request.getUrl().toString().split("\\?")[0]);
+  }
+
+  private static TestResponseInterceptor initializeAppForTenantManagement(String... responses) {
+    initializeAppWithResponses(responses);
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    FirebaseAuth.getInstance().getTenantManager().setInterceptor(interceptor);
+    return interceptor;
+  }
+
+  private static TestResponseInterceptor initializeAppForTenantManagementWithStatusCode(
+      int statusCode, String response) {
+    FirebaseApp.initializeApp(new FirebaseOptions.Builder()
+        .setCredentials(credentials)
+        .setHttpTransport(
+            new MockHttpTransport.Builder()
+                .setLowLevelHttpResponse(
+                  new MockLowLevelHttpResponse().setContent(response).setStatusCode(statusCode))
+                .build())
+        .setProjectId("test-project-id")
+        .build());
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    FirebaseAuth.getInstance().getTenantManager().setInterceptor(interceptor);
+    return interceptor;
+  }
+
+  private static void initializeAppWithResponses(String... responses) {
+    List<MockLowLevelHttpResponse> mocks = new ArrayList<>();
+    for (String response : responses) {
+      mocks.add(new MockLowLevelHttpResponse().setContent(response));
+    }
+    MockHttpTransport transport = new MultiRequestMockHttpTransport(mocks);
+    FirebaseApp.initializeApp(new FirebaseOptions.Builder()
+        .setCredentials(credentials)
+        .setHttpTransport(transport)
+        .setProjectId("test-project-id")
+        .build());
+  }
+
+  private static GenericJson parseRequestContent(TestResponseInterceptor interceptor)
+      throws IOException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    interceptor.getResponse().getRequest().getContent().writeTo(out);
+    return JSON_FACTORY.fromString(new String(out.toByteArray()), GenericJson.class);
+  }
+}
diff --git a/src/test/java/com/google/firebase/auth/ListTenantsPageTest.java b/src/test/java/com/google/firebase/auth/multitenancy/ListTenantsPageTest.java
similarity index 98%
rename from src/test/java/com/google/firebase/auth/ListTenantsPageTest.java
rename to src/test/java/com/google/firebase/auth/multitenancy/ListTenantsPageTest.java
index c1d09bc..99a9786 100644
--- a/src/test/java/com/google/firebase/auth/ListTenantsPageTest.java
+++ b/src/test/java/com/google/firebase/auth/multitenancy/ListTenantsPageTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.google.firebase.auth;
+package com.google.firebase.auth.multitenancy;
 
 import static junit.framework.TestCase.assertTrue;
 import static org.junit.Assert.assertEquals;
@@ -24,10 +24,11 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
 
 import com.google.api.client.googleapis.util.Utils;
-import com.google.api.client.json.JsonFactory;
 import com.google.common.collect.ImmutableList;
-import com.google.common.io.BaseEncoding;
+import com.google.firebase.auth.FirebaseAuthException;
 import com.google.firebase.auth.internal.ListTenantsResponse;
+import com.google.firebase.auth.multitenancy.ListTenantsPage;
+import com.google.firebase.auth.multitenancy.Tenant;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Iterator;
diff --git a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java
similarity index 96%
rename from src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java
rename to src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java
index a6d83dc..61a8419 100644
--- a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.google.firebase.auth;
+package com.google.firebase.auth.multitenancy;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -39,7 +39,18 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.firebase.FirebaseApp;
+import com.google.firebase.auth.ExportedUserRecord;
+import com.google.firebase.auth.FirebaseAuth;
+import com.google.firebase.auth.FirebaseAuthException;
+import com.google.firebase.auth.FirebaseToken;
+import com.google.firebase.auth.ListProviderConfigsPage;
+import com.google.firebase.auth.ListUsersPage;
+import com.google.firebase.auth.OidcProviderConfig;
+import com.google.firebase.auth.ProviderConfigTestUtils;
 import com.google.firebase.auth.ProviderConfigTestUtils.TemporaryProviderConfig;
+import com.google.firebase.auth.SamlProviderConfig;
+import com.google.firebase.auth.UserRecord;
+import com.google.firebase.auth.UserTestUtils;
 import com.google.firebase.auth.UserTestUtils.RandomUser;
 import com.google.firebase.auth.UserTestUtils.TemporaryUser;
 import com.google.firebase.internal.Nullable;
@@ -56,7 +67,6 @@ import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExternalResource;
 
 public class TenantAwareFirebaseAuthIT {
 
@@ -247,7 +257,7 @@ public class TenantAwareFirebaseAuthIT {
       fail("No error thrown for verifying a token with the wrong tenant-aware client");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.TENANT_ID_MISMATCH_ERROR,
+      assertEquals("tenant-id-mismatch",
           ((FirebaseAuthException) e.getCause()).getErrorCode());
     }
   }
@@ -420,7 +430,7 @@ public class TenantAwareFirebaseAuthIT {
       String customToken, @Nullable String tenantId) throws IOException {
     final GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key="
         + IntegrationTestUtils.getApiKey());
-    ImmutableMap.Builder<String, Object> content = ImmutableMap.<String, Object>builder();
+    ImmutableMap.Builder<String, Object> content = ImmutableMap.builder();
     content.put("token", customToken);
     content.put("returnSecureToken", true);
     if (tenantId != null) {
diff --git a/src/test/java/com/google/firebase/auth/TenantManagerIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java
similarity index 95%
rename from src/test/java/com/google/firebase/auth/TenantManagerIT.java
rename to src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java
index 6d96a11..086e25a 100644
--- a/src/test/java/com/google/firebase/auth/TenantManagerIT.java
+++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.google.firebase.auth;
+package com.google.firebase.auth.multitenancy;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -27,6 +27,9 @@ import com.google.api.core.ApiFuture;
 import com.google.api.core.ApiFutureCallback;
 import com.google.api.core.ApiFutures;
 import com.google.common.util.concurrent.MoreExecutors;
+import com.google.firebase.auth.FirebaseAuth;
+import com.google.firebase.auth.FirebaseAuthException;
+import com.google.firebase.auth.internal.AuthHttpClient;
 import com.google.firebase.testing.IntegrationTestUtils;
 import java.util.ArrayList;
 import java.util.List;
@@ -78,7 +81,7 @@ public class TenantManagerIT {
       fail("No error thrown for getting a deleted tenant");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.TENANT_NOT_FOUND_ERROR,
+      assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR,
           ((FirebaseAuthException) e.getCause()).getErrorCode());
     }
   }
diff --git a/src/test/java/com/google/firebase/auth/TenantTest.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java
similarity index 97%
rename from src/test/java/com/google/firebase/auth/TenantTest.java
rename to src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java
index f47ccf8..39b7de3 100644
--- a/src/test/java/com/google/firebase/auth/TenantTest.java
+++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-package com.google.firebase.auth;
+package com.google.firebase.auth.multitenancy;
 
-import static com.google.firebase.auth.Tenant.UpdateRequest;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
@@ -24,6 +23,7 @@ import static org.junit.Assert.assertTrue;
 
 import com.google.api.client.googleapis.util.Utils;
 import com.google.api.client.json.JsonFactory;
+import com.google.firebase.auth.multitenancy.Tenant;
 import java.io.IOException;
 import java.util.Map;
 import org.junit.Test;