Repository: firebase/firebase-admin-java
Pull request #469 (2020-08-08 12:56:32)

Added toString() in TopicManagementResponse for logging

googlebot: 
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

:memo: **Please visit <https://cla.developers.google.com/> to sign.**

Once you've signed (or fixed any issues), please reply here with `@googlebot I signed it!` and we'll verify it.

----

#### What to do if you already signed the CLA

##### Individual signers

*   It's possible we don't have your GitHub username or you're using a different email address on your commit. Check [your existing CLA data](https://cla.developers.google.com/clas) and verify that your [email is set on your git commits](https://help.github.com/articles/setting-your-email-in-git/).

##### Corporate signers

*   Your company has a Point of Contact who decides which employees are authorized to participate. Ask your POC to be added to the group of authorized contributors. If you don't know who your Point of Contact is, direct the Google project maintainer to [go/cla#troubleshoot](http://go/cla#troubleshoot) ([Public version](https://opensource.google/docs/cla/#troubleshoot)).
*   The email used to register you as an authorized contributor must be the email used for the Git commit. Check [your existing CLA data](https://cla.developers.google.com/clas) and verify that your [email is set on your git commits](https://help.github.com/articles/setting-your-email-in-git/).
*   The email used to register you as an authorized contributor must also be [attached to your GitHub account](https://github.com/settings/emails).
		

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Ffirebase%2Ffirebase-admin-java%2Fpull%2F469) for more info**.

<!-- need_sender_cla -->
shambhand: @googlebot I signed it!
googlebot: CLAs look good, thanks!

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Ffirebase%2Ffirebase-admin-java%2Fpull%2F469) for more info**.

<!-- ok -->

Patch:
diff --git a/.gitignore b/.gitignore
index a869aa6..6e7d29c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@ target/
 release.properties
 integration_cert.json
 integration_apikey.txt
+.DS_Store
diff --git a/pom.xml b/pom.xml
index adbf239..b018754 100644
--- a/pom.xml
+++ b/pom.xml
@@ -198,7 +198,7 @@
                     <plugin>
                         <groupId>org.jacoco</groupId>
                         <artifactId>jacoco-maven-plugin</artifactId>
-                        <version>0.7.9</version>
+                        <version>0.8.5</version>
                         <executions>
                             <execution>
                                 <id>pre-unit-test</id>
@@ -289,7 +289,7 @@
             <!-- Test Phase -->
             <plugin>
                 <artifactId>maven-surefire-plugin</artifactId>
-                <version>2.19.1</version>
+                <version>2.22.0</version>
                 <configuration>
                     <skipTests>${skipUTs}</skipTests>
                 </configuration>
diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
index ad30d2c..6c84107 100644
--- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
@@ -62,50 +62,13 @@ public abstract class AbstractFirebaseAuth {
   private final Supplier<? extends FirebaseUserManager> userManager;
   private final JsonFactory jsonFactory;
 
-  protected AbstractFirebaseAuth(Builder builder) {
+  protected AbstractFirebaseAuth(Builder<?> builder) {
     this.firebaseApp = checkNotNull(builder.firebaseApp);
     this.tokenFactory = threadSafeMemoize(builder.tokenFactory);
     this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier);
     this.cookieVerifier = threadSafeMemoize(builder.cookieVerifier);
     this.userManager = threadSafeMemoize(builder.userManager);
-    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();
-              }
-            });
+    this.jsonFactory = firebaseApp.getOptions().getJsonFactory();
   }
 
   /**
@@ -220,6 +183,51 @@ public abstract class AbstractFirebaseAuth {
     };
   }
 
+  /**
+   * Creates a new Firebase session cookie from the given ID token and options. The returned JWT can
+   * be set as a server-side session cookie with a custom cookie policy.
+   *
+   * @param idToken The Firebase ID token to exchange for a session cookie.
+   * @param options Additional options required to create the cookie.
+   * @return A Firebase session cookie string.
+   * @throws IllegalArgumentException If the ID token is null or empty, or if options is null.
+   * @throws FirebaseAuthException If an error occurs while generating the session cookie.
+   */
+  public String createSessionCookie(@NonNull String idToken, @NonNull SessionCookieOptions options)
+      throws FirebaseAuthException {
+    return createSessionCookieOp(idToken, options).call();
+  }
+
+  /**
+   * Similar to {@link #createSessionCookie(String, SessionCookieOptions)} but performs the
+   * operation asynchronously.
+   *
+   * @param idToken The Firebase ID token to exchange for a session cookie.
+   * @param options Additional options required to create the cookie.
+   * @return An {@code ApiFuture} which will complete successfully with a session cookie string. If
+   *     an error occurs while generating the cookie or if the specified ID token is invalid, the
+   *     future throws a {@link FirebaseAuthException}.
+   * @throws IllegalArgumentException If the ID token is null or empty, or if options is null.
+   */
+  public ApiFuture<String> createSessionCookieAsync(
+      @NonNull String idToken, @NonNull SessionCookieOptions options) {
+    return createSessionCookieOp(idToken, options).callAsync(firebaseApp);
+  }
+
+  private CallableOperation<String, FirebaseAuthException> createSessionCookieOp(
+      final String idToken, final SessionCookieOptions options) {
+    checkNotDestroyed();
+    checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty");
+    checkNotNull(options, "options must not be null");
+    final FirebaseUserManager userManager = getUserManager();
+    return new CallableOperation<String, FirebaseAuthException>() {
+      @Override
+      protected String execute() throws FirebaseAuthException {
+        return userManager.createSessionCookie(idToken, options);
+      }
+    };
+  }
+
   /**
    * Parses and verifies a Firebase ID Token.
    *
@@ -320,6 +328,87 @@ public abstract class AbstractFirebaseAuth {
     return verifier;
   }
 
+  /**
+   * Parses and verifies a Firebase session cookie.
+   *
+   * <p>If verified successfully, returns a parsed version of the cookie from which the UID and the
+   * other claims can be read. If the cookie is invalid, throws a {@link FirebaseAuthException}.
+   *
+   * <p>This method does not check whether the cookie has been revoked. See {@link
+   * #verifySessionCookie(String, boolean)}.
+   *
+   * @param cookie A Firebase session cookie string to verify and parse.
+   * @return A {@link FirebaseToken} representing the verified and decoded cookie.
+   */
+  public FirebaseToken verifySessionCookie(String cookie) throws FirebaseAuthException {
+    return verifySessionCookie(cookie, false);
+  }
+
+  /**
+   * Parses and verifies a Firebase session cookie.
+   *
+   * <p>If {@code checkRevoked} is true, additionally verifies that the cookie has not been revoked.
+   *
+   * <p>If verified successfully, returns a parsed version of the cookie from which the UID and the
+   * other claims can be read. If the cookie is invalid or has been revoked while {@code
+   * checkRevoked} is true, throws a {@link FirebaseAuthException}.
+   *
+   * @param cookie A Firebase session cookie string to verify and parse.
+   * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked.
+   * @return A {@link FirebaseToken} representing the verified and decoded cookie.
+   */
+  public FirebaseToken verifySessionCookie(String cookie, boolean checkRevoked)
+      throws FirebaseAuthException {
+    return verifySessionCookieOp(cookie, checkRevoked).call();
+  }
+
+  /**
+   * Similar to {@link #verifySessionCookie(String)} but performs the operation asynchronously.
+   *
+   * @param cookie A Firebase session cookie string to verify and parse.
+   * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or
+   *     unsuccessfully with the failure Exception.
+   */
+  public ApiFuture<FirebaseToken> verifySessionCookieAsync(String cookie) {
+    return verifySessionCookieAsync(cookie, false);
+  }
+
+  /**
+   * Similar to {@link #verifySessionCookie(String, boolean)} but performs the operation
+   * asynchronously.
+   *
+   * @param cookie A Firebase session cookie string to verify and parse.
+   * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked.
+   * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or
+   *     unsuccessfully with the failure Exception.
+   */
+  public ApiFuture<FirebaseToken> verifySessionCookieAsync(String cookie, boolean checkRevoked) {
+    return verifySessionCookieOp(cookie, checkRevoked).callAsync(firebaseApp);
+  }
+
+  private CallableOperation<FirebaseToken, FirebaseAuthException> verifySessionCookieOp(
+      final String cookie, final boolean checkRevoked) {
+    checkNotDestroyed();
+    checkArgument(!Strings.isNullOrEmpty(cookie), "Session cookie must not be null or empty");
+    final FirebaseTokenVerifier sessionCookieVerifier = getSessionCookieVerifier(checkRevoked);
+    return new CallableOperation<FirebaseToken, FirebaseAuthException>() {
+      @Override
+      public FirebaseToken execute() throws FirebaseAuthException {
+        return sessionCookieVerifier.verifyToken(cookie);
+      }
+    };
+  }
+
+  @VisibleForTesting
+  FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) {
+    FirebaseTokenVerifier verifier = cookieVerifier.get();
+    if (checkRevoked) {
+      FirebaseUserManager userManager = getUserManager();
+      verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager);
+    }
+    return verifier;
+  }
+
   /**
    * Revokes all refresh tokens for the specified user.
    *
@@ -1637,19 +1726,11 @@ public abstract class AbstractFirebaseAuth {
     };
   }
 
-  FirebaseApp getFirebaseApp() {
-    return this.firebaseApp;
-  }
-
-  FirebaseTokenVerifier getCookieVerifier() {
-    return this.cookieVerifier.get();
-  }
-
   FirebaseUserManager getUserManager() {
     return this.userManager.get();
   }
 
-  protected <T> Supplier<T> threadSafeMemoize(final Supplier<T> supplier) {
+  <T> Supplier<T> threadSafeMemoize(final Supplier<T> supplier) {
     return Suppliers.memoize(
         new Supplier<T>() {
           @Override
@@ -1663,7 +1744,7 @@ public abstract class AbstractFirebaseAuth {
         });
   }
 
-  void checkNotDestroyed() {
+  private void checkNotDestroyed() {
     synchronized (lock) {
       checkState(
           !destroyed.get(),
@@ -1682,42 +1763,80 @@ public abstract class AbstractFirebaseAuth {
   /** Performs any additional required clean up. */
   protected abstract void doDestroy();
 
-  static Builder builder() {
-    return new Builder();
-  }
+  protected abstract static class Builder<T extends  Builder<T>> {
 
-  static class Builder {
-    protected FirebaseApp firebaseApp;
+    private FirebaseApp firebaseApp;
     private Supplier<FirebaseTokenFactory> tokenFactory;
     private Supplier<? extends FirebaseTokenVerifier> idTokenVerifier;
     private Supplier<? extends FirebaseTokenVerifier> cookieVerifier;
-    private Supplier<FirebaseUserManager> userManager;
+    private Supplier<? extends FirebaseUserManager> userManager;
 
-    private Builder() {}
+    protected abstract T getThis();
+
+    public FirebaseApp getFirebaseApp() {
+      return firebaseApp;
+    }
 
-    Builder setFirebaseApp(FirebaseApp firebaseApp) {
+    public T setFirebaseApp(FirebaseApp firebaseApp) {
       this.firebaseApp = firebaseApp;
-      return this;
+      return getThis();
     }
 
-    Builder setTokenFactory(Supplier<FirebaseTokenFactory> tokenFactory) {
+    public T setTokenFactory(Supplier<FirebaseTokenFactory> tokenFactory) {
       this.tokenFactory = tokenFactory;
-      return this;
+      return getThis();
     }
 
-    Builder setIdTokenVerifier(Supplier<? extends FirebaseTokenVerifier> idTokenVerifier) {
+    public T setIdTokenVerifier(Supplier<? extends FirebaseTokenVerifier> idTokenVerifier) {
       this.idTokenVerifier = idTokenVerifier;
-      return this;
+      return getThis();
     }
 
-    Builder setCookieVerifier(Supplier<? extends FirebaseTokenVerifier> cookieVerifier) {
+    public T setCookieVerifier(Supplier<? extends FirebaseTokenVerifier> cookieVerifier) {
       this.cookieVerifier = cookieVerifier;
-      return this;
+      return getThis();
     }
 
-    Builder setUserManager(Supplier<FirebaseUserManager> userManager) {
+    public T setUserManager(Supplier<FirebaseUserManager> userManager) {
       this.userManager = userManager;
-      return this;
+      return getThis();
     }
   }
+
+  protected static <T extends Builder<T>> T populateBuilderFromApp(
+      Builder<T> builder, final FirebaseApp app, @Nullable final String tenantId) {
+    return 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, tenantId);
+              }
+            })
+        .setUserManager(
+            new Supplier<FirebaseUserManager>() {
+              @Override
+              public FirebaseUserManager get() {
+                return FirebaseUserManager
+                    .builder()
+                    .setFirebaseApp(app)
+                    .setTenantId(tenantId)
+                    .build();
+              }
+            });
+  }
 }
diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
index f44bd23..417ea64 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
@@ -16,21 +16,11 @@
 
 package com.google.firebase.auth;
 
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import com.google.api.client.util.Clock;
-import com.google.api.core.ApiFuture;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Strings;
 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;
 
 /**
  * This class is the entry point for all server-side Firebase Authentication actions.
@@ -46,14 +36,9 @@ public final class FirebaseAuth extends AbstractFirebaseAuth {
 
   private final Supplier<TenantManager> tenantManager;
 
-  FirebaseAuth(final Builder builder) {
+  private FirebaseAuth(final Builder builder) {
     super(builder);
-    tenantManager = threadSafeMemoize(new Supplier<TenantManager>() {
-      @Override
-      public TenantManager get() {
-        return new TenantManager(builder.firebaseApp);
-      }
-    });
+    tenantManager = threadSafeMemoize(builder.tenantManager);
   }
 
   public TenantManager getTenantManager() {
@@ -84,167 +69,18 @@ public final class FirebaseAuth extends AbstractFirebaseAuth {
     return service.getInstance();
   }
 
-  /**
-   * Creates a new Firebase session cookie from the given ID token and options. The returned JWT can
-   * be set as a server-side session cookie with a custom cookie policy.
-   *
-   * @param idToken The Firebase ID token to exchange for a session cookie.
-   * @param options Additional options required to create the cookie.
-   * @return A Firebase session cookie string.
-   * @throws IllegalArgumentException If the ID token is null or empty, or if options is null.
-   * @throws FirebaseAuthException If an error occurs while generating the session cookie.
-   */
-  public String createSessionCookie(@NonNull String idToken, @NonNull SessionCookieOptions options)
-      throws FirebaseAuthException {
-    return createSessionCookieOp(idToken, options).call();
-  }
-
-  /**
-   * Similar to {@link #createSessionCookie(String, SessionCookieOptions)} but performs the
-   * operation asynchronously.
-   *
-   * @param idToken The Firebase ID token to exchange for a session cookie.
-   * @param options Additional options required to create the cookie.
-   * @return An {@code ApiFuture} which will complete successfully with a session cookie string. If
-   *     an error occurs while generating the cookie or if the specified ID token is invalid, the
-   *     future throws a {@link FirebaseAuthException}.
-   * @throws IllegalArgumentException If the ID token is null or empty, or if options is null.
-   */
-  public ApiFuture<String> createSessionCookieAsync(
-      @NonNull String idToken, @NonNull SessionCookieOptions options) {
-    return createSessionCookieOp(idToken, options).callAsync(getFirebaseApp());
-  }
-
-  private CallableOperation<String, FirebaseAuthException> createSessionCookieOp(
-      final String idToken, final SessionCookieOptions options) {
-    checkNotDestroyed();
-    checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty");
-    checkNotNull(options, "options must not be null");
-    final FirebaseUserManager userManager = getUserManager();
-    return new CallableOperation<String, FirebaseAuthException>() {
-      @Override
-      protected String execute() throws FirebaseAuthException {
-        return userManager.createSessionCookie(idToken, options);
-      }
-    };
-  }
-
-  /**
-   * Parses and verifies a Firebase session cookie.
-   *
-   * <p>If verified successfully, returns a parsed version of the cookie from which the UID and the
-   * other claims can be read. If the cookie is invalid, throws a {@link FirebaseAuthException}.
-   *
-   * <p>This method does not check whether the cookie has been revoked. See {@link
-   * #verifySessionCookie(String, boolean)}.
-   *
-   * @param cookie A Firebase session cookie string to verify and parse.
-   * @return A {@link FirebaseToken} representing the verified and decoded cookie.
-   */
-  public FirebaseToken verifySessionCookie(String cookie) throws FirebaseAuthException {
-    return verifySessionCookie(cookie, false);
-  }
-
-  /**
-   * Parses and verifies a Firebase session cookie.
-   *
-   * <p>If {@code checkRevoked} is true, additionally verifies that the cookie has not been revoked.
-   *
-   * <p>If verified successfully, returns a parsed version of the cookie from which the UID and the
-   * other claims can be read. If the cookie is invalid or has been revoked while {@code
-   * checkRevoked} is true, throws a {@link FirebaseAuthException}.
-   *
-   * @param cookie A Firebase session cookie string to verify and parse.
-   * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked.
-   * @return A {@link FirebaseToken} representing the verified and decoded cookie.
-   */
-  public FirebaseToken verifySessionCookie(String cookie, boolean checkRevoked)
-      throws FirebaseAuthException {
-    return verifySessionCookieOp(cookie, checkRevoked).call();
-  }
-
-  /**
-   * Similar to {@link #verifySessionCookie(String)} but performs the operation asynchronously.
-   *
-   * @param cookie A Firebase session cookie string to verify and parse.
-   * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or
-   *     unsuccessfully with the failure Exception.
-   */
-  public ApiFuture<FirebaseToken> verifySessionCookieAsync(String cookie) {
-    return verifySessionCookieAsync(cookie, false);
-  }
-
-  /**
-   * Similar to {@link #verifySessionCookie(String, boolean)} but performs the operation
-   * asynchronously.
-   *
-   * @param cookie A Firebase session cookie string to verify and parse.
-   * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked.
-   * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or
-   *     unsuccessfully with the failure Exception.
-   */
-  public ApiFuture<FirebaseToken> verifySessionCookieAsync(String cookie, boolean checkRevoked) {
-    return verifySessionCookieOp(cookie, checkRevoked).callAsync(getFirebaseApp());
-  }
-
-  private CallableOperation<FirebaseToken, FirebaseAuthException> verifySessionCookieOp(
-      final String cookie, final boolean checkRevoked) {
-    checkNotDestroyed();
-    checkArgument(!Strings.isNullOrEmpty(cookie), "Session cookie must not be null or empty");
-    final FirebaseTokenVerifier sessionCookieVerifier = getSessionCookieVerifier(checkRevoked);
-    return new CallableOperation<FirebaseToken, FirebaseAuthException>() {
-      @Override
-      public FirebaseToken execute() throws FirebaseAuthException {
-        return sessionCookieVerifier.verifyToken(cookie);
-      }
-    };
-  }
-
-  @VisibleForTesting
-  FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) {
-    FirebaseTokenVerifier verifier = getCookieVerifier();
-    if (checkRevoked) {
-      FirebaseUserManager userManager = getUserManager();
-      verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager);
-    }
-    return verifier;
-  }
-
   @Override
   protected void doDestroy() { }
 
   private static FirebaseAuth fromApp(final FirebaseApp app) {
-    return new FirebaseAuth(
-        AbstractFirebaseAuth.builder()
-          .setFirebaseApp(app)
-          .setTokenFactory(
-              new Supplier<FirebaseTokenFactory>() {
-                @Override
-                public FirebaseTokenFactory get() {
-                  return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM);
-                }
-              })
-          .setIdTokenVerifier(
-              new Supplier<FirebaseTokenVerifier>() {
-                @Override
-                public FirebaseTokenVerifier get() {
-                  return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM);
-                }
-              })
-          .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).build();
-                }
-              }));
+    return populateBuilderFromApp(builder(), app, null)
+        .setTenantManager(new Supplier<TenantManager>() {
+          @Override
+          public TenantManager get() {
+            return new TenantManager(app);
+          }
+        })
+        .build();
   }
 
   private static class FirebaseAuthService extends FirebaseService<FirebaseAuth> {
@@ -258,4 +94,29 @@ public final class FirebaseAuth extends AbstractFirebaseAuth {
       instance.destroy();
     }
   }
+
+  static Builder builder() {
+    return new Builder();
+  }
+
+  static class Builder extends AbstractFirebaseAuth.Builder<Builder> {
+
+    private Supplier<TenantManager> tenantManager;
+
+    private Builder() { }
+
+    @Override
+    protected Builder getThis() {
+      return this;
+    }
+
+    public Builder setTenantManager(Supplier<TenantManager> tenantManager) {
+      this.tenantManager = tenantManager;
+      return this;
+    }
+
+    public FirebaseAuth build() {
+      return new FirebaseAuth(this);
+    }
+  }
 }
diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java
index e0105e9..7c8f9f0 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java
@@ -99,6 +99,11 @@ final class FirebaseTokenUtils {
   }
 
   static FirebaseTokenVerifierImpl createSessionCookieVerifier(FirebaseApp app, Clock clock) {
+    return createSessionCookieVerifier(app, clock, null);
+  }
+
+  static FirebaseTokenVerifierImpl createSessionCookieVerifier(
+      FirebaseApp app, Clock clock, @Nullable String tenantId) {
     String projectId = ImplFirebaseTrampolines.getProjectId(app);
     checkState(!Strings.isNullOrEmpty(projectId),
         "Must initialize FirebaseApp with a project ID to call verifySessionCookie()");
@@ -107,12 +112,13 @@ final class FirebaseTokenUtils {
     GooglePublicKeysManager publicKeysManager = newPublicKeysManager(
         app.getOptions(), clock, SESSION_COOKIE_CERT_URL);
     return FirebaseTokenVerifierImpl.builder()
-        .setJsonFactory(app.getOptions().getJsonFactory())
-        .setPublicKeysManager(publicKeysManager)
-        .setIdTokenVerifier(idTokenVerifier)
         .setShortName("session cookie")
         .setMethod("verifySessionCookie()")
         .setDocUrl("https://firebase.google.com/docs/auth/admin/manage-cookies")
+        .setJsonFactory(app.getOptions().getJsonFactory())
+        .setPublicKeysManager(publicKeysManager)
+        .setIdTokenVerifier(idTokenVerifier)
+        .setTenantId(tenantId)
         .build();
   }
 
diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java
index 5404374..95ef169 100644
--- a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java
@@ -18,9 +18,16 @@ package com.google.firebase.auth.multitenancy;
 
 import static com.google.common.base.Preconditions.checkArgument;
 
+import com.google.api.core.ApiAsyncFunction;
+import com.google.api.core.ApiFuture;
+import com.google.api.core.ApiFutures;
 import com.google.common.base.Strings;
+import com.google.common.util.concurrent.MoreExecutors;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.auth.AbstractFirebaseAuth;
+import com.google.firebase.auth.FirebaseAuthException;
+import com.google.firebase.auth.FirebaseToken;
+import com.google.firebase.auth.SessionCookieOptions;
 
 /**
  * The tenant-aware Firebase client.
@@ -32,10 +39,10 @@ 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 TenantAwareFirebaseAuth(Builder builder) {
+    super(builder);
+    checkArgument(!Strings.isNullOrEmpty(builder.tenantId));
+    this.tenantId = builder.tenantId;
   }
 
   /** Returns the client's tenant ID. */
@@ -43,8 +50,58 @@ public final class TenantAwareFirebaseAuth extends AbstractFirebaseAuth {
     return tenantId;
   }
 
+  @Override
+  public String createSessionCookie(
+      String idToken, SessionCookieOptions options) throws FirebaseAuthException {
+    verifyIdToken(idToken);
+    return super.createSessionCookie(idToken, options);
+  }
+
+  @Override
+  public ApiFuture<String> createSessionCookieAsync(
+      final String idToken, final SessionCookieOptions options) {
+    ApiFuture<FirebaseToken> future = verifyIdTokenAsync(idToken);
+    return ApiFutures.transformAsync(future, new ApiAsyncFunction<FirebaseToken, String>() {
+      @Override
+      public ApiFuture<String> apply(FirebaseToken input) {
+        return TenantAwareFirebaseAuth.super.createSessionCookieAsync(idToken, options);
+      }
+    }, MoreExecutors.directExecutor());
+  }
+
   @Override
   protected void doDestroy() {
     // Nothing extra needs to be destroyed.
   }
+
+  static TenantAwareFirebaseAuth fromApp(FirebaseApp app, String tenantId) {
+    return populateBuilderFromApp(builder(), app, tenantId)
+        .setTenantId(tenantId)
+        .build();
+  }
+
+  static Builder builder() {
+    return new Builder();
+  }
+
+  static class Builder extends AbstractFirebaseAuth.Builder<Builder> {
+
+    private String tenantId;
+
+    private Builder() { }
+
+    @Override
+    protected Builder getThis() {
+      return this;
+    }
+
+    public Builder setTenantId(String tenantId) {
+      this.tenantId = tenantId;
+      return this;
+    }
+
+    TenantAwareFirebaseAuth build() {
+      return new TenantAwareFirebaseAuth(this);
+    }
+  }
 }
diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java
index 11f26b0..dcb226d 100644
--- a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java
+++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java
@@ -79,7 +79,7 @@ public final class TenantManager {
   public synchronized TenantAwareFirebaseAuth getAuthForTenant(@NonNull String tenantId) {
     checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty.");
     if (!tenantAwareAuths.containsKey(tenantId)) {
-      tenantAwareAuths.put(tenantId, new TenantAwareFirebaseAuth(firebaseApp, tenantId));
+      tenantAwareAuths.put(tenantId, TenantAwareFirebaseAuth.fromApp(firebaseApp, tenantId));
     }
     return tenantAwareAuths.get(tenantId);
   }
diff --git a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java
index b8f9257..bbf4c94 100644
--- a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java
+++ b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java
@@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.api.client.json.GenericJson;
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.firebase.internal.NonNull;
@@ -122,5 +123,13 @@ public class TopicManagementResponse {
     public String getReason() {
       return reason;
     }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+              .add("index", index)
+              .add("reason", reason)
+              .toString();
+    }
   }
 }


Test patch:
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
index 635e1bf..bfc8d17 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
@@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -28,11 +29,11 @@ import com.google.api.core.ApiFuture;
 import com.google.common.base.Defaults;
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
-import com.google.common.collect.ImmutableMap;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.TestOnlyImplFirebaseTrampolines;
 import com.google.firebase.testing.ServiceAccount;
+import com.google.firebase.testing.TestResponseInterceptor;
 import com.google.firebase.testing.TestUtils;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
@@ -43,7 +44,6 @@ import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
-
 import org.junit.After;
 import org.junit.Test;
 
@@ -169,8 +169,7 @@ public class FirebaseAuthTest {
 
   @Test
   public void testIdTokenVerifierInitializedOnDemand() throws Exception {
-    FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
-        getFirebaseToken("idTokenUser"));
+    FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("idTokenUser");
     CountingSupplier<FirebaseTokenVerifier> countingSupplier = new CountingSupplier<>(
         Suppliers.ofInstance(tokenVerifier));
 
@@ -185,37 +184,33 @@ public class FirebaseAuthTest {
 
   @Test
   public void testVerifyIdTokenWithNull() {
-    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null);
-    tokenVerifier.lastTokenString = "_init_";
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid");
     FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier);
 
     try {
       auth.verifyIdTokenAsync(null);
       fail("No error thrown for null id token");
     } catch (IllegalArgumentException expected) {
-      assertEquals("_init_", tokenVerifier.getLastTokenString());
+      assertNull(tokenVerifier.getLastTokenString());
     }
   }
 
   @Test
   public void testVerifyIdTokenWithEmptyString() {
-    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null);
-    tokenVerifier.lastTokenString = "_init_";
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid");
     FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier);
 
     try {
       auth.verifyIdTokenAsync("");
       fail("No error thrown for null id token");
     } catch (IllegalArgumentException expected) {
-      assertEquals("_init_", tokenVerifier.getLastTokenString());
+      assertNull(tokenVerifier.getLastTokenString());
     }
-
   }
 
   @Test
   public void testVerifyIdToken() throws Exception {
-    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
-        getFirebaseToken("testUser"));
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("testUser");
     FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier);
 
     FirebaseToken firebaseToken = auth.verifyIdToken("idtoken");
@@ -242,8 +237,7 @@ public class FirebaseAuthTest {
 
   @Test
   public void testVerifyIdTokenAsync() throws Exception {
-    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
-        getFirebaseToken("testUser"));
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("testUser");
     FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier);
 
     FirebaseToken firebaseToken = auth.verifyIdTokenAsync("idtoken").get();
@@ -300,8 +294,7 @@ public class FirebaseAuthTest {
 
   @Test
   public void testSessionCookieVerifierInitializedOnDemand() throws Exception {
-    FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
-        getFirebaseToken("cookieUser"));
+    FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("cookieUser");
     CountingSupplier<FirebaseTokenVerifier> countingSupplier = new CountingSupplier<>(
         Suppliers.ofInstance(tokenVerifier));
     FirebaseAuth auth = getAuthForSessionCookieVerification(countingSupplier);
@@ -316,37 +309,33 @@ public class FirebaseAuthTest {
 
   @Test
   public void testVerifySessionCookieWithNull() {
-    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null);
-    tokenVerifier.lastTokenString = "_init_";
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid");
     FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier);
 
     try {
       auth.verifySessionCookieAsync(null);
       fail("No error thrown for null id token");
     } catch (IllegalArgumentException expected) {
-      assertEquals("_init_", tokenVerifier.getLastTokenString());
+      assertNull(tokenVerifier.getLastTokenString());
     }
   }
 
   @Test
   public void testVerifySessionCookieWithEmptyString() {
-    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null);
-    tokenVerifier.lastTokenString = "_init_";
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid");
     FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier);
 
     try {
       auth.verifySessionCookieAsync("");
       fail("No error thrown for null id token");
     } catch (IllegalArgumentException expected) {
-      assertEquals("_init_", tokenVerifier.getLastTokenString());
+      assertNull(tokenVerifier.getLastTokenString());
     }
-
   }
 
   @Test
   public void testVerifySessionCookie() throws Exception {
-    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
-        getFirebaseToken("testUser"));
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("testUser");
     FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier);
 
     FirebaseToken firebaseToken = auth.verifySessionCookie("idtoken");
@@ -373,8 +362,7 @@ public class FirebaseAuthTest {
 
   @Test
   public void testVerifySessionCookieAsync() throws Exception {
-    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
-        getFirebaseToken("testUser"));
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("testUser");
     FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier);
 
     FirebaseToken firebaseToken = auth.verifySessionCookieAsync("idtoken").get();
@@ -417,10 +405,6 @@ public class FirebaseAuthTest {
     }
   }
 
-  private FirebaseToken getFirebaseToken(String subject) {
-    return new FirebaseToken(ImmutableMap.<String, Object>of("sub", subject));
-  }
-
   private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVerifier) {
     return getAuthForIdTokenVerification(Suppliers.ofInstance(tokenVerifier));
   }
@@ -429,11 +413,11 @@ public class FirebaseAuthTest {
       Supplier<? extends FirebaseTokenVerifier> tokenVerifierSupplier) {
     FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions);
     FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build();
-    return new FirebaseAuth(
-        AbstractFirebaseAuth.builder()
-          .setFirebaseApp(app)
-          .setIdTokenVerifier(tokenVerifierSupplier)
-          .setUserManager(Suppliers.ofInstance(userManager)));
+    return FirebaseAuth.builder()
+        .setFirebaseApp(app)
+        .setIdTokenVerifier(tokenVerifierSupplier)
+        .setUserManager(Suppliers.ofInstance(userManager))
+        .build();
   }
 
   private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier tokenVerifier) {
@@ -444,45 +428,23 @@ public class FirebaseAuthTest {
       Supplier<? extends FirebaseTokenVerifier> tokenVerifierSupplier) {
     FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions);
     FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build();
-    return new FirebaseAuth(
-          AbstractFirebaseAuth.builder()
-          .setFirebaseApp(app)
-          .setCookieVerifier(tokenVerifierSupplier)
-          .setUserManager(Suppliers.ofInstance(userManager)));
+    return FirebaseAuth.builder()
+        .setFirebaseApp(app)
+        .setCookieVerifier(tokenVerifierSupplier)
+        .setUserManager(Suppliers.ofInstance(userManager))
+        .build();
   }
 
-  private static class MockTokenVerifier implements FirebaseTokenVerifier {
-
-    private String lastTokenString;
-
-    private FirebaseToken result;
-    private FirebaseAuthException exception;
-
-    private MockTokenVerifier(FirebaseToken result, FirebaseAuthException exception) {
-      this.result = result;
-      this.exception = exception;
-    }
-
-    @Override
-    public FirebaseToken verifyToken(String token) throws FirebaseAuthException {
-      lastTokenString = token;
-      if (exception != null) {
-        throw exception;
-      }
-      return result;
-    }
-
-    String getLastTokenString() {
-      return this.lastTokenString;
-    }
-
-    static MockTokenVerifier fromResult(FirebaseToken result) {
-      return new MockTokenVerifier(result, null);
-    }
-
-    static MockTokenVerifier fromException(FirebaseAuthException exception) {
-      return new MockTokenVerifier(null, exception);
-    }
+  public static TestResponseInterceptor setUserManager(
+      AbstractFirebaseAuth.Builder<?> builder, FirebaseApp app, String tenantId) {
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    FirebaseUserManager userManager = FirebaseUserManager.builder()
+        .setFirebaseApp(app)
+        .setTenantId(tenantId)
+        .build();
+    userManager.setInterceptor(interceptor);
+    builder.setUserManager(Suppliers.ofInstance(userManager));
+    return interceptor;
   }
 
   private static class CountingSupplier<T> implements Supplier<T> {
diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java
index 5dd1e9c..379da0a 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java
@@ -217,6 +217,19 @@ public class FirebaseTokenVerifierImplTest {
     tokenVerifier.verifyToken("not.a.jwt");
   }
 
+  @Test
+  public void testVerifyTokenWithTenantId() throws FirebaseAuthException {
+    FirebaseTokenVerifierImpl verifier = fullyPopulatedBuilder()
+        .setTenantId("TENANT_1")
+        .build();
+
+    FirebaseToken firebaseToken = verifier.verifyToken(createTokenWithTenantId("TENANT_1"));
+
+    assertEquals(TEST_TOKEN_ISSUER, firebaseToken.getIssuer());
+    assertEquals(TestTokenFactory.UID, firebaseToken.getUid());
+    assertEquals("TENANT_1", firebaseToken.getTenantId());
+  }
+
   @Test
   public void testVerifyTokenDifferentTenantIds() {
     try {
diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
index d407fd8..7012ef6 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
@@ -2602,19 +2602,18 @@ public class FirebaseUserManagerTest {
         .setProjectId("test-project-id")
         .setHttpTransport(transport)
         .build());
-    return new FirebaseAuth(
-        AbstractFirebaseAuth.builder()
-          .setFirebaseApp(app)
-          .setUserManager(new Supplier<FirebaseUserManager>() {
-            @Override
-            public FirebaseUserManager get() {
-              return FirebaseUserManager
-                .builder()
-                .setFirebaseApp(app)
-                .setHttpRequestFactory(transport.createRequestFactory())
-                .build();
-            }
-          }));
+    return FirebaseAuth.builder()
+        .setFirebaseApp(app)
+        .setUserManager(new Supplier<FirebaseUserManager>() {
+          @Override
+          public FirebaseUserManager get() {
+            return FirebaseUserManager.builder()
+              .setFirebaseApp(app)
+              .setHttpRequestFactory(transport.createRequestFactory())
+              .build();
+          }
+          })
+        .build();
   }
 
   private static void checkUserRecord(UserRecord userRecord) {
diff --git a/src/test/java/com/google/firebase/auth/MockTokenVerifier.java b/src/test/java/com/google/firebase/auth/MockTokenVerifier.java
new file mode 100644
index 0000000..51f7d5a
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/MockTokenVerifier.java
@@ -0,0 +1,59 @@
+/*
+ * 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 com.google.common.collect.ImmutableMap;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class MockTokenVerifier implements FirebaseTokenVerifier {
+
+  private final FirebaseToken result;
+  private final FirebaseAuthException exception;
+  private final AtomicReference<String> lastTokenString = new AtomicReference<>();
+
+  private MockTokenVerifier(FirebaseToken result, FirebaseAuthException exception) {
+    this.result = result;
+    this.exception = exception;
+  }
+
+  @Override
+  public FirebaseToken verifyToken(String token) throws FirebaseAuthException {
+    lastTokenString.set(token);
+    if (exception != null) {
+      throw exception;
+    }
+    return result;
+  }
+
+  public String getLastTokenString() {
+    return this.lastTokenString.get();
+  }
+
+  public static MockTokenVerifier fromUid(String uid) {
+    long iat = System.currentTimeMillis() / 1000;
+    return fromResult(new FirebaseToken(ImmutableMap.<String, Object>of(
+        "sub", uid, "iat", iat)));
+  }
+
+  public static MockTokenVerifier fromResult(FirebaseToken result) {
+    return new MockTokenVerifier(result, null);
+  }
+
+  public static MockTokenVerifier fromException(FirebaseAuthException exception) {
+    return new MockTokenVerifier(null, exception);
+  }
+}
diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java
new file mode 100644
index 0000000..df7bcf0
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java
@@ -0,0 +1,292 @@
+/*
+ * 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.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.HttpMethods;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.common.base.Suppliers;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import com.google.firebase.TestOnlyImplFirebaseTrampolines;
+import com.google.firebase.auth.FirebaseAuthException;
+import com.google.firebase.auth.FirebaseAuthTest;
+import com.google.firebase.auth.FirebaseToken;
+import com.google.firebase.auth.MockGoogleCredentials;
+import com.google.firebase.auth.MockTokenVerifier;
+import com.google.firebase.auth.SessionCookieOptions;
+import com.google.firebase.testing.TestResponseInterceptor;
+import com.google.firebase.testing.TestUtils;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Test;
+
+public class TenantAwareFirebaseAuthTest {
+
+  private static final String TENANT_ID = "test-tenant";
+
+  private static final String AUTH_BASE_URL = "/v1/projects/test-project-id/tenants/" + TENANT_ID;
+
+  private static final SessionCookieOptions COOKIE_OPTIONS = SessionCookieOptions.builder()
+      .setExpiresIn(TimeUnit.HOURS.toMillis(1))
+      .build();
+
+  private static final FirebaseAuthException AUTH_EXCEPTION = new FirebaseAuthException(
+      "code", "reason");
+
+  private static final String CREATE_COOKIE_RESPONSE = TestUtils.loadResource(
+      "createSessionCookie.json");
+
+  @After
+  public void tearDown() {
+    TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
+  }
+
+  @Test
+  public void testCreateSessionCookieAsync() throws Exception {
+    MockTokenVerifier verifier = MockTokenVerifier.fromUid("uid");
+    TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(verifier);
+    TestResponseInterceptor interceptor = setUserManager(builder, CREATE_COOKIE_RESPONSE);
+    TenantAwareFirebaseAuth auth = builder.build();
+
+    String cookie = auth.createSessionCookieAsync("testToken", COOKIE_OPTIONS).get();
+
+    assertEquals("MockCookieString", cookie);
+    assertEquals("testToken", verifier.getLastTokenString());
+    GenericJson parsed = parseRequestContent(interceptor);
+    assertEquals(2, parsed.size());
+    assertEquals("testToken", parsed.get("idToken"));
+    assertEquals(new BigDecimal(3600), parsed.get("validDuration"));
+    checkUrl(interceptor, AUTH_BASE_URL + ":createSessionCookie");
+  }
+
+  @Test
+  public void testCreateSessionCookieAsyncError() throws Exception {
+    TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(
+        MockTokenVerifier.fromException(AUTH_EXCEPTION));
+    TestResponseInterceptor interceptor = setUserManager(builder, CREATE_COOKIE_RESPONSE);
+    TenantAwareFirebaseAuth auth = builder.build();
+
+    try {
+      auth.createSessionCookieAsync("testToken", COOKIE_OPTIONS).get();
+      fail("No error thrown for invalid ID token");
+    } catch (ExecutionException e) {
+      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      FirebaseAuthException cause = (FirebaseAuthException) e.getCause();
+      assertEquals("code", cause.getErrorCode());
+    }
+
+    assertNull(interceptor.getResponse());
+  }
+
+  @Test
+  public void testCreateSessionCookie() throws Exception {
+    TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(
+        MockTokenVerifier.fromUid("uid"));
+    TestResponseInterceptor interceptor = setUserManager(builder, CREATE_COOKIE_RESPONSE);
+    TenantAwareFirebaseAuth auth = builder.build();
+
+    String cookie = auth.createSessionCookie("testToken", COOKIE_OPTIONS);
+
+    assertEquals("MockCookieString", cookie);
+    GenericJson parsed = parseRequestContent(interceptor);
+    assertEquals(2, parsed.size());
+    assertEquals("testToken", parsed.get("idToken"));
+    assertEquals(new BigDecimal(3600), parsed.get("validDuration"));
+    checkUrl(interceptor, AUTH_BASE_URL + ":createSessionCookie");
+  }
+
+  @Test
+  public void testCreateSessionCookieError() {
+    TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(
+        MockTokenVerifier.fromException(AUTH_EXCEPTION));
+    TestResponseInterceptor interceptor = setUserManager(builder, CREATE_COOKIE_RESPONSE);
+    TenantAwareFirebaseAuth auth = builder.build();
+
+    try {
+      auth.createSessionCookie("testToken", COOKIE_OPTIONS);
+      fail("No error thrown for invalid ID token");
+    } catch (FirebaseAuthException e) {
+      assertEquals("code", e.getErrorCode());
+    }
+
+    assertNull(interceptor.getResponse());
+  }
+
+  @Test
+  public void testVerifySessionCookie() throws FirebaseAuthException {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid");
+    TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier);
+    setUserManager(builder);
+    TenantAwareFirebaseAuth auth = builder.build();
+
+    FirebaseToken token = auth.verifySessionCookie("cookie");
+
+    assertEquals("uid", token.getUid());
+    assertEquals("cookie", tokenVerifier.getLastTokenString());
+  }
+
+  @Test
+  public void testVerifySessionCookieFailure() {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(AUTH_EXCEPTION);
+    TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier);
+    setUserManager(builder);
+    TenantAwareFirebaseAuth auth = builder.build();
+
+    try {
+      auth.verifySessionCookie("cookie");
+      fail("No error thrown for invalid token");
+    } catch (FirebaseAuthException authException) {
+      assertEquals("code", authException.getErrorCode());
+      assertEquals("cookie", tokenVerifier.getLastTokenString());
+    }
+  }
+
+  @Test
+  public void testVerifySessionCookieAsync() throws Exception {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid");
+    TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier);
+    setUserManager(builder);
+    TenantAwareFirebaseAuth auth = builder.build();
+
+    FirebaseToken firebaseToken = auth.verifySessionCookieAsync("cookie").get();
+
+    assertEquals("uid", firebaseToken.getUid());
+    assertEquals("cookie", tokenVerifier.getLastTokenString());
+  }
+
+  @Test
+  public void testVerifySessionCookieAsyncFailure() throws InterruptedException {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(AUTH_EXCEPTION);
+    TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier);
+    setUserManager(builder);
+    TenantAwareFirebaseAuth auth = builder.build();
+
+    try {
+      auth.verifySessionCookieAsync("cookie").get();
+      fail("No error thrown for invalid token");
+    } catch (ExecutionException e) {
+      FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+      assertEquals("code", authException.getErrorCode());
+      assertEquals("cookie", tokenVerifier.getLastTokenString());
+    }
+  }
+
+  @Test
+  public void testVerifySessionCookieWithCheckRevoked() throws FirebaseAuthException {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid");
+    TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier);
+    TestResponseInterceptor interceptor = setUserManager(
+        builder, TestUtils.loadResource("getUser.json"));
+    TenantAwareFirebaseAuth auth = builder.build();
+
+    FirebaseToken token = auth.verifySessionCookie("cookie", true);
+
+    assertEquals("uid", token.getUid());
+    assertEquals("cookie", tokenVerifier.getLastTokenString());
+    checkUrl(interceptor, AUTH_BASE_URL + "/accounts:lookup");
+  }
+
+  @Test
+  public void testVerifySessionCookieWithCheckRevokedFailure() {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(AUTH_EXCEPTION);
+    TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier);
+    setUserManager(builder);
+    TenantAwareFirebaseAuth auth = builder.build();
+
+    try {
+      auth.verifySessionCookie("cookie", true);
+      fail("No error thrown for invalid token");
+    } catch (FirebaseAuthException e) {
+      assertEquals("code", e.getErrorCode());
+      assertEquals("cookie", tokenVerifier.getLastTokenString());
+    }
+  }
+
+  @Test
+  public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws InterruptedException {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(AUTH_EXCEPTION);
+    TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier);
+    setUserManager(builder);
+    TenantAwareFirebaseAuth auth = builder.build();
+
+    try {
+      auth.verifySessionCookieAsync("cookie", true).get();
+      fail("No error thrown for invalid token");
+    } catch (ExecutionException e) {
+      FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+      assertEquals("code", authException.getErrorCode());
+      assertEquals("cookie", tokenVerifier.getLastTokenString());
+    }
+  }
+
+  private static TenantAwareFirebaseAuth.Builder builderForTokenVerification(
+      MockTokenVerifier verifier) {
+    return TenantAwareFirebaseAuth.builder()
+        .setTenantId(TENANT_ID)
+        .setIdTokenVerifier(Suppliers.ofInstance(verifier))
+        .setCookieVerifier(Suppliers.ofInstance(verifier));
+  }
+
+  private static TestResponseInterceptor setUserManager(TenantAwareFirebaseAuth.Builder builder) {
+    return setUserManager(builder, "{}");
+  }
+
+  private static TestResponseInterceptor setUserManager(
+      TenantAwareFirebaseAuth.Builder builder, String response) {
+    FirebaseApp app = initializeAppWithResponse(response);
+    builder.setFirebaseApp(app);
+    return FirebaseAuthTest.setUserManager(builder, app, TENANT_ID);
+  }
+
+  private static FirebaseApp initializeAppWithResponse(String response) {
+    MockHttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent(response))
+        .build();
+    return FirebaseApp.initializeApp(new FirebaseOptions.Builder()
+        .setCredentials(new MockGoogleCredentials("token"))
+        .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 Utils.getDefaultJsonFactory().fromString(
+        new String(out.toByteArray()), GenericJson.class);
+  }
+
+  private static void checkUrl(TestResponseInterceptor interceptor, String url) {
+    HttpRequest request = interceptor.getResponse().getRequest();
+    assertEquals(HttpMethods.POST, request.getRequestMethod());
+    assertEquals(url, request.getUrl().getRawPath());
+  }
+}
diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java
index f939c83..c7e1cc2 100644
--- a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java
+++ b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java
@@ -340,6 +340,17 @@ public class InstanceIdClientImplTest {
     new TopicManagementResponse(ImmutableList.<GenericJson>of());
   }
 
+  @Test
+  public void testTopicManagementResponseErrorToString() {
+    GenericJson json = new GenericJson().set("error", "test error");
+    ImmutableList<GenericJson> jsonList = ImmutableList.of(json);
+
+    TopicManagementResponse topicManagementResponse = new TopicManagementResponse(jsonList);
+
+    String expected = "[Error{index=0, reason=unknown-error}]";
+    assertEquals(expected, topicManagementResponse.getErrors().toString());
+  }
+
   private static InstanceIdClientImpl initInstanceIdClient(
       final MockLowLevelHttpResponse mockResponse,
       final HttpResponseInterceptor interceptor) {