Repository: firebase/firebase-admin-java
Pull request #467 (2020-08-05 22:51:10)

feat(auth): Support for creating tenant-scoped session cookies
* Upgraded some Maven plugins to latest to avoid some issues with Jacoco code coverage reporting in older versions.
* Cleaned up the `Builder` APIs in `FirebaseAuth` and `TenantAwareFirebaseAuth`.
* Moved session cookie creation and verification APIs to `AbstractFirebaseAuth` so they are inherited by both subclasses of this API.
* Implemented support for verifying tenant-scoped session cookies (by passing a tenant ID to the respective `FirebaseTokenVerifier`).
* Implemented support for creating tenant-scoped session cookies (by verifying the ID token locally which also matches tenant IDs) 

RELEASE NOTE: Exposing `createSessionCookie()` and `verifySessionCookie()` methods from the `TenantAwareFirebaseAuth` class.

hiranya911: Those are existing docs. We are just moving them from `FirebaseAuth` to `AbstractFirebaseAuth`. I'm also debating whether to merge this PR now or finish off the v7 work first. I'll figure it out next week.

Patch:
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);
   }


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());
+  }
+}