Repository: firebase/firebase-admin-java
Pull request #246 (2019-02-03 06:18:44)

Refactoring the FirebaseAuth Token Verification
The implementation of `FirebaseAuth` for the most part is a monolith without clear separation of concerns. It is currently tightly coupled with the `FirebaseTokenFactory`, `FirebaseTokenVerifier` and `FirebaseUserManager` concrete classes. Because of this it is difficult to make changes to the individual helper classes, and also it is difficult to properly unit test the `FirebaseAuth` API surface. This is evident from the lack of test coverage in the following classes/packages:

* com.google.firebase.auth 86.48%
* FirebaseAuth 69%
* FirebaseToken 49%
* FirebaseUserManager 89%

This is the first of a series of refactorings I've planned in order to make the implementation more modular and more testable. In this PR I'm mostly focusing on the token verification logic, while keeping user management and custom token creation intact. The key changes include:

1. `FirebaseTokenVerifier` is now an interface. This dependency is injected into the `FirebaseAuth` via a a builder. This makes it possible to test all token verification APIs by using a mock `FirebaseTokenVerifier` in unit tests.
2. `FirebaseTokenVerifierImpl` class provides the default implementation of `FirebaseTokenVerifier`. All uses of the `IdToken` API are handled by this class as implementation details. This encapsulation enables us to easily change how we parse/verify ID tokens in the future (if the need ever arises).
3. Token revocation checks are implemented as a separate implementation of the `FirebaseTokenVerifier` interface, and engaged as needed via the decorator pattern.
4. `FirebaseToken` implementation has been decoupled from the Google API client's `IdToken` implementation.
5. Using the Guava `Supplier<T>` interface to inject token verifier dependencies into `FirebaseAuth`. In addition to dependency injection, this provides us an elegant way to implement lazy loading as well.
6. Increased unit test coverage.

Test coverage numbers after this change:

* com.google.firebase.auth 92.31%
* FirebaseAuth 80% (we can cover the rest when we refactor token creation and user mgt)
* FirebaseToken 100%
* FirebaseUserManager 89% (unchanged in this PR)

hiranya911: Resolves #65 
hiranya911: Thanks @ashwinraghav. I've addressed your comments. Please take a look.
hiranya911: Lot of these methods used to have anonymous inner classes that wrap the arguments. Which is why they are marked `final` in most cases. But this is no longer the case. In any case, I've made the arguments to all private helper methods `final`.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e669a28..cea3b25 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Unreleased
 
+-
+
+# v6.8.0
+
 - [added] Implemented new `sendAll()` and `sendMulticast()` APIs in
   `FirebaseMessaging`.
 - [changed] Removed org.json dependency and replaced with com.google.code.gson.
diff --git a/pom.xml b/pom.xml
index 0fcfe20..27e137a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
 
     <groupId>com.google.firebase</groupId>
     <artifactId>firebase-admin</artifactId>
-    <version>6.7.1-SNAPSHOT</version>
+    <version>6.8.1-SNAPSHOT</version>
     <packaging>jar</packaging>
 
     <name>firebase-admin</name>
diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
index 843c42f..0e02f83 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
@@ -25,6 +25,8 @@ 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.common.base.Suppliers;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.ImplFirebaseTrampolines;
 import com.google.firebase.auth.FirebaseUserManager.EmailLinkType;
@@ -34,17 +36,15 @@ import com.google.firebase.auth.ListUsersPage.PageFactory;
 import com.google.firebase.auth.UserRecord.CreateRequest;
 import com.google.firebase.auth.UserRecord.UpdateRequest;
 import com.google.firebase.auth.internal.FirebaseTokenFactory;
-import com.google.firebase.auth.internal.FirebaseTokenVerifier;
-import com.google.firebase.auth.internal.KeyManagers;
 import com.google.firebase.internal.CallableOperation;
 import com.google.firebase.internal.FirebaseService;
 import com.google.firebase.internal.NonNull;
 import com.google.firebase.internal.Nullable;
+
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * This class is the entry point for all server-side Firebase Authentication actions.
@@ -56,40 +56,27 @@ import java.util.concurrent.atomic.AtomicReference;
  */
 public class FirebaseAuth {
 
+  private static final String SERVICE_ID = FirebaseAuth.class.getName();
+
   private static final String ERROR_CUSTOM_TOKEN = "ERROR_CUSTOM_TOKEN";
-  private static final String ERROR_INVALID_ID_TOKEN = "ERROR_INVALID_CREDENTIAL";
-  private static final String ERROR_INVALID_SESSION_COOKIE = "ERROR_INVALID_COOKIE";
 
-  private final Clock clock;
+  private final Object lock = new Object();
+  private final AtomicBoolean destroyed = new AtomicBoolean(false);
 
   private final FirebaseApp firebaseApp;
-  private final KeyManagers keyManagers;
-  private final String projectId;
+  private final Supplier<FirebaseTokenFactory> tokenFactory;
+  private final Supplier<? extends FirebaseTokenVerifier> idTokenVerifier;
+  private final Supplier<? extends FirebaseTokenVerifier> cookieVerifier;
   private final JsonFactory jsonFactory;
   private final FirebaseUserManager userManager;
-  private final AtomicReference<FirebaseTokenFactory> tokenFactory;
-  private final AtomicBoolean destroyed;
-  private final Object lock;
-
-  private FirebaseAuth(FirebaseApp firebaseApp) {
-    this(firebaseApp, KeyManagers.getDefault(firebaseApp, Clock.SYSTEM), Clock.SYSTEM);
-  }
 
-  /**
-   * Constructor for injecting a GooglePublicKeysManager, which is used to verify tokens are
-   * correctly signed. This should only be used for testing to override the default key manager.
-   */
-  @VisibleForTesting
-  FirebaseAuth(FirebaseApp firebaseApp, KeyManagers keyManagers, Clock clock) {
-    this.firebaseApp = checkNotNull(firebaseApp);
-    this.keyManagers = checkNotNull(keyManagers);
-    this.clock = checkNotNull(clock);
-    this.projectId = ImplFirebaseTrampolines.getProjectId(firebaseApp);
+  private FirebaseAuth(Builder builder) {
+    this.firebaseApp = checkNotNull(builder.firebaseApp);
+    this.tokenFactory = threadSafeMemoize(builder.tokenFactory);
+    this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier);
+    this.cookieVerifier = threadSafeMemoize(builder.cookieVerifier);
     this.jsonFactory = firebaseApp.getOptions().getJsonFactory();
     this.userManager = new FirebaseUserManager(firebaseApp);
-    this.tokenFactory = new AtomicReference<>(null);
-    this.destroyed = new AtomicBoolean(false);
-    this.lock = new Object();
   }
 
   /**
@@ -224,40 +211,23 @@ public class FirebaseAuth {
   private CallableOperation<FirebaseToken, FirebaseAuthException> verifySessionCookieOp(
       final String cookie, final boolean checkRevoked) {
     checkNotDestroyed();
-    checkState(!Strings.isNullOrEmpty(projectId),
-        "Must initialize FirebaseApp with a project ID to call verifySessionCookie()");
+    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 {
-        FirebaseTokenVerifier firebaseTokenVerifier =
-            FirebaseTokenVerifier.createSessionCookieVerifier(projectId, keyManagers, clock);
-        FirebaseToken firebaseToken;
-        try {
-          firebaseToken = FirebaseToken.parse(jsonFactory, cookie);
-        } catch (IOException e) {
-          throw new FirebaseAuthException(ERROR_INVALID_SESSION_COOKIE,
-              "Failed to parse cookie", e);
-        }
-        // This will throw a FirebaseAuthException with details on how the token is invalid.
-        firebaseTokenVerifier.verifyTokenAndSignature(firebaseToken.getToken());
-
-        if (checkRevoked) {
-          checkRevoked(firebaseToken, "session cookie",
-              FirebaseUserManager.SESSION_COOKIE_REVOKED_ERROR);
-        }
-        return firebaseToken;
+        return sessionCookieVerifier.verifyToken(cookie);
       }
     };
   }
 
-  private void checkRevoked(
-      FirebaseToken firebaseToken, String label, String errorCode) throws FirebaseAuthException {
-    String uid = firebaseToken.getUid();
-    UserRecord user = userManager.getUserById(uid);
-    long issuedAt = (long) firebaseToken.getClaims().get("iat");
-    if (user.getTokensValidAfterTimestamp() > issuedAt * 1000) {
-      throw new FirebaseAuthException(errorCode, "Firebase " + label + " revoked");
+  @VisibleForTesting
+  FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) {
+    FirebaseTokenVerifier verifier = cookieVerifier.get();
+    if (checkRevoked) {
+      verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager);
     }
+    return verifier;
   }
 
   /**
@@ -355,7 +325,7 @@ public class FirebaseAuth {
       final String uid, final Map<String, Object> developerClaims) {
     checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
-    final FirebaseTokenFactory tokenFactory = ensureTokenFactory();
+    final FirebaseTokenFactory tokenFactory = this.tokenFactory.get();
     return new CallableOperation<String, FirebaseAuthException>() {
       @Override
       public String execute() throws FirebaseAuthException {
@@ -369,29 +339,6 @@ public class FirebaseAuth {
     };
   }
 
-  private FirebaseTokenFactory ensureTokenFactory() {
-    FirebaseTokenFactory result = this.tokenFactory.get();
-    if (result == null) {
-      synchronized (lock) {
-        result = this.tokenFactory.get();
-        if (result == null) {
-          try {
-            result = FirebaseTokenFactory.fromApp(firebaseApp, clock);
-            this.tokenFactory.set(result);
-          } catch (IOException e) {
-            throw new IllegalStateException(
-                "Failed to initialize FirebaseTokenFactory. Make sure to initialize the SDK "
-                    + "with service account credentials or specify a service account "
-                    + "ID with iam.serviceAccounts.signBlob permission. Please refer to "
-                    + "https://firebase.google.com/docs/auth/admin/create-custom-tokens for more "
-                    + "details on creating custom tokens.", e);
-          }
-        }
-      }
-    }
-    return result;
-  }
-
   /**
    * Parses and verifies a Firebase ID Token.
    *
@@ -472,31 +419,24 @@ public class FirebaseAuth {
       final String token, final boolean checkRevoked) {
     checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(token), "ID token must not be null or empty");
-    checkArgument(!Strings.isNullOrEmpty(projectId),
-        "Must initialize FirebaseApp with a project ID to call verifyIdToken()");
+    final FirebaseTokenVerifier verifier = getIdTokenVerifier(checkRevoked);
     return new CallableOperation<FirebaseToken, FirebaseAuthException>() {
       @Override
       protected FirebaseToken execute() throws FirebaseAuthException {
-        FirebaseTokenVerifier firebaseTokenVerifier =
-            FirebaseTokenVerifier.createIdTokenVerifier(projectId, keyManagers, clock);
-        FirebaseToken firebaseToken;
-        try {
-          firebaseToken = FirebaseToken.parse(jsonFactory, token);
-        } catch (IOException e) {
-          throw new FirebaseAuthException(ERROR_INVALID_ID_TOKEN, "Failed to parse token", e);
-        }
-
-        // This will throw a FirebaseAuthException with details on how the token is invalid.
-        firebaseTokenVerifier.verifyTokenAndSignature(firebaseToken.getToken());
-
-        if (checkRevoked) {
-          checkRevoked(firebaseToken, "auth token", FirebaseUserManager.ID_TOKEN_REVOKED_ERROR);
-        }       
-        return firebaseToken;
+        return verifier.verifyToken(token);
       }
     };
   }
 
+  @VisibleForTesting
+  FirebaseTokenVerifier getIdTokenVerifier(boolean checkRevoked) {
+    FirebaseTokenVerifier verifier = idTokenVerifier.get();
+    if (checkRevoked) {
+      verifier = RevocationCheckDecorator.decorateIdTokenVerifier(verifier, userManager);
+    }
+    return verifier;
+  }
+
   /**
    * Revokes all refresh tokens for the specified user.
    *
@@ -705,13 +645,12 @@ public class FirebaseAuth {
    * @throws IllegalArgumentException If the specified page token is empty, or max results value
    *     is invalid.
    */
-  public ApiFuture<ListUsersPage> listUsersAsync(
-      @Nullable final String pageToken, final int maxResults) {
+  public ApiFuture<ListUsersPage> listUsersAsync(@Nullable String pageToken, int maxResults) {
     return listUsersOp(pageToken, maxResults).callAsync(firebaseApp);
   }
 
   private CallableOperation<ListUsersPage, FirebaseAuthException> listUsersOp(
-      @Nullable String pageToken, int maxResults) {
+      @Nullable final String pageToken, final int maxResults) {
     checkNotDestroyed();
     final PageFactory factory = new PageFactory(
         new DefaultUserSource(userManager, jsonFactory), maxResults, pageToken);
@@ -874,7 +813,7 @@ public class FirebaseAuth {
    *     {@link FirebaseAuthException}.
    * @throws IllegalArgumentException If the user ID string is null or empty.
    */
-  public ApiFuture<Void> deleteUserAsync(final String uid) {
+  public ApiFuture<Void> deleteUserAsync(String uid) {
     return deleteUserOp(uid).callAsync(firebaseApp);
   }
 
@@ -959,7 +898,7 @@ public class FirebaseAuth {
   }
 
   private CallableOperation<UserImportResult, FirebaseAuthException> importUsersOp(
-      List<ImportUserRecord> users, UserImportOptions options) {
+      final List<ImportUserRecord> users, final UserImportOptions options) {
     checkNotDestroyed();
     final UserImportRequest request = new UserImportRequest(users, options, jsonFactory);
     return new CallableOperation<UserImportResult, FirebaseAuthException>() {
@@ -1130,6 +1069,11 @@ public class FirebaseAuth {
             .callAsync(firebaseApp);
   }
 
+  @VisibleForTesting
+  FirebaseUserManager getUserManager() {
+    return this.userManager;
+  }
+
   private CallableOperation<String, FirebaseAuthException> generateEmailActionLinkOp(
           final EmailLinkType type, final String email, final ActionCodeSettings settings) {
     checkNotDestroyed();
@@ -1145,9 +1089,17 @@ public class FirebaseAuth {
     };
   }
 
-  @VisibleForTesting
-  FirebaseUserManager getUserManager() {
-    return this.userManager;
+  private <T> Supplier<T> threadSafeMemoize(final Supplier<T> supplier) {
+    checkNotNull(supplier);
+    return Suppliers.memoize(new Supplier<T>() {
+      @Override
+      public T get() {
+        synchronized (lock) {
+          checkNotDestroyed();
+          return supplier.get();
+        }
+      }
+    });
   }
 
   private void checkNotDestroyed() {
@@ -1163,12 +1115,72 @@ public class FirebaseAuth {
     }
   }
 
-  private static final String SERVICE_ID = FirebaseAuth.class.getName();
+  private static FirebaseAuth fromApp(final FirebaseApp app) {
+    return FirebaseAuth.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);
+          }
+        })
+        .build();
+  }
+
+  @VisibleForTesting
+  static Builder builder() {
+    return new Builder();
+  }
+
+  static class Builder {
+    private FirebaseApp firebaseApp;
+    private Supplier<FirebaseTokenFactory> tokenFactory;
+    private Supplier<? extends FirebaseTokenVerifier> idTokenVerifier;
+    private Supplier<? extends FirebaseTokenVerifier> cookieVerifier;
+
+    private Builder() { }
+
+    Builder setFirebaseApp(FirebaseApp firebaseApp) {
+      this.firebaseApp = firebaseApp;
+      return this;
+    }
+
+    Builder setTokenFactory(Supplier<FirebaseTokenFactory> tokenFactory) {
+      this.tokenFactory = tokenFactory;
+      return this;
+    }
+
+    Builder setIdTokenVerifier(Supplier<? extends FirebaseTokenVerifier> idTokenVerifier) {
+      this.idTokenVerifier = idTokenVerifier;
+      return this;
+    }
+
+    Builder setCookieVerifier(Supplier<? extends FirebaseTokenVerifier> cookieVerifier) {
+      this.cookieVerifier = cookieVerifier;
+      return this;
+    }
+
+    FirebaseAuth build() {
+      return new FirebaseAuth(this);
+    }
+  }
 
   private static class FirebaseAuthService extends FirebaseService<FirebaseAuth> {
 
     FirebaseAuthService(FirebaseApp app) {
-      super(SERVICE_ID, new FirebaseAuth(app));
+      super(SERVICE_ID, FirebaseAuth.fromApp(app));
     }
 
     @Override
diff --git a/src/main/java/com/google/firebase/auth/FirebaseToken.java b/src/main/java/com/google/firebase/auth/FirebaseToken.java
index 1726250..3d7b0b2 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseToken.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseToken.java
@@ -16,164 +16,64 @@
 
 package com.google.firebase.auth;
 
-import com.google.api.client.auth.openidconnect.IdToken;
-import com.google.api.client.json.JsonFactory;
-import com.google.api.client.json.webtoken.JsonWebSignature;
-import com.google.api.client.util.Key;
+import static com.google.common.base.Preconditions.checkArgument;
 
-import java.io.IOException;
+import com.google.common.collect.ImmutableMap;
 import java.util.Map;
 
 /**
- * Implementation of a Parsed Firebase Token returned by {@link FirebaseAuth#verifyIdToken(String)}.
- * It can used to get the uid and other attributes of the user provided in the Token.
+ * A decoded and verified Firebase token. Can be used to get the uid and other user attributes
+ * available in the token. See {@link FirebaseAuth#verifyIdToken(String)} and
+ * {@link FirebaseAuth#verifySessionCookie(String)} for details on how to obtain an instance of
+ * this class.
  */
 public final class FirebaseToken {
 
-  private final FirebaseTokenImpl token;
+  private final Map<String, Object> claims;
 
-  FirebaseToken(FirebaseTokenImpl token) {
-    this.token = token;
-  }
-
-  static FirebaseToken parse(JsonFactory jsonFactory, String tokenString) throws IOException {
-    try {
-      JsonWebSignature jws =
-          JsonWebSignature.parser(jsonFactory)
-              .setPayloadClass(FirebaseTokenImpl.Payload.class)
-              .parse(tokenString);
-      return new FirebaseToken(
-          new FirebaseTokenImpl(
-              jws.getHeader(),
-              (FirebaseTokenImpl.Payload) jws.getPayload(),
-              jws.getSignatureBytes(),
-              jws.getSignedContentBytes()));
-    } catch (IOException e) {
-      throw new IOException(
-          "Decoding Firebase ID token failed. Make sure you passed the entire string JWT "
-              + "which represents an ID token. See https://firebase.google.com/docs/auth/admin/"
-              + "verify-id-tokens for details on how to retrieve an ID token.",
-          e);
-    }
+  FirebaseToken(Map<String, Object> claims) {
+    checkArgument(claims != null && claims.containsKey("sub"),
+        "Claims map must at least contain sub");
+    this.claims = ImmutableMap.copyOf(claims);
   }
 
   /** Returns the Uid for the this token. */
   public String getUid() {
-    return token.getPayload().getSubject();
+    return (String) claims.get("sub");
   }
 
   /** Returns the Issuer for the this token. */
   public String getIssuer() {
-    return token.getPayload().getIssuer();
+    return (String) claims.get("iss");
   }
 
   /** Returns the user's display name. */
   public String getName() {
-    return token.getPayload().getName();
+    return (String) claims.get("name");
   }
 
   /** Returns the Uri string of the user's profile photo. */
   public String getPicture() {
-    return token.getPayload().getPicture();
+    return (String) claims.get("picture");
   }
 
   /** 
    * Returns the e-mail address for this user, or {@code null} if it's unavailable.
    */
   public String getEmail() {
-    return token.getPayload().getEmail();
+    return (String) claims.get("email");
   }
 
   /** 
    * Indicates if the email address returned by {@link #getEmail()} has been verified as good.
    */
   public boolean isEmailVerified() {
-    return token.getPayload().isEmailVerified();
+    Object emailVerified = claims.get("email_verified");
+    return emailVerified != null && (Boolean) emailVerified;
   }
 
   /** Returns a map of all of the claims on this token. */
   public Map<String, Object> getClaims() {
-    return token.getPayload();
-  }
-
-  FirebaseTokenImpl getToken() {
-    return token;
-  }
-
-  static class FirebaseTokenImpl extends IdToken {
-
-    FirebaseTokenImpl(
-        Header header, Payload payload, byte[] signatureBytes, byte[] signedContentBytes) {
-      super(header, payload, signatureBytes, signedContentBytes);
-    }
-
-    @Override
-    public Payload getPayload() {
-      return (Payload) super.getPayload();
-    }
-
-    /** Represents a FirebaseWebToken Payload. */
-    public static class Payload extends IdToken.Payload {
-
-      /**
-       * Timestamp of the last time this user authenticated with Firebase on the device receiving
-       * this token.
-       */
-      @Key("auth_time")
-      private long authTime;
-
-      /** User's primary email address. */
-      @Key private String email;
-
-      /** Indicates whether or not the e-mail field is verified to be a known-good address. */
-      @Key("email_verified")
-      private boolean emailVerified;
-
-      /** User's Display Name. */
-      @Key private String name;
-
-      /** URI of the User's profile picture. */
-      @Key private String picture;
-
-      /**
-       * Returns the UID of the user represented by this token. This is an alias for {@link
-       * #getSubject()}
-       */
-      public String getUid() {
-        return getSubject();
-      }
-
-      /**
-       * Returns the time in seconds from the Unix Epoch that this user last authenticated with
-       * Firebase on this device.
-       */
-      public long getAuthTime() {
-        return authTime;
-      }
-
-      /** 
-       * Returns the e-mail address for this user, or {@code null} if it's unavailable.
-       */
-      public String getEmail() {
-        return email;
-      }
-
-      /**
-       * Indicates if the email address returned by {@link #getEmail()} has been verified as good.
-       */
-      public boolean isEmailVerified() {
-        return emailVerified;
-      }
-
-      /** Returns the user's display name. */
-      public String getName() {
-        return name;
-      }
-
-      /** Returns the Uri string of the user's profile photo. */
-      public String getPicture() {
-        return picture;
-      }
-    }
+    return this.claims;
   }
 }
diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java
new file mode 100644
index 0000000..dbb5628
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright  2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.api.client.auth.openidconnect.IdTokenVerifier;
+import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.api.client.util.Clock;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import com.google.firebase.ImplFirebaseTrampolines;
+import com.google.firebase.auth.internal.CryptoSigners;
+import com.google.firebase.auth.internal.FirebaseTokenFactory;
+
+import java.io.IOException;
+
+final class FirebaseTokenUtils {
+
+  private static final String ID_TOKEN_CERT_URL =
+      "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com";
+  private static final String ID_TOKEN_ISSUER_PREFIX = "https://securetoken.google.com/";
+
+  private static final String SESSION_COOKIE_CERT_URL =
+      "https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys";
+  private static final String SESSION_COOKIE_ISSUER_PREFIX = "https://session.firebase.google.com/";
+
+  // The default JsonFactory implementation we get from Google API client does not support parsing
+  // JSON strings with control characters in text. The public key certificates we get from Google
+  // auth servers contain some control characters, and therefore we must use a JsonFactory that is
+  // capable of parsing such text.
+  static final JsonFactory UNQUOTED_CTRL_CHAR_JSON_FACTORY = new GsonFactory();
+
+  private FirebaseTokenUtils() { }
+
+  static FirebaseTokenFactory createTokenFactory(FirebaseApp firebaseApp, Clock clock) {
+    try {
+      return new FirebaseTokenFactory(
+          firebaseApp.getOptions().getJsonFactory(),
+          clock,
+          CryptoSigners.getCryptoSigner(firebaseApp));
+    } catch (IOException e) {
+      throw new IllegalStateException(
+          "Failed to initialize FirebaseTokenFactory. Make sure to initialize the SDK "
+              + "with service account credentials or specify a service account "
+              + "ID with iam.serviceAccounts.signBlob permission. Please refer to "
+              + "https://firebase.google.com/docs/auth/admin/create-custom-tokens for more "
+              + "details on creating custom tokens.", e);
+    }
+  }
+
+  static FirebaseTokenVerifierImpl createIdTokenVerifier(FirebaseApp app, Clock clock) {
+    String projectId = ImplFirebaseTrampolines.getProjectId(app);
+    checkState(!Strings.isNullOrEmpty(projectId),
+        "Must initialize FirebaseApp with a project ID to call verifyIdToken()");
+    IdTokenVerifier idTokenVerifier = newIdTokenVerifier(
+        clock, ID_TOKEN_ISSUER_PREFIX, projectId);
+    GooglePublicKeysManager publicKeysManager = newPublicKeysManager(
+        app.getOptions(), clock, ID_TOKEN_CERT_URL);
+    return FirebaseTokenVerifierImpl.builder()
+        .setShortName("ID token")
+        .setMethod("verifyIdToken()")
+        .setDocUrl("https://firebase.google.com/docs/auth/admin/verify-id-tokens")
+        .setJsonFactory(app.getOptions().getJsonFactory())
+        .setPublicKeysManager(publicKeysManager)
+        .setIdTokenVerifier(idTokenVerifier)
+        .build();
+  }
+
+  static FirebaseTokenVerifierImpl createSessionCookieVerifier(FirebaseApp app, Clock clock) {
+    String projectId = ImplFirebaseTrampolines.getProjectId(app);
+    checkState(!Strings.isNullOrEmpty(projectId),
+        "Must initialize FirebaseApp with a project ID to call verifySessionCookie()");
+    IdTokenVerifier idTokenVerifier = newIdTokenVerifier(
+        clock, SESSION_COOKIE_ISSUER_PREFIX, projectId);
+    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")
+        .build();
+  }
+
+  private static GooglePublicKeysManager newPublicKeysManager(
+      FirebaseOptions options, Clock clock, String certUrl) {
+    return new GooglePublicKeysManager.Builder(
+        options.getHttpTransport(), UNQUOTED_CTRL_CHAR_JSON_FACTORY)
+        .setClock(clock)
+        .setPublicCertsEncodedUrl(certUrl)
+        .build();
+  }
+
+  private static IdTokenVerifier newIdTokenVerifier(
+      Clock clock, String issuerPrefix, String projectId) {
+    return new IdTokenVerifier.Builder()
+        .setClock(clock)
+        .setAudience(ImmutableList.of(projectId))
+        .setIssuer(issuerPrefix + projectId)
+        .build();
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifier.java b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifier.java
new file mode 100644
index 0000000..e73f823
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifier.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright  2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+/**
+ * An interface for verifying Firebase token strings.  Exists mainly to facilitate easy testing
+ * and extension/decoration of the token verification functionality.
+ */
+interface FirebaseTokenVerifier {
+
+  /**
+   * Verifies that the given token string is a valid Firebase JWT.
+   *
+   * @param token The token string to be verified.
+   * @return A decoded representation of the input token string.
+   * @throws FirebaseAuthException If the input token string fails to verify due to any reason.
+   */
+  FirebaseToken verifyToken(String token) throws FirebaseAuthException;
+
+}
diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java
new file mode 100644
index 0000000..1da2d7a
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright  2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.api.client.auth.openidconnect.IdToken;
+import com.google.api.client.auth.openidconnect.IdToken.Payload;
+import com.google.api.client.auth.openidconnect.IdTokenVerifier;
+import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.webtoken.JsonWebSignature.Header;
+import com.google.api.client.util.ArrayMap;
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.security.GeneralSecurityException;
+import java.security.PublicKey;
+
+/**
+ * The default implementation of the {@link FirebaseTokenVerifier} interface. Uses the Google API
+ * client's {@code IdToken} API to decode and verify token strings. Can be customized to verify
+ * both Firebase ID tokens and session cookies.
+ */
+final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
+
+  private static final String RS256 = "RS256";
+  private static final String FIREBASE_AUDIENCE =
+      "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit";
+  private static final String ERROR_INVALID_CREDENTIAL = "ERROR_INVALID_CREDENTIAL";
+  private static final String ERROR_RUNTIME_EXCEPTION = "ERROR_RUNTIME_EXCEPTION";
+
+  private final JsonFactory jsonFactory;
+  private final GooglePublicKeysManager publicKeysManager;
+  private final IdTokenVerifier idTokenVerifier;
+  private final String method;
+  private final String shortName;
+  private final String articledShortName;
+  private final String docUrl;
+
+  private FirebaseTokenVerifierImpl(Builder builder) {
+    this.jsonFactory = checkNotNull(builder.jsonFactory);
+    this.publicKeysManager = checkNotNull(builder.publicKeysManager);
+    this.idTokenVerifier = checkNotNull(builder.idTokenVerifier);
+    checkArgument(!Strings.isNullOrEmpty(builder.method), "method name must be specified");
+    checkArgument(!Strings.isNullOrEmpty(builder.shortName), "shortName must be specified");
+    checkArgument(!Strings.isNullOrEmpty(builder.docUrl), "docUrl must be specified");
+    this.method = builder.method;
+    this.shortName = builder.shortName;
+    this.articledShortName = prefixWithIndefiniteArticle(this.shortName);
+    this.docUrl = builder.docUrl;
+  }
+
+  /**
+   * Verifies that the given token string is a valid Firebase JWT. This implementation considers
+   * a token string to be valid if all the following conditions are met:
+   * <ol>
+   *   <li>The token string is a valid RS256 JWT.</li>
+   *   <li>The JWT contains a valid key ID (kid) claim.</li>
+   *   <li>The JWT is not expired, and it has been issued some time in the past.</li>
+   *   <li>The JWT contains valid issuer (iss) and audience (aud) claims as determined by the
+   *   {@code IdTokenVerifier}.</li>
+   *   <li>The JWT contains a valid subject (sub) claim.</li>
+   *   <li>The JWT is signed by a Firebase Auth backend server.</li>
+   * </ol>
+   *
+   * @param token The token string to be verified.
+   * @return A decoded representation of the input token string.
+   * @throws FirebaseAuthException If the input token string does not meet any of the conditions
+   *     listed above.
+   */
+  @Override
+  public FirebaseToken verifyToken(String token) throws FirebaseAuthException {
+    IdToken idToken = parse(token);
+    checkContents(idToken);
+    checkSignature(idToken);
+    return new FirebaseToken(idToken.getPayload());
+  }
+
+  GooglePublicKeysManager getPublicKeysManager() {
+    return publicKeysManager;
+  }
+
+  IdTokenVerifier getIdTokenVerifier() {
+    return idTokenVerifier;
+  }
+
+  String getMethod() {
+    return method;
+  }
+
+  String getShortName() {
+    return shortName;
+  }
+
+  String getArticledShortName() {
+    return articledShortName;
+  }
+
+  String getDocUrl() {
+    return docUrl;
+  }
+
+  private String prefixWithIndefiniteArticle(String word) {
+    if ("aeiouAEIOU".indexOf(word.charAt(0)) < 0) {
+      return "a " + word;
+    } else {
+      return "an " + word;
+    }
+  }
+
+  private IdToken parse(String token) throws FirebaseAuthException {
+    try {
+      return IdToken.parse(jsonFactory, token);
+    } catch (IOException e) {
+      String detailedError = String.format(
+          "Failed to parse Firebase %s. Make sure you passed a string that represents a complete "
+              + "and valid JWT. See %s for details on how to retrieve %s.",
+          shortName,
+          docUrl,
+          articledShortName);
+      throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, detailedError, e);
+    }
+  }
+
+  private void checkContents(final IdToken token) throws FirebaseAuthException {
+    String errorMessage = getErrorIfContentInvalid(token);
+    if (errorMessage != null) {
+      String detailedError = String.format("%s %s", errorMessage, getVerifyTokenMessage());
+      throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, detailedError);
+    }
+  }
+
+  private void checkSignature(IdToken token) throws FirebaseAuthException {
+    try {
+      if (!isSignatureValid(token)) {
+        throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL,
+            String.format(
+                "Failed to verify the signature of Firebase %s. %s",
+                shortName,
+                getVerifyTokenMessage()));
+      }
+    } catch (GeneralSecurityException | IOException e) {
+      throw new FirebaseAuthException(
+          ERROR_RUNTIME_EXCEPTION, "Error while verifying signature.", e);
+    }
+  }
+
+  private String getErrorIfContentInvalid(final IdToken idToken) {
+    final Header header = idToken.getHeader();
+    final Payload payload = idToken.getPayload();
+
+    String errorMessage = null;
+    if (header.getKeyId() == null) {
+      errorMessage = getErrorForTokenWithoutKid(header, payload);
+    } else if (!RS256.equals(header.getAlgorithm())) {
+      errorMessage = String.format(
+          "Firebase %s has incorrect algorithm. Expected \"%s\" but got \"%s\".",
+          shortName,
+          RS256,
+          header.getAlgorithm());
+    } else if (!idToken.verifyAudience(idTokenVerifier.getAudience())) {
+      errorMessage = String.format(
+          "Firebase %s has incorrect \"aud\" (audience) claim. Expected \"%s\" but got \"%s\". %s",
+          shortName,
+          joinWithComma(idTokenVerifier.getAudience()),
+          joinWithComma(payload.getAudienceAsList()),
+          getProjectIdMatchMessage());
+    } else if (!idToken.verifyIssuer(idTokenVerifier.getIssuers())) {
+      errorMessage = String.format(
+          "Firebase %s has incorrect \"iss\" (issuer) claim. Expected \"%s\" but got \"%s\". %s",
+          shortName,
+          joinWithComma(idTokenVerifier.getIssuers()),
+          payload.getIssuer(),
+          getProjectIdMatchMessage());
+    } else if (payload.getSubject() == null) {
+      errorMessage = String.format(
+          "Firebase %s has no \"sub\" (subject) claim.",
+          shortName);
+    } else if (payload.getSubject().isEmpty()) {
+      errorMessage = String.format(
+          "Firebase %s has an empty string \"sub\" (subject) claim.",
+          shortName);
+    } else if (payload.getSubject().length() > 128) {
+      errorMessage = String.format(
+          "Firebase %s has \"sub\" (subject) claim longer than 128 characters.",
+          shortName);
+    } else if (!verifyTimestamps(idToken)) {
+      errorMessage = String.format(
+          "Firebase %s has expired or is not yet valid. Get a fresh %s and try again.",
+          shortName,
+          shortName);
+    }
+
+    return errorMessage;
+  }
+
+  private String getVerifyTokenMessage() {
+    return String.format(
+        "See %s for details on how to retrieve %s.",
+        docUrl,
+        articledShortName);
+  }
+
+  /**
+   * Verifies the cryptographic signature on the FirebaseToken. Can block on a web request to fetch
+   * the keys if they have expired.
+   */
+  private boolean isSignatureValid(IdToken token) throws GeneralSecurityException, IOException {
+    for (PublicKey key : publicKeysManager.getPublicKeys()) {
+      if (token.verifySignature(key)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private String getErrorForTokenWithoutKid(IdToken.Header header, IdToken.Payload payload) {
+    if (isCustomToken(payload)) {
+      return String.format("%s expects %s, but was given a custom token.",
+          method, articledShortName);
+    } else if (isLegacyCustomToken(header, payload)) {
+      return String.format("%s expects %s, but was given a legacy custom token.",
+          method, articledShortName);
+    }
+    return String.format("Firebase %s has no \"kid\" claim.", shortName);
+  }
+
+  private String joinWithComma(Iterable<String> strings) {
+    return Joiner.on(',').join(strings);
+  }
+
+  private String getProjectIdMatchMessage() {
+    return String.format(
+        "Make sure the %s comes from the same Firebase project as the service account used to "
+            + "authenticate this SDK.",
+        shortName);
+  }
+
+  private boolean verifyTimestamps(IdToken token) {
+    long currentTimeMillis = idTokenVerifier.getClock().currentTimeMillis();
+    return token.verifyTime(currentTimeMillis, idTokenVerifier.getAcceptableTimeSkewSeconds());
+  }
+
+  private boolean isCustomToken(IdToken.Payload payload) {
+    return FIREBASE_AUDIENCE.equals(payload.getAudience());
+  }
+
+  private boolean isLegacyCustomToken(IdToken.Header header, IdToken.Payload payload) {
+    return "HS256".equals(header.getAlgorithm())
+        && new BigDecimal(0).equals(payload.get("v"))
+        && containsLegacyUidField(payload);
+  }
+
+  private boolean containsLegacyUidField(IdToken.Payload payload) {
+    Object dataField = payload.get("d");
+    if (dataField instanceof ArrayMap) {
+      return ((ArrayMap) dataField).get("uid") != null;
+    }
+    return false;
+  }
+
+  static Builder builder() {
+    return new Builder();
+  }
+
+  static final class Builder {
+
+    private JsonFactory jsonFactory;
+    private GooglePublicKeysManager publicKeysManager;
+    private String method;
+    private String shortName;
+    private IdTokenVerifier idTokenVerifier;
+    private String docUrl;
+
+    private Builder() { }
+
+    Builder setJsonFactory(JsonFactory jsonFactory) {
+      this.jsonFactory = jsonFactory;
+      return this;
+    }
+
+    Builder setPublicKeysManager(GooglePublicKeysManager publicKeysManager) {
+      this.publicKeysManager = publicKeysManager;
+      return this;
+    }
+
+    Builder setMethod(String method) {
+      this.method = method;
+      return this;
+    }
+
+    Builder setShortName(String shortName) {
+      this.shortName = shortName;
+      return this;
+    }
+
+    Builder setIdTokenVerifier(IdTokenVerifier idTokenVerifier) {
+      this.idTokenVerifier = idTokenVerifier;
+      return this;
+    }
+
+    Builder setDocUrl(String docUrl) {
+      this.docUrl = docUrl;
+      return this;
+    }
+
+    FirebaseTokenVerifierImpl build() {
+      return new FirebaseTokenVerifierImpl(this);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
index 7f534fd..8298499 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -65,8 +65,6 @@ class FirebaseUserManager {
 
   static final String USER_NOT_FOUND_ERROR = "user-not-found";
   static final String INTERNAL_ERROR = "internal-error";
-  static final String ID_TOKEN_REVOKED_ERROR = "id-token-revoked";
-  static final String SESSION_COOKIE_REVOKED_ERROR = "session-cookie-revoked";
 
   // Map of server-side error codes to SDK error codes.
   // SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors
diff --git a/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java b/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java
new file mode 100644
index 0000000..e53ad25
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright  2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Strings;
+
+/**
+ * A decorator for adding token revocation checks to an existing {@link FirebaseTokenVerifier}.
+ */
+class RevocationCheckDecorator implements FirebaseTokenVerifier {
+
+  static final String ID_TOKEN_REVOKED_ERROR = "id-token-revoked";
+  static final String SESSION_COOKIE_REVOKED_ERROR = "session-cookie-revoked";
+
+  private final FirebaseTokenVerifier tokenVerifier;
+  private final FirebaseUserManager userManager;
+  private final String errorCode;
+  private final String shortName;
+
+  private RevocationCheckDecorator(
+      FirebaseTokenVerifier tokenVerifier,
+      FirebaseUserManager userManager,
+      String errorCode,
+      String shortName) {
+    this.tokenVerifier = checkNotNull(tokenVerifier);
+    this.userManager = checkNotNull(userManager);
+    checkArgument(!Strings.isNullOrEmpty(errorCode));
+    checkArgument(!Strings.isNullOrEmpty(shortName));
+    this.errorCode = errorCode;
+    this.shortName = shortName;
+  }
+
+  /**
+   * If the wrapped {@link FirebaseTokenVerifier} deems the input token string is valid, checks
+   * whether the token has been revoked.
+   */
+  @Override
+  public FirebaseToken verifyToken(String token) throws FirebaseAuthException {
+    FirebaseToken firebaseToken = tokenVerifier.verifyToken(token);
+    if (isRevoked(firebaseToken)) {
+      throw new FirebaseAuthException(errorCode, "Firebase " + shortName + " revoked");
+    }
+    return firebaseToken;
+  }
+
+  private boolean isRevoked(FirebaseToken firebaseToken) throws FirebaseAuthException {
+    UserRecord user = userManager.getUserById(firebaseToken.getUid());
+    long issuedAtInSeconds = (long) firebaseToken.getClaims().get("iat");
+    return user.getTokensValidAfterTimestamp() > issuedAtInSeconds * 1000;
+  }
+
+  static RevocationCheckDecorator decorateIdTokenVerifier(
+      FirebaseTokenVerifier tokenVerifier, FirebaseUserManager userManager) {
+    return new RevocationCheckDecorator(
+        tokenVerifier, userManager, ID_TOKEN_REVOKED_ERROR, "id token");
+  }
+
+  static RevocationCheckDecorator decorateSessionCookieVerifier(
+      FirebaseTokenVerifier tokenVerifier, FirebaseUserManager userManager) {
+    return new RevocationCheckDecorator(
+        tokenVerifier, userManager, SESSION_COOKIE_REVOKED_ERROR, "session cookie");
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java
index 3447f05..6ea70b8 100644
--- a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java
+++ b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java
@@ -31,7 +31,7 @@ import java.util.Map;
 /**
  * A set of {@link CryptoSigner} implementations and utilities for interacting with them.
  */
-class CryptoSigners {
+public class CryptoSigners {
 
   private static final String METADATA_SERVICE_URL =
       "http://metadata/computeMetadata/v1/instance/service-accounts/default/email";
@@ -128,7 +128,7 @@ class CryptoSigners {
    * Initializes a {@link CryptoSigner} instance for the given Firebase app. Follows the protocol
    * documented at go/firebase-admin-sign.
    */
-  static CryptoSigner getCryptoSigner(FirebaseApp firebaseApp) throws IOException {
+  public static CryptoSigner getCryptoSigner(FirebaseApp firebaseApp) throws IOException {
     GoogleCredentials credentials = ImplFirebaseTrampolines.getCredentials(firebaseApp);
 
     // If the SDK was initialized with a service account, use it to sign bytes.
diff --git a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java
index 72ef917..95d3131 100644
--- a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java
+++ b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java
@@ -27,7 +27,6 @@ import com.google.api.client.util.Clock;
 import com.google.api.client.util.StringUtils;
 
 import com.google.common.base.Strings;
-import com.google.firebase.FirebaseApp;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Map;
@@ -43,7 +42,7 @@ public class FirebaseTokenFactory {
   private final Clock clock;
   private final CryptoSigner signer;
 
-  FirebaseTokenFactory(JsonFactory jsonFactory, Clock clock, CryptoSigner signer) {
+  public FirebaseTokenFactory(JsonFactory jsonFactory, Clock clock, CryptoSigner signer) {
     this.jsonFactory = checkNotNull(jsonFactory);
     this.clock = checkNotNull(clock);
     this.signer = checkNotNull(signer);
@@ -94,12 +93,4 @@ public class FirebaseTokenFactory {
     String signature = Base64.encodeBase64URLSafeString(signer.sign(contentBytes));
     return content + "." + signature;
   }
-
-  public static FirebaseTokenFactory fromApp(
-      FirebaseApp firebaseApp, Clock clock) throws IOException {
-    return new FirebaseTokenFactory(
-        firebaseApp.getOptions().getJsonFactory(),
-        clock,
-        CryptoSigners.getCryptoSigner(firebaseApp));
-  }
 }
diff --git a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenVerifier.java b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenVerifier.java
deleted file mode 100644
index 98b269f..0000000
--- a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenVerifier.java
+++ /dev/null
@@ -1,335 +0,0 @@
-/*
- * Copyright 2017 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.firebase.auth.internal;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import com.google.api.client.auth.openidconnect.IdToken;
-import com.google.api.client.auth.openidconnect.IdToken.Payload;
-import com.google.api.client.auth.openidconnect.IdTokenVerifier;
-import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
-import com.google.api.client.json.webtoken.JsonWebSignature.Header;
-import com.google.api.client.util.ArrayMap;
-import com.google.api.client.util.Clock;
-import com.google.common.base.Strings;
-import com.google.firebase.auth.FirebaseAuthException;
-
-import com.google.firebase.internal.NonNull;
-import java.io.IOException;
-import java.math.BigDecimal;
-import java.security.GeneralSecurityException;
-import java.security.PublicKey;
-import java.util.Collection;
-import java.util.Collections;
-
-/**
- * Verifies that a JWT returned by Firebase is valid for use in the this project.
- *
- * <p>This class should be kept as a Singleton within the server in order to maximize caching of the
- * public signing keys.
- */
-public final class FirebaseTokenVerifier extends IdTokenVerifier {
-
-  static final String ID_TOKEN_CERT_URL =
-      "https://www.googleapis.com/robot/v1/metadata/x509/"
-          + "securetoken@system.gserviceaccount.com";
-  static final String ID_TOKEN_ISSUER_PREFIX = "https://securetoken.google.com/";
-
-  static final String SESSION_COOKIE_CERT_URL =
-      "https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys";
-  static final String SESSION_COOKIE_ISSUER_PREFIX = "https://session.firebase.google.com/";
-
-  private static final String FIREBASE_AUDIENCE =
-      "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit";
-
-  private static final String ERROR_INVALID_CREDENTIAL = "ERROR_INVALID_CREDENTIAL";
-  private static final String ERROR_RUNTIME_EXCEPTION = "ERROR_RUNTIME_EXCEPTION";
-  private static final String PROJECT_ID_MATCH_MESSAGE =
-      "Make sure the %s comes from the same Firebase project as the service account used to "
-          + "authenticate this SDK.";
-  private static final String VERIFY_TOKEN_DOCS_MESSAGE =
-      "See %s for details on how to retrieve %s.";
-  private static final String ALGORITHM = "RS256";
-
-  private final String projectId;
-  private final GooglePublicKeysManager publicKeysManager;
-  private final String method;
-  private final String shortName;
-  private final String articledShortName;
-
-  private final String projectIdMatchMessage;
-  private final String verifyTokenMessage;
-
-  private FirebaseTokenVerifier(Builder builder) {
-    super(builder);
-    checkArgument(!Strings.isNullOrEmpty(builder.projectId), "projectId must be set");
-    checkArgument(!Strings.isNullOrEmpty(builder.shortName), "shortName must be set");
-    checkArgument(!Strings.isNullOrEmpty(builder.method), "method must be set");
-    this.projectId = builder.projectId;
-    this.shortName = builder.shortName;
-    this.articledShortName = prefixWithIndefiniteArticle(shortName);
-    this.method = builder.method;
-    this.publicKeysManager = checkNotNull(builder.publicKeysManager,
-        "publicKeysManager must be set");
-    this.projectIdMatchMessage = String.format(PROJECT_ID_MATCH_MESSAGE, shortName);
-    this.verifyTokenMessage = String.format(VERIFY_TOKEN_DOCS_MESSAGE, builder.docUrl,
-        articledShortName);
-  }
-
-  /**
-   * We are changing the semantics of the super-class method in order to provide more details on why
-   * this is failing to the developer.
-   */
-  public void verifyTokenAndSignature(IdToken token) throws FirebaseAuthException {
-    Payload payload = token.getPayload();
-    Header header = token.getHeader();
-    String errorMessage = null;
-
-    boolean isCustomToken =
-        payload.getAudience() != null && payload.getAudience().equals(FIREBASE_AUDIENCE);
-    boolean isLegacyCustomToken =
-        header.getAlgorithm() != null
-            && header.getAlgorithm().equals("HS256")
-            && payload.get("v") != null
-            && payload.get("v").equals(new BigDecimal(0))
-            && payload.get("d") != null
-            && payload.get("d") instanceof ArrayMap
-            && ((ArrayMap) payload.get("d")).get("uid") != null;
-
-    if (header.getKeyId() == null) {
-      if (isCustomToken) {
-        errorMessage = String.format("%s expects %s, but was given a custom token.",
-            method, articledShortName);
-      } else if (isLegacyCustomToken) {
-        errorMessage = String.format("%s expects %s, but was given a legacy custom token.",
-            method, articledShortName);
-      } else {
-        errorMessage = String.format("Firebase %s has no \"kid\" claim.", shortName);
-      }
-    } else if (header.getAlgorithm() == null || !header.getAlgorithm().equals(ALGORITHM)) {
-      errorMessage =
-          String.format(
-              "Firebase %s has incorrect algorithm. Expected \"%s\" but got \"%s\".",
-              shortName, ALGORITHM, header.getAlgorithm());
-    } else if (!token.verifyAudience(getAudience())) {
-      errorMessage =
-          String.format(
-              "Firebase %s has incorrect \"aud\" (audience) claim. Expected \"%s\" but got "
-                  + "\"%s\". %s", shortName, concat(getAudience()),
-              concat(token.getPayload().getAudienceAsList()), projectIdMatchMessage);
-    } else if (!token.verifyIssuer(getIssuers())) {
-      errorMessage =
-          String.format(
-              "Firebase %s has incorrect \"iss\" (issuer) claim. "
-                  + "Expected \"%s\" but got \"%s\". %s", shortName, concat(getIssuers()),
-              token.getPayload().getIssuer(), projectIdMatchMessage);
-    } else if (payload.getSubject() == null) {
-      errorMessage = String.format("Firebase %s has no \"sub\" (subject) claim.", shortName);
-    } else if (payload.getSubject().isEmpty()) {
-      errorMessage = String.format("Firebase %s has an empty string \"sub\" (subject) claim.",
-          shortName);
-    } else if (payload.getSubject().length() > 128) {
-      errorMessage = String.format("Firebase %s has \"sub\" (subject) claim longer than "
-          + "128 characters.", shortName);
-    } else if (!token.verifyTime(getClock().currentTimeMillis(), getAcceptableTimeSkewSeconds())) {
-      errorMessage =
-          String.format("Firebase %s has expired or is not yet valid. Get a fresh %s and "
-              + "try again.", shortName, shortName);
-    }
-
-    if (errorMessage != null) {
-      throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL,
-          String.format("%s %s", errorMessage, verifyTokenMessage));
-    }
-
-    try {
-      if (!verifySignature(token)) {
-        throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL,
-            String.format("Firebase %s isn't signed by a valid public key. %s",
-                shortName, verifyTokenMessage));
-      }
-    } catch (IOException | GeneralSecurityException e) {
-      throw new FirebaseAuthException(
-          ERROR_RUNTIME_EXCEPTION, "Error while verifying signature.", e);
-    }
-  }
-
-  private String prefixWithIndefiniteArticle(String word) {
-    if ("aeiouAEIOU".indexOf(word.charAt(0)) < 0) {
-      return "a " + word;
-    } else {
-      return "an " + word;
-    }
-  }
-
-  private String concat(Collection<String> collection) {
-    StringBuilder stringBuilder = new StringBuilder();
-    for (String inputLine : collection) {
-      stringBuilder.append(inputLine.trim()).append(", ");
-    }
-    return stringBuilder.substring(0, stringBuilder.length() - 2);
-  }
-
-  /**
-   * Verifies the cryptographic signature on the FirebaseToken. Can block on a web request to fetch
-   * the keys if they have expired.
-   */
-  private boolean verifySignature(IdToken token) throws GeneralSecurityException, IOException {
-    for (PublicKey key : publicKeysManager.getPublicKeys()) {
-      if (token.verifySignature(key)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  public String getProjectId() {
-    return projectId;
-  }
-
-  /** 
-   * Builder for {@link FirebaseTokenVerifier}.
-   */
-  public static class Builder extends IdTokenVerifier.Builder {
-
-    private String projectId;
-    private String shortName;
-    private String method;
-    private String docUrl;
-    private GooglePublicKeysManager publicKeysManager;
-
-    public String getProjectId() {
-      return projectId;
-    }
-
-    /**
-     * Sets the project ID and a URL prefix for the issuer (iss) claim. The full issuer claim
-     * is the concatenation of the prefix and the project ID.
-     *
-     * @param issuerPrefix A URL prefix.
-     * @param projectId A Firebase project ID.
-     * @return This builder.
-     */
-    public Builder setProjectId(String issuerPrefix, String projectId) {
-      this.projectId = projectId;
-      this.setAudience(Collections.singleton(projectId));
-      this.setIssuer(issuerPrefix + projectId);
-      return this;
-    }
-
-    /**
-     * Sets the short name of the type of tokens being validated (e.g. ID token, session cookie).
-     *
-     * @param shortName A short string identifier.
-     * @return This builder.
-     */
-    public Builder setShortName(String shortName) {
-      this.shortName = shortName;
-      return this;
-    }
-
-    /**
-     * Sets the name of the operation that triggers token verification (e.g. verifyIdToken())
-     *
-     * @param method A method name.
-     * @return This builder.
-     */
-    public Builder setMethod(String method) {
-      this.method = method;
-      return this;
-    }
-
-    /**
-     * A URL to public documentation where more information about token verification can be found.
-     *
-     * @param docUrl A documentation URL.
-     * @return This builder.
-     */
-    public Builder setDocUrl(String docUrl) {
-      this.docUrl = docUrl;
-      return this;
-    }
-
-    /**
-     * Sets the {@code Clock} instance to be used to compare token issue and expiry times.
-     *
-     * @param clock A {@code Clock} instance.
-     * @return This builder.
-     */
-    @Override
-    public Builder setClock(Clock clock) {
-      return (Builder) super.setClock(clock);
-    }
-
-    /**
-     * Overrides the GooglePublicKeysManager from the default.
-     *
-     * @param publicKeysManager A public keys manager.
-     * @return This builder.
-     */
-    public Builder setPublicKeysManager(GooglePublicKeysManager publicKeysManager) {
-      this.publicKeysManager = publicKeysManager;
-      return this;
-    }
-
-    @Override
-    public FirebaseTokenVerifier build() {
-      return new FirebaseTokenVerifier(this);
-    }
-  }
-
-  /**
-   * Creates a new {@link FirebaseTokenVerifier} for verifying Firebase ID tokens.
-   *
-   * @param projectId Project ID string
-   * @param keyManagers {@link KeyManagers} instance with public key managers to use
-   * @param clock {@code Clock} instance for Google API client
-   * @return A new {@link FirebaseTokenVerifier} instance
-   */
-  @NonNull public static FirebaseTokenVerifier createIdTokenVerifier(
-      @NonNull String projectId, @NonNull KeyManagers keyManagers, @NonNull Clock clock) {
-    return new FirebaseTokenVerifier.Builder()
-        .setProjectId(ID_TOKEN_ISSUER_PREFIX, projectId)
-        .setPublicKeysManager(keyManagers.getIdTokenKeysManager())
-        .setShortName("ID token")
-        .setMethod("verifyIdToken()")
-        .setDocUrl("https://firebase.google.com/docs/auth/admin/verify-id-tokens")
-        .setClock(clock)
-        .build();
-  }
-
-  /**
-   * Creates a new {@link FirebaseTokenVerifier} for verifying Firebase ID tokens.
-   *
-   * @param projectId Project ID string
-   * @param keyManagers {@link KeyManagers} instance with public key managers to use
-   * @param clock {@code Clock} instance for Google API client
-   * @return A new {@link FirebaseTokenVerifier} instance
-   */
-  @NonNull public static FirebaseTokenVerifier createSessionCookieVerifier(
-      @NonNull String projectId, @NonNull KeyManagers keyManagers, @NonNull Clock clock) {
-    return new FirebaseTokenVerifier.Builder()
-        .setProjectId(SESSION_COOKIE_ISSUER_PREFIX, projectId)
-        .setPublicKeysManager(keyManagers.getSessionCookieKeysManager())
-        .setShortName("session cookie")
-        .setMethod("verifySessionCookie()")
-        .setDocUrl("https://firebase.google.com/docs/auth/admin/manage-cookies")
-        .setClock(clock)
-        .build();
-  }
-}
diff --git a/src/main/java/com/google/firebase/auth/internal/KeyManagers.java b/src/main/java/com/google/firebase/auth/internal/KeyManagers.java
deleted file mode 100644
index b22837a..0000000
--- a/src/main/java/com/google/firebase/auth/internal/KeyManagers.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright 2017 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.firebase.auth.internal;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
-import com.google.api.client.http.HttpTransport;
-import com.google.api.client.json.gson.GsonFactory;
-import com.google.api.client.util.Clock;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.firebase.FirebaseApp;
-import com.google.firebase.internal.NonNull;
-
-/**
- * A utility for initializing and keeping tack of the various public key manager instances
- * used by {@link com.google.firebase.auth.FirebaseAuth}.
- */
-public class KeyManagers {
-
-  private final GooglePublicKeysManager idTokenKeysManager;
-  private final GooglePublicKeysManager sessionCookieKeysManager;
-
-  private KeyManagers(
-      GooglePublicKeysManager idTokenKeysManager,
-      GooglePublicKeysManager sessionCookieKeysManager) {
-    this.idTokenKeysManager = checkNotNull(idTokenKeysManager);
-    this.sessionCookieKeysManager = checkNotNull(sessionCookieKeysManager);
-  }
-
-  /**
-   * Returns the key manager that should be used for ID token verification.
-   */
-  GooglePublicKeysManager getIdTokenKeysManager() {
-    return idTokenKeysManager;
-  }
-
-  /**
-   * Returns the key manager that should be used for session cookie verification.
-   */
-  GooglePublicKeysManager getSessionCookieKeysManager() {
-    return sessionCookieKeysManager;
-  }
-
-  /**
-   * Initialize a new set of key managers for the specified app using the given clock.
-   *
-   * @param app A {@link FirebaseApp} instance.
-   * @param clock A {@code Clock} to be used with Google API client.
-   * @return A new {@link KeyManagers} instance.
-   */
-  public static KeyManagers getDefault(@NonNull FirebaseApp app, @NonNull Clock clock) {
-    HttpTransport transport = app.getOptions().getHttpTransport();
-    return getDefault(transport, clock);
-  }
-
-  @VisibleForTesting
-  static KeyManagers getDefault(HttpTransport transport, Clock clock) {
-    return new KeyManagers(
-        createPublicKeysManager(transport, clock, FirebaseTokenVerifier.ID_TOKEN_CERT_URL),
-        createPublicKeysManager(transport, clock, FirebaseTokenVerifier.SESSION_COOKIE_CERT_URL));
-  }
-
-  private static GooglePublicKeysManager createPublicKeysManager(
-      HttpTransport transport, Clock clock, String certUrl) {
-    return new GooglePublicKeysManager.Builder(transport, new GsonFactory())
-        .setClock(clock)
-        .setPublicCertsEncodedUrl(certUrl)
-        .build();
-  }
-}


Test patch:
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
index b01eeb9..badb9ea 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
@@ -79,7 +79,7 @@ public class FirebaseAuthIT {
       "https://www.googleapis.com/identitytoolkit/v3/relyingparty/emailLinkSignin";
   private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
   private static final HttpTransport transport = Utils.getDefaultTransport();
-  public static final String ACTION_LINK_CONTINUE_URL = "http://localhost/?a=1&b=2#c=3";
+  private static final String ACTION_LINK_CONTINUE_URL = "http://localhost/?a=1&b=2#c=3";
 
   private static FirebaseAuth auth;
 
@@ -406,7 +406,7 @@ public class FirebaseAuthIT {
       fail("expecting exception");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.ID_TOKEN_REVOKED_ERROR,
+      assertEquals(RevocationCheckDecorator.ID_TOKEN_REVOKED_ERROR,
                    ((FirebaseAuthException) e.getCause()).getErrorCode());
     }
     idToken = signInWithCustomToken(customToken);
@@ -440,7 +440,7 @@ public class FirebaseAuthIT {
       fail("expecting exception");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.SESSION_COOKIE_REVOKED_ERROR,
+      assertEquals(RevocationCheckDecorator.SESSION_COOKIE_REVOKED_ERROR,
           ((FirebaseAuthException) e.getCause()).getErrorCode());
     }
 
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
index 8f19e5c..c495c41 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
@@ -16,6 +16,7 @@
 
 package com.google.firebase.auth;
 
+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;
@@ -25,9 +26,13 @@ import static org.junit.Assert.fail;
 
 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.auth.internal.FirebaseTokenFactory;
 import com.google.firebase.testing.ServiceAccount;
 import com.google.firebase.testing.TestUtils;
 import java.lang.reflect.InvocationTargetException;
@@ -38,10 +43,10 @@ import java.util.List;
 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.Assert;
-import org.junit.Before;
 import org.junit.Test;
 
 public class FirebaseAuthTest {
@@ -50,11 +55,6 @@ public class FirebaseAuthTest {
       .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream()))
       .build();
 
-  @Before
-  public void setup() {
-    FirebaseApp.initializeApp(firebaseOptions);
-  }
-
   @After
   public void cleanup() {
     TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
@@ -62,6 +62,7 @@ public class FirebaseAuthTest {
 
   @Test
   public void testGetInstance() {
+    FirebaseApp.initializeApp(firebaseOptions);
     FirebaseAuth defaultAuth = FirebaseAuth.getInstance();
     assertNotNull(defaultAuth);
     assertSame(defaultAuth, FirebaseAuth.getInstance());
@@ -164,4 +165,354 @@ public class FirebaseAuthTest {
   public void testAuthExceptionEmptyErrorCode() {
     new FirebaseAuthException("", "test");
   }
+
+  @Test
+  public void testDefaultIdTokenVerifier() {
+    FirebaseApp.initializeApp(firebaseOptions);
+
+    FirebaseTokenVerifier tokenVerifier = FirebaseAuth.getInstance()
+        .getIdTokenVerifier(false);
+
+    assertTrue(tokenVerifier instanceof FirebaseTokenVerifierImpl);
+    String shortName = ((FirebaseTokenVerifierImpl) tokenVerifier).getShortName();
+    assertEquals("ID token", shortName);
+  }
+
+  @Test
+  public void testIdTokenVerifierInitializedOnDemand() throws Exception {
+    FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
+        getFirebaseToken("idTokenUser"));
+    CountingSupplier<FirebaseTokenVerifier> countingSupplier = new CountingSupplier<>(
+        Suppliers.ofInstance(tokenVerifier));
+
+    FirebaseAuth auth = getAuthForIdTokenVerification(countingSupplier);
+    assertEquals(0, countingSupplier.getCount());
+
+    auth.verifyIdToken("idToken");
+    auth.verifyIdToken("idToken");
+
+    assertEquals(1, countingSupplier.getCount());
+  }
+
+  @Test
+  public void testVerifyIdTokenWithNull() {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null);
+    tokenVerifier.lastTokenString = "_init_";
+    FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier);
+
+    try {
+      auth.verifyIdTokenAsync(null);
+      fail("No error thrown for null id token");
+    } catch (IllegalArgumentException expected) {
+      assertEquals("_init_", tokenVerifier.getLastTokenString());
+    }
+  }
+
+  @Test
+  public void testVerifyIdTokenWithEmptyString() {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null);
+    tokenVerifier.lastTokenString = "_init_";
+    FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier);
+
+    try {
+      auth.verifyIdTokenAsync("");
+      fail("No error thrown for null id token");
+    } catch (IllegalArgumentException expected) {
+      assertEquals("_init_", tokenVerifier.getLastTokenString());
+    }
+
+  }
+
+  @Test
+  public void testVerifyIdToken() throws Exception {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
+        getFirebaseToken("testUser"));
+    FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier);
+
+    FirebaseToken firebaseToken = auth.verifyIdToken("idtoken");
+
+    assertEquals("testUser", firebaseToken.getUid());
+    assertEquals("idtoken", tokenVerifier.getLastTokenString());
+  }
+
+  @Test
+  public void testVerifyIdTokenFailure() {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(
+        new FirebaseAuthException("TEST_CODE", "Test error message"));
+    FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier);
+
+    try {
+      auth.verifyIdToken("idtoken");
+      fail("No error thrown for invalid token");
+    } catch (FirebaseAuthException authException) {
+      assertEquals("TEST_CODE", authException.getErrorCode());
+      assertEquals("Test error message", authException.getMessage());
+      assertEquals("idtoken", tokenVerifier.getLastTokenString());
+    }
+  }
+
+  @Test
+  public void testVerifyIdTokenAsync() throws Exception {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
+        getFirebaseToken("testUser"));
+    FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier);
+
+    FirebaseToken firebaseToken = auth.verifyIdTokenAsync("idtoken").get();
+
+    assertEquals("testUser", firebaseToken.getUid());
+    assertEquals("idtoken", tokenVerifier.getLastTokenString());
+  }
+
+  @Test
+  public void testVerifyIdTokenAsyncFailure() throws InterruptedException {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(
+        new FirebaseAuthException("TEST_CODE", "Test error message"));
+    FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier);
+
+    try {
+      auth.verifyIdTokenAsync("idtoken").get();
+      fail("No error thrown for invalid token");
+    } catch (ExecutionException e) {
+      FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+      assertEquals("TEST_CODE", authException.getErrorCode());
+      assertEquals("Test error message", authException.getMessage());
+      assertEquals("idtoken", tokenVerifier.getLastTokenString());
+    }
+  }
+
+  @Test
+  public void testVerifyIdTokenWithCheckRevokedAsyncFailure() throws InterruptedException {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(
+        new FirebaseAuthException("TEST_CODE", "Test error message"));
+    FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier);
+
+    try {
+      auth.verifyIdTokenAsync("idtoken", true).get();
+      fail("No error thrown for invalid token");
+    } catch (ExecutionException e) {
+      FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+      assertEquals("TEST_CODE", authException.getErrorCode());
+      assertEquals("Test error message", authException.getMessage());
+      assertEquals("idtoken", tokenVerifier.getLastTokenString());
+    }
+  }
+
+  @Test
+  public void testDefaultSessionCookieVerifier() {
+    FirebaseApp.initializeApp(firebaseOptions);
+
+    FirebaseTokenVerifier tokenVerifier = FirebaseAuth.getInstance()
+        .getSessionCookieVerifier(false);
+
+    assertTrue(tokenVerifier instanceof FirebaseTokenVerifierImpl);
+    String shortName = ((FirebaseTokenVerifierImpl) tokenVerifier).getShortName();
+    assertEquals("session cookie", shortName);
+  }
+
+  @Test
+  public void testSessionCookieVerifierInitializedOnDemand() throws Exception {
+    FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
+        getFirebaseToken("cookieUser"));
+    CountingSupplier<FirebaseTokenVerifier> countingSupplier = new CountingSupplier<>(
+        Suppliers.ofInstance(tokenVerifier));
+    FirebaseAuth auth = getAuthForSessionCookieVerification(countingSupplier);
+
+    assertEquals(0, countingSupplier.getCount());
+
+    auth.verifySessionCookie("sessionCookie");
+    auth.verifySessionCookie("sessionCookie");
+
+    assertEquals(1, countingSupplier.getCount());
+  }
+
+  @Test
+  public void testVerifySessionCookieWithNull() {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null);
+    tokenVerifier.lastTokenString = "_init_";
+    FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier);
+
+    try {
+      auth.verifySessionCookieAsync(null);
+      fail("No error thrown for null id token");
+    } catch (IllegalArgumentException expected) {
+      assertEquals("_init_", tokenVerifier.getLastTokenString());
+    }
+  }
+
+  @Test
+  public void testVerifySessionCookieWithEmptyString() {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null);
+    tokenVerifier.lastTokenString = "_init_";
+    FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier);
+
+    try {
+      auth.verifySessionCookieAsync("");
+      fail("No error thrown for null id token");
+    } catch (IllegalArgumentException expected) {
+      assertEquals("_init_", tokenVerifier.getLastTokenString());
+    }
+
+  }
+
+  @Test
+  public void testVerifySessionCookie() throws Exception {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
+        getFirebaseToken("testUser"));
+    FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier);
+
+    FirebaseToken firebaseToken = auth.verifySessionCookie("idtoken");
+
+    assertEquals("testUser", firebaseToken.getUid());
+    assertEquals("idtoken", tokenVerifier.getLastTokenString());
+  }
+
+  @Test
+  public void testVerifySessionCookieFailure() {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(
+        new FirebaseAuthException("TEST_CODE", "Test error message"));
+    FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier);
+
+    try {
+      auth.verifySessionCookie("idtoken");
+      fail("No error thrown for invalid token");
+    } catch (FirebaseAuthException authException) {
+      assertEquals("TEST_CODE", authException.getErrorCode());
+      assertEquals("Test error message", authException.getMessage());
+      assertEquals("idtoken", tokenVerifier.getLastTokenString());
+    }
+  }
+
+  @Test
+  public void testVerifySessionCookieAsync() throws Exception {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
+        getFirebaseToken("testUser"));
+    FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier);
+
+    FirebaseToken firebaseToken = auth.verifySessionCookieAsync("idtoken").get();
+
+    assertEquals("testUser", firebaseToken.getUid());
+    assertEquals("idtoken", tokenVerifier.getLastTokenString());
+  }
+
+  @Test
+  public void testVerifySessionCookieAsyncFailure() throws InterruptedException {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(
+        new FirebaseAuthException("TEST_CODE", "Test error message"));
+    FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier);
+
+    try {
+      auth.verifySessionCookieAsync("idtoken").get();
+      fail("No error thrown for invalid token");
+    } catch (ExecutionException e) {
+      FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+      assertEquals("TEST_CODE", authException.getErrorCode());
+      assertEquals("Test error message", authException.getMessage());
+      assertEquals("idtoken", tokenVerifier.getLastTokenString());
+    }
+  }
+
+  @Test
+  public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws InterruptedException {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(
+        new FirebaseAuthException("TEST_CODE", "Test error message"));
+    FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier);
+
+    try {
+      auth.verifySessionCookieAsync("idtoken", true).get();
+      fail("No error thrown for invalid token");
+    } catch (ExecutionException e) {
+      FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+      assertEquals("TEST_CODE", authException.getErrorCode());
+      assertEquals("Test error message", authException.getMessage());
+      assertEquals("idtoken", tokenVerifier.getLastTokenString());
+    }
+  }
+
+  private FirebaseToken getFirebaseToken(String subject) {
+    return new FirebaseToken(ImmutableMap.<String, Object>of("sub", subject));
+  }
+
+  private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVerifier) {
+    return getAuthForIdTokenVerification(Suppliers.ofInstance(tokenVerifier));
+  }
+
+  private FirebaseAuth getAuthForIdTokenVerification(
+      Supplier<? extends FirebaseTokenVerifier> tokenVerifierSupplier) {
+    FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions);
+    return FirebaseAuth.builder()
+        .setFirebaseApp(app)
+        .setTokenFactory(Suppliers.<FirebaseTokenFactory>ofInstance(null))
+        .setIdTokenVerifier(tokenVerifierSupplier)
+        .setCookieVerifier(Suppliers.<FirebaseTokenVerifier>ofInstance(null))
+        .build();
+  }
+
+  private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier tokenVerifier) {
+    return getAuthForSessionCookieVerification(Suppliers.ofInstance(tokenVerifier));
+  }
+
+  private FirebaseAuth getAuthForSessionCookieVerification(
+      Supplier<? extends FirebaseTokenVerifier> tokenVerifierSupplier) {
+    FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions);
+    return FirebaseAuth.builder()
+        .setFirebaseApp(app)
+        .setTokenFactory(Suppliers.<FirebaseTokenFactory>ofInstance(null))
+        .setIdTokenVerifier(Suppliers.<FirebaseTokenVerifier>ofInstance(null))
+        .setCookieVerifier(tokenVerifierSupplier)
+        .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);
+    }
+  }
+
+  private static class CountingSupplier<T> implements Supplier<T> {
+
+    private final AtomicInteger counter = new AtomicInteger(0);
+    private final Supplier<T> supplier;
+
+    CountingSupplier(Supplier<T> supplier) {
+      this.supplier = checkNotNull(supplier);
+    }
+
+    @Override
+    public T get() {
+      counter.incrementAndGet();
+      return supplier.get();
+    }
+
+    int getCount() {
+      return counter.get();
+    }
+  }
 }
diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenTest.java
new file mode 100644
index 0000000..af65561
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/FirebaseTokenTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright  2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.api.client.auth.openidconnect.IdToken;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.junit.Test;
+
+public class FirebaseTokenTest {
+
+  @Test
+  public void testFirebaseToken() {
+    Map<String, Object> claims = ImmutableMap.<String, Object>builder()
+        .put("sub", "testUser")
+        .put("iss", "test-project-id")
+        .put("email", "test@example.com")
+        .put("email_verified", true)
+        .put("name", "Test User")
+        .put("picture", "https://picture.url")
+        .put("custom", "claim")
+        .build();
+
+    FirebaseToken firebaseToken = new FirebaseToken(claims);
+
+    assertEquals("testUser", firebaseToken.getUid());
+    assertEquals("test-project-id", firebaseToken.getIssuer());
+    assertEquals("test@example.com", firebaseToken.getEmail());
+    assertTrue(firebaseToken.isEmailVerified());
+    assertEquals("Test User", firebaseToken.getName());
+    assertEquals("https://picture.url", firebaseToken.getPicture());
+    assertEquals("claim", firebaseToken.getClaims().get("custom"));
+    assertEquals(7, firebaseToken.getClaims().size());
+  }
+
+  @Test
+  public void testFirebaseTokenMinimal() {
+    Map<String, Object> claims = ImmutableMap.<String, Object>builder()
+        .put("sub", "testUser")
+        .build();
+
+    FirebaseToken firebaseToken = new FirebaseToken(claims);
+
+    assertEquals("testUser", firebaseToken.getUid());
+    assertNull(firebaseToken.getIssuer());
+    assertNull(firebaseToken.getEmail());
+    assertFalse(firebaseToken.isEmailVerified());
+    assertNull(firebaseToken.getName());
+    assertNull(firebaseToken.getPicture());
+    assertEquals(1, firebaseToken.getClaims().size());
+  }
+
+  @Test
+  public void testFirebaseTokenFromIdToken() {
+    IdToken.Payload payload = new IdToken.Payload()
+        .setSubject("testUser")
+        .setIssuer("test-project-id")
+        .set("email", "test@example.com")
+        .set("email_verified", true)
+        .set("name", "Test User")
+        .set("picture", "https://picture.url")
+        .set("custom", "claim");
+
+    FirebaseToken firebaseToken = new FirebaseToken(payload);
+
+    assertEquals("testUser", firebaseToken.getUid());
+    assertEquals("test-project-id", firebaseToken.getIssuer());
+    assertEquals("test@example.com", firebaseToken.getEmail());
+    assertTrue(firebaseToken.isEmailVerified());
+    assertEquals("Test User", firebaseToken.getName());
+    assertEquals("https://picture.url", firebaseToken.getPicture());
+    assertEquals("claim", firebaseToken.getClaims().get("custom"));
+    assertEquals(7, firebaseToken.getClaims().size());
+  }
+
+  @Test
+  public void testFirebaseTokenFromMinimalIdToken() {
+    IdToken.Payload payload = new IdToken.Payload()
+        .setSubject("testUser");
+
+    FirebaseToken firebaseToken = new FirebaseToken(payload);
+
+    assertEquals("testUser", firebaseToken.getUid());
+    assertNull(firebaseToken.getIssuer());
+    assertNull(firebaseToken.getEmail());
+    assertFalse(firebaseToken.isEmailVerified());
+    assertNull(firebaseToken.getName());
+    assertNull(firebaseToken.getPicture());
+    assertEquals(1, firebaseToken.getClaims().size());
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testFirebaseTokenNullClaims() {
+    new FirebaseToken(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testFirebaseTokenNoUid() {
+    ImmutableMap<String, Object> claimsWithoutSub = ImmutableMap.<String, Object>of(
+        "custom", "claim");
+    new FirebaseToken(claimsWithoutSub);
+  }
+}
diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenUtilsTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenUtilsTest.java
new file mode 100644
index 0000000..f452091
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/FirebaseTokenUtilsTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright  2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static junit.framework.TestCase.assertNotNull;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import com.google.api.client.auth.openidconnect.IdTokenVerifier;
+import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.api.client.testing.http.FixedClock;
+import com.google.api.client.util.Clock;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.common.collect.Iterables;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import com.google.firebase.TestOnlyImplFirebaseTrampolines;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class FirebaseTokenUtilsTest {
+
+  private static final Clock CLOCK = new FixedClock(2002000L * 1000);
+  private static final String TEST_PROJECT_ID = "test-project-id";
+  private static final GoogleCredentials MOCK_CREDENTIALS = GoogleCredentials.create(null);
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @After
+  public void tearDown() {
+    TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
+  }
+
+  @Test
+  public void testCreateIdTokenVerifier() {
+    FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder()
+        .setCredentials(MOCK_CREDENTIALS)
+        .setProjectId(TEST_PROJECT_ID)
+        .build());
+
+    FirebaseTokenVerifierImpl idTokenVerifier = FirebaseTokenUtils.createIdTokenVerifier(
+        app, CLOCK);
+
+    assertEquals("verifyIdToken()", idTokenVerifier.getMethod());
+    assertEquals("ID token", idTokenVerifier.getShortName());
+    assertEquals("an ID token", idTokenVerifier.getArticledShortName());
+    assertEquals("https://firebase.google.com/docs/auth/admin/verify-id-tokens",
+        idTokenVerifier.getDocUrl());
+    verifyPublicKeysManager(idTokenVerifier.getPublicKeysManager(),
+        "https://www.googleapis.com/robot/v1/metadata/x509/"
+            + "securetoken@system.gserviceaccount.com");
+    verifyJwtVerifier(idTokenVerifier.getIdTokenVerifier(),
+        "https://securetoken.google.com/test-project-id");
+  }
+
+  @Test
+  public void testCreateIdTokenVerifierWithoutProjectId() {
+    FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder()
+        .setCredentials(MOCK_CREDENTIALS)
+        .build());
+
+    thrown.expectMessage("Must initialize FirebaseApp with a project ID to call verifyIdToken()");
+    FirebaseTokenUtils.createIdTokenVerifier(app, CLOCK);
+  }
+
+  @Test
+  public void testSessionCookieVerifier() {
+    FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder()
+        .setCredentials(MOCK_CREDENTIALS)
+        .setProjectId(TEST_PROJECT_ID)
+        .build());
+
+    FirebaseTokenVerifierImpl cookieVerifier = FirebaseTokenUtils.createSessionCookieVerifier(
+        app, CLOCK);
+
+    assertEquals("verifySessionCookie()", cookieVerifier.getMethod());
+    assertEquals("session cookie", cookieVerifier.getShortName());
+    assertEquals("a session cookie", cookieVerifier.getArticledShortName());
+    assertEquals("https://firebase.google.com/docs/auth/admin/manage-cookies",
+        cookieVerifier.getDocUrl());
+    verifyPublicKeysManager(cookieVerifier.getPublicKeysManager(),
+        "https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys");
+    verifyJwtVerifier(cookieVerifier.getIdTokenVerifier(),
+        "https://session.firebase.google.com/test-project-id");
+  }
+
+  @Test
+  public void testCreateSessionCookieVerifierWithoutProjectId() {
+    FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder()
+        .setCredentials(MOCK_CREDENTIALS)
+        .build());
+
+    thrown.expectMessage("Must initialize FirebaseApp with a project ID to call "
+        + "verifySessionCookie()");
+    FirebaseTokenUtils.createSessionCookieVerifier(app, CLOCK);
+  }
+
+  private void verifyPublicKeysManager(GooglePublicKeysManager publicKeysManager, String certUrl) {
+    assertNotNull(publicKeysManager);
+    assertEquals(certUrl, publicKeysManager.getPublicCertsEncodedUrl());
+    assertSame(CLOCK, publicKeysManager.getClock());
+    assertTrue(publicKeysManager.getJsonFactory() instanceof GsonFactory);
+  }
+
+  private void verifyJwtVerifier(IdTokenVerifier jwtVerifier, String issuer) {
+    assertNotNull(jwtVerifier);
+    assertEquals(issuer, jwtVerifier.getIssuer());
+    assertEquals(TEST_PROJECT_ID, Iterables.getOnlyElement(jwtVerifier.getAudience()));
+    assertSame(CLOCK, jwtVerifier.getClock());
+  }
+}
diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java
new file mode 100644
index 0000000..b10817a
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright  2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.api.client.auth.openidconnect.IdTokenVerifier;
+import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.json.webtoken.JsonWebSignature;
+import com.google.api.client.json.webtoken.JsonWebToken.Payload;
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.firebase.testing.ServiceAccount;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class FirebaseTokenVerifierImplTest {
+
+  private static final String CUSTOM_TOKEN_AUDIENCE =
+      "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit"
+          + ".v1.IdentityToolkit";
+
+  private static final String LEGACY_CUSTOM_TOKEN =
+      "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkIjp7"
+          + "InVpZCI6IjEiLCJhYmMiOiIwMTIzNDU2Nzg5fiFAIyQlXiYqKClfKy09YWJjZGVmZ2hpamtsbW5vcHF"
+          + "yc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWiwuLzsnW11cXDw"
+          + "-P1wie318In0sInYiOjAsImlhdCI6MTQ4MDk4Mj"
+          + "U2NH0.ZWEpoHgIPCAz8Q-cNFBS8jiqClTJ3j27yuRkQo-QxyI";
+
+  private static final String TEST_TOKEN_ISSUER = "https://test.token.issuer";
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  private FirebaseTokenVerifier tokenVerifier;
+  private TestTokenFactory tokenFactory;
+
+  @Before
+  public void setUp() throws Exception {
+    ServiceAccount serviceAccount = ServiceAccount.EDITOR;
+    GooglePublicKeysManager publicKeysManager = newPublicKeysManager(serviceAccount.getCert());
+    this.tokenVerifier = newTestTokenVerifier(publicKeysManager);
+    this.tokenFactory = new TestTokenFactory(serviceAccount.getPrivateKey(), TEST_TOKEN_ISSUER);
+  }
+
+  @Test
+  public void testVerifyToken() throws Exception {
+    String token = tokenFactory.createToken();
+
+    FirebaseToken firebaseToken = tokenVerifier.verifyToken(token);
+
+    assertEquals(TEST_TOKEN_ISSUER, firebaseToken.getIssuer());
+    assertEquals(TestTokenFactory.UID, firebaseToken.getUid());
+  }
+
+  @Test
+  public void testVerifyTokenWithoutKeyId() throws Exception {
+    String token = createTokenWithoutKeyId();
+
+    thrown.expectMessage("Firebase test token has no \"kid\" claim.");
+    tokenVerifier.verifyToken(token);
+  }
+
+  @Test
+  public void testVerifyTokenFirebaseCustomToken() throws Exception {
+    String token = createCustomToken();
+
+    thrown.expectMessage("verifyTestToken() expects a test token, but was given a custom token.");
+    tokenVerifier.verifyToken(token);
+  }
+
+  @Test
+  public void testVerifyTokenIncorrectAlgorithm() throws Exception {
+    String token = createTokenWithIncorrectAlgorithm();
+
+    thrown.expectMessage("Firebase test token has incorrect algorithm.");
+    tokenVerifier.verifyToken(token);
+  }
+
+  @Test
+  public void testVerifyTokenIncorrectAudience() throws Exception {
+    String token = createTokenWithIncorrectAudience();
+
+    thrown.expectMessage("Firebase test token has incorrect \"aud\" (audience) claim.");
+    tokenVerifier.verifyToken(token);
+  }
+
+  @Test
+  public void testVerifyTokenIncorrectIssuer() throws Exception {
+    String token = createTokenWithIncorrectIssuer();
+
+    thrown.expectMessage("Firebase test token has incorrect \"iss\" (issuer) claim.");
+    tokenVerifier.verifyToken(token);
+  }
+
+  @Test
+  public void testVerifyTokenMissingSubject() throws Exception {
+    String token = createTokenWithSubject(null);
+
+    thrown.expectMessage("Firebase test token has no \"sub\" (subject) claim.");
+    tokenVerifier.verifyToken(token);
+  }
+
+  @Test
+  public void testVerifyTokenEmptySubject() throws Exception {
+    String token = createTokenWithSubject("");
+
+    thrown.expectMessage("Firebase test token has an empty string \"sub\" (subject) claim.");
+    tokenVerifier.verifyToken(token);
+  }
+
+  @Test
+  public void testVerifyTokenLongSubject() throws Exception {
+    String token = createTokenWithSubject(Strings.repeat("a", 129));
+
+    thrown.expectMessage(
+        "Firebase test token has \"sub\" (subject) claim longer than 128 characters.");
+    tokenVerifier.verifyToken(token);
+  }
+
+  @Test
+  public void testVerifyTokenIssuedAtInFuture() throws Exception {
+    long tenMinutesIntoTheFuture = (TestTokenFactory.CLOCK.currentTimeMillis() / 1000)
+        + TimeUnit.MINUTES.toSeconds(10);
+    String token = createTokenWithTimestamps(
+        tenMinutesIntoTheFuture,
+        tenMinutesIntoTheFuture + TimeUnit.HOURS.toSeconds(1));
+
+    thrown.expectMessage("Firebase test token has expired or is not yet valid.");
+    tokenVerifier.verifyToken(token);
+  }
+
+  @Test
+  public void testVerifyTokenExpired() throws Exception {
+    long twoHoursInPast = (TestTokenFactory.CLOCK.currentTimeMillis() / 1000)
+        - TimeUnit.HOURS.toSeconds(2);
+    String token = createTokenWithTimestamps(
+        twoHoursInPast,
+        twoHoursInPast + TimeUnit.HOURS.toSeconds(1));
+
+    thrown.expectMessage("Firebase test token has expired or is not yet valid.");
+    tokenVerifier.verifyToken(token);
+  }
+
+  @Test
+  public void testVerifyTokenIncorrectCert() throws Exception {
+    String token = tokenFactory.createToken();
+    GooglePublicKeysManager publicKeysManager = newPublicKeysManager(
+        ServiceAccount.NONE.getCert());
+    FirebaseTokenVerifier tokenVerifier = newTestTokenVerifier(publicKeysManager);
+
+    thrown.expectMessage("Failed to verify the signature of Firebase test token. "
+        + "See https://test.doc.url for details on how to retrieve a test token.");
+    tokenVerifier.verifyToken(token);
+  }
+
+  @Test
+  public void verifyTokenCertificateError() {
+    MockHttpTransport failingTransport = new MockHttpTransport() {
+      @Override
+      public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+        throw new IOException("Expected error");
+      }
+    };
+    GooglePublicKeysManager publicKeysManager = newPublicKeysManager(failingTransport);
+    FirebaseTokenVerifier idTokenVerifier = newTestTokenVerifier(publicKeysManager);
+    String token = tokenFactory.createToken();
+
+    try {
+      idTokenVerifier.verifyToken(token);
+      Assert.fail("No exception thrown");
+    } catch (FirebaseAuthException expected) {
+      assertTrue(expected.getCause() instanceof IOException);
+      assertEquals("Expected error", expected.getCause().getMessage());
+    }
+  }
+
+  @Test
+  public void testLegacyCustomToken() throws Exception {
+    thrown.expectMessage(
+        "verifyTestToken() expects a test token, but was given a legacy custom token.");
+    tokenVerifier.verifyToken(LEGACY_CUSTOM_TOKEN);
+  }
+
+  @Test
+  public void testMalformedToken() throws Exception {
+    thrown.expectMessage(
+        "Failed to parse Firebase test token. Make sure you passed a string that represents a "
+            + "complete and valid JWT. See https://test.doc.url for details on how to retrieve "
+            + "a test token.");
+    tokenVerifier.verifyToken("not.a.jwt");
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testBuilderNoPublicKeysManager() {
+    fullyPopulatedBuilder().setPublicKeysManager(null).build();
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testBuilderNoJsonFactory() {
+    fullyPopulatedBuilder().setJsonFactory(null).build();
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testBuilderNoIdTokenVerifier() {
+    fullyPopulatedBuilder().setIdTokenVerifier(null).build();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBuilderNoMethodName() {
+    fullyPopulatedBuilder().setMethod(null).build();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBuilderNoShortName() {
+    fullyPopulatedBuilder().setShortName(null).build();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBuilderNoDocUrl() {
+    fullyPopulatedBuilder().setDocUrl(null).build();
+  }
+
+  private GooglePublicKeysManager newPublicKeysManager(String certificate) {
+    String serviceAccountCertificates =
+        String.format("{\"%s\" : \"%s\"}", TestTokenFactory.PRIVATE_KEY_ID, certificate);
+    HttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(
+            new MockLowLevelHttpResponse().setContent(serviceAccountCertificates))
+        .build();
+    return newPublicKeysManager(transport);
+  }
+
+  private GooglePublicKeysManager newPublicKeysManager(HttpTransport transport) {
+    return new GooglePublicKeysManager.Builder(
+        transport, FirebaseTokenUtils.UNQUOTED_CTRL_CHAR_JSON_FACTORY)
+        .setClock(TestTokenFactory.CLOCK)
+        .setPublicCertsEncodedUrl("https://test.cert.url")
+        .build();
+  }
+
+  private FirebaseTokenVerifier newTestTokenVerifier(GooglePublicKeysManager publicKeysManager) {
+    return FirebaseTokenVerifierImpl.builder()
+        .setShortName("test token")
+        .setMethod("verifyTestToken()")
+        .setDocUrl("https://test.doc.url")
+        .setJsonFactory(TestTokenFactory.JSON_FACTORY)
+        .setPublicKeysManager(publicKeysManager)
+        .setIdTokenVerifier(newIdTokenVerifier())
+        .build();
+  }
+
+  private FirebaseTokenVerifierImpl.Builder fullyPopulatedBuilder() {
+    return FirebaseTokenVerifierImpl.builder()
+        .setShortName("test token")
+        .setMethod("verifyTestToken()")
+        .setDocUrl("https://test.doc.url")
+        .setJsonFactory(TestTokenFactory.JSON_FACTORY)
+        .setPublicKeysManager(newPublicKeysManager(ServiceAccount.EDITOR.getCert()))
+        .setIdTokenVerifier(newIdTokenVerifier());
+  }
+
+  private IdTokenVerifier newIdTokenVerifier() {
+    return new IdTokenVerifier.Builder()
+          .setClock(TestTokenFactory.CLOCK)
+          .setAudience(ImmutableList.of(TestTokenFactory.PROJECT_ID))
+          .setIssuer(TEST_TOKEN_ISSUER)
+          .build();
+  }
+
+  private String createTokenWithoutKeyId() {
+    JsonWebSignature.Header header = tokenFactory.createHeader();
+    header.setKeyId(null);
+    return tokenFactory.createToken(header);
+  }
+
+  private String createTokenWithSubject(String sub) {
+    Payload payload = tokenFactory.createTokenPayload();
+    payload.setSubject(sub);
+    return tokenFactory.createToken(payload);
+  }
+
+  private String createCustomToken() {
+    JsonWebSignature.Header header = tokenFactory.createHeader();
+    header.setKeyId(null);
+    Payload payload = tokenFactory.createTokenPayload();
+    payload.setAudience(CUSTOM_TOKEN_AUDIENCE);
+    return tokenFactory.createToken(header, payload);
+  }
+
+  private String createTokenWithIncorrectAlgorithm() {
+    JsonWebSignature.Header header = tokenFactory.createHeader();
+    header.setAlgorithm("HSA");
+    return tokenFactory.createToken(header);
+  }
+
+  private String createTokenWithIncorrectAudience() {
+    Payload payload = tokenFactory.createTokenPayload();
+    payload.setAudience("invalid-audience");
+    return tokenFactory.createToken(payload);
+  }
+
+  private String createTokenWithIncorrectIssuer() {
+    Payload payload = tokenFactory.createTokenPayload();
+    payload.setIssuer("https://incorrect.issuer.prefix/" + TestTokenFactory.PROJECT_ID);
+    return tokenFactory.createToken(payload);
+  }
+
+  private String createTokenWithTimestamps(long issuedAtSeconds, long expirationSeconds) {
+    Payload payload = tokenFactory.createTokenPayload();
+    payload.setIssuedAtTimeSeconds(issuedAtSeconds);
+    payload.setExpirationTimeSeconds(expirationSeconds);
+    return tokenFactory.createToken(payload);
+  }
+}
diff --git a/src/test/java/com/google/firebase/auth/TestOnlyImplFirebaseAuthTrampolines.java b/src/test/java/com/google/firebase/auth/TestOnlyImplFirebaseAuthTrampolines.java
deleted file mode 100644
index fb66500..0000000
--- a/src/test/java/com/google/firebase/auth/TestOnlyImplFirebaseAuthTrampolines.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright 2017 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.firebase.auth;
-
-import com.google.api.client.json.JsonFactory;
-
-import java.io.IOException;
-
-/**
- * Provides trampolines into package-private Auth APIs used by components of Firebase
- *
- * <p>This class will not be compiled into the shipping library and can only be used in tests.
- */
-public final class TestOnlyImplFirebaseAuthTrampolines {
-
-  private TestOnlyImplFirebaseAuthTrampolines() {}
-
-  /* FirebaseApp */
-  public static FirebaseToken.FirebaseTokenImpl getToken(FirebaseToken tokenHolder) {
-    return tokenHolder.getToken();
-  }
-
-  /* FirebaseToken */
-  public static FirebaseToken parseToken(JsonFactory jsonFactory, String tokenString)
-      throws IOException {
-    return FirebaseToken.parse(jsonFactory, tokenString);
-  }
-
-}
diff --git a/src/test/java/com/google/firebase/auth/TestTokenFactory.java b/src/test/java/com/google/firebase/auth/TestTokenFactory.java
new file mode 100644
index 0000000..1f774ee
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/TestTokenFactory.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright  2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import com.google.api.client.googleapis.util.Utils;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.webtoken.JsonWebSignature;
+import com.google.api.client.json.webtoken.JsonWebToken;
+import com.google.api.client.testing.http.FixedClock;
+import com.google.api.client.util.Clock;
+import com.google.common.io.BaseEncoding;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.spec.KeySpec;
+import java.security.spec.PKCS8EncodedKeySpec;
+
+class TestTokenFactory {
+
+  public static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory();
+  public static final Clock CLOCK = new FixedClock(2002000L * 1000);
+  public static final String PROJECT_ID = "proj-test-101";
+  public static final String PRIVATE_KEY_ID = "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd";
+  public static final String UID = "someUid";
+
+
+  private final PrivateKey privateKey;
+  private final String issuer;
+
+  TestTokenFactory(String privateKey, String issuer) throws GeneralSecurityException {
+    byte[] privateBytes = BaseEncoding.base64().decode(privateKey);
+    KeySpec spec = new PKCS8EncodedKeySpec(privateBytes);
+    this.privateKey = KeyFactory.getInstance("RSA").generatePrivate(spec);
+    this.issuer = issuer;
+  }
+
+  public String createToken() {
+    return createToken(createTokenPayload());
+  }
+
+  public String createToken(JsonWebSignature.Header header) {
+    return createToken(header, createTokenPayload());
+  }
+
+  public String createToken(JsonWebSignature.Payload payload) {
+    return createToken(createHeader(), payload);
+  }
+
+  public String createToken(JsonWebSignature.Header header, JsonWebToken.Payload payload) {
+    try {
+      return JsonWebSignature.signUsingRsaSha256(privateKey, JSON_FACTORY, header, payload);
+    } catch (GeneralSecurityException | IOException e) {
+      throw new RuntimeException("Failed to create test token", e);
+    }
+  }
+
+  public JsonWebSignature.Header createHeader() {
+    JsonWebSignature.Header header = new JsonWebSignature.Header();
+    header.setAlgorithm("RS256");
+    header.setType("JWT");
+    header.setKeyId(PRIVATE_KEY_ID);
+    return header;
+  }
+
+  public JsonWebToken.Payload createTokenPayload() {
+    JsonWebToken.Payload payload = new JsonWebToken.Payload();
+    payload.setIssuer(issuer);
+    payload.setAudience(PROJECT_ID);
+    payload.setIssuedAtTimeSeconds(CLOCK.currentTimeMillis() / 1000);
+    payload.setExpirationTimeSeconds(CLOCK.currentTimeMillis() / 1000 + 3600);
+    payload.setSubject(UID);
+    return payload;
+  }
+}
diff --git a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenVerifierTest.java b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenVerifierTest.java
deleted file mode 100644
index da92a75..0000000
--- a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenVerifierTest.java
+++ /dev/null
@@ -1,476 +0,0 @@
-/*
- * Copyright 2017 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.firebase.auth.internal;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import com.google.api.client.auth.openidconnect.IdToken;
-import com.google.api.client.http.LowLevelHttpRequest;
-import com.google.api.client.json.JsonFactory;
-import com.google.api.client.json.gson.GsonFactory;
-import com.google.api.client.json.webtoken.JsonWebSignature;
-import com.google.api.client.json.webtoken.JsonWebSignature.Header;
-import com.google.api.client.json.webtoken.JsonWebToken;
-import com.google.api.client.json.webtoken.JsonWebToken.Payload;
-import com.google.api.client.testing.http.FixedClock;
-import com.google.api.client.testing.http.MockHttpTransport;
-import com.google.api.client.testing.http.MockLowLevelHttpResponse;
-import com.google.common.io.BaseEncoding;
-import com.google.firebase.auth.FirebaseAuthException;
-import com.google.firebase.auth.FirebaseToken;
-import com.google.firebase.auth.TestOnlyImplFirebaseAuthTrampolines;
-import com.google.firebase.testing.ServiceAccount;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.security.KeyFactory;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.KeySpec;
-import java.security.spec.PKCS8EncodedKeySpec;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-/** 
- * Unit tests for {@link FirebaseTokenVerifier}.
- */
-public class FirebaseTokenVerifierTest {
-
-  private static final JsonFactory FACTORY = new GsonFactory();
-  private static final FixedClock CLOCK = new FixedClock(2002000L * 1000);
-  private static final String PROJECT_ID = "proj-test-101";
-  private static final String TOKEN_ISSUER =
-      FirebaseTokenVerifier.ID_TOKEN_ISSUER_PREFIX + PROJECT_ID;
-  private static final String COOKIE_ISSUER =
-      FirebaseTokenVerifier.SESSION_COOKIE_ISSUER_PREFIX + PROJECT_ID;
-  private static final String PRIVATE_KEY_ID = "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd";
-  private static final String UID = "someUid";
-  private static final String ALGORITHM = "RS256";
-  private static final String LEGACY_CUSTOM_TOKEN =
-      "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkIjp7"
-          + "InVpZCI6IjEiLCJhYmMiOiIwMTIzNDU2Nzg5fiFAIyQlXiYqKClfKy09YWJjZGVmZ2hpamtsbW5vcHF"
-          + "yc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWiwuLzsnW11cXDw"
-          + "-P1wie318In0sInYiOjAsImlhdCI6MTQ4MDk4Mj"
-          + "U2NH0.ZWEpoHgIPCAz8Q-cNFBS8jiqClTJ3j27yuRkQo-QxyI";
-
-  @Rule public ExpectedException thrown = ExpectedException.none();
-
-  private PrivateKey privateKey;
-  private FirebaseTokenVerifier idTokenVerifier;
-  private FirebaseTokenVerifier cookieVerifier;
-
-  private void initCrypto(String privateKey, String certificate)
-      throws NoSuchAlgorithmException, InvalidKeySpecException {
-    byte[] privateBytes = BaseEncoding.base64().decode(privateKey);
-    KeySpec spec = new PKCS8EncodedKeySpec(privateBytes);
-    String serviceAccountCertificates =
-        String.format("{\"%s\" : \"%s\"}", PRIVATE_KEY_ID, certificate);
-
-    MockHttpTransport mockTransport =
-        new MockHttpTransport.Builder()
-            .setLowLevelHttpResponse(
-                new MockLowLevelHttpResponse().setContent(serviceAccountCertificates))
-            .build();
-    KeyManagers keyManagers = KeyManagers.getDefault(mockTransport, CLOCK);
-    this.privateKey = KeyFactory.getInstance("RSA").generatePrivate(spec);
-    this.idTokenVerifier = FirebaseTokenVerifier.createIdTokenVerifier(
-        PROJECT_ID, keyManagers, CLOCK);
-    this.cookieVerifier = FirebaseTokenVerifier.createSessionCookieVerifier(
-        PROJECT_ID, keyManagers, CLOCK);
-  }
-
-  @Before
-  public void setUp() throws Exception {
-    initCrypto(ServiceAccount.EDITOR.getPrivateKey(), ServiceAccount.EDITOR.getCert());
-  }
-
-  private JsonWebSignature.Header createHeader() {
-    JsonWebSignature.Header header = new JsonWebSignature.Header();
-    header.setAlgorithm(ALGORITHM);
-    header.setType("JWT");
-    header.setKeyId(PRIVATE_KEY_ID);
-    return header;
-  }
-
-  private JsonWebToken.Payload createTokenPayload() {
-    JsonWebToken.Payload payload = new JsonWebToken.Payload();
-    payload.setIssuer(TOKEN_ISSUER);
-    payload.setAudience(PROJECT_ID);
-    payload.setIssuedAtTimeSeconds(CLOCK.currentTimeMillis() / 1000);
-    payload.setExpirationTimeSeconds(CLOCK.currentTimeMillis() / 1000 + 3600);
-    payload.setSubject(UID);
-    return payload;
-  }
-
-  private JsonWebToken.Payload createCookiePayload() {
-    JsonWebToken.Payload payload = new JsonWebToken.Payload();
-    payload.setIssuer(COOKIE_ISSUER);
-    payload.setAudience(PROJECT_ID);
-    payload.setIssuedAtTimeSeconds(CLOCK.currentTimeMillis() / 1000);
-    payload.setExpirationTimeSeconds(CLOCK.currentTimeMillis() / 1000 + 3600);
-    payload.setSubject(UID);
-    return payload;
-  }
-
-  private String createToken(JsonWebSignature.Header header, JsonWebToken.Payload payload)
-      throws GeneralSecurityException, IOException {
-    return JsonWebSignature.signUsingRsaSha256(privateKey, FACTORY, header, payload);
-  }
-
-  @Test
-  public void verifyToken() throws Exception {
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), createTokenPayload()));
-
-    IdToken.Payload payload = (IdToken.Payload) token.getClaims();
-    assertTrue(payload.getAudienceAsList().contains(PROJECT_ID));
-    assertEquals(TOKEN_ISSUER, payload.getIssuer());
-    idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-
-    token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), createCookiePayload()));
-    thrown.expectMessage("Firebase ID token has incorrect \"iss\" (issuer) claim.");
-    idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyCookie() throws Exception {
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), createCookiePayload()));
-
-    IdToken.Payload payload = (IdToken.Payload) token.getClaims();
-    assertTrue(payload.getAudienceAsList().contains(PROJECT_ID));
-    assertEquals(COOKIE_ISSUER, payload.getIssuer());
-    cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-
-    token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), createTokenPayload()));
-    thrown.expectMessage("Firebase session cookie has incorrect \"iss\" (issuer) claim.");
-    cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyTokenFailure_MissingKeyId() throws Exception {
-    Header header = createHeader();
-    header.setKeyId(null);
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(header, createTokenPayload()));
-    thrown.expectMessage("Firebase ID token has no \"kid\" claim.");
-    idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyCookieFailure_MissingKeyId() throws Exception {
-    Header header = createHeader();
-    header.setKeyId(null);
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(header, createCookiePayload()));
-    thrown.expectMessage("Firebase session cookie has no \"kid\" claim.");
-    cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyTokenFailure_MissingKeyId_CustomToken() throws Exception {
-    Header header = createHeader();
-    header.setKeyId(null);
-    Payload payload = createTokenPayload();
-    payload.setAudience(
-        "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit"
-            + ".v1.IdentityToolkit");
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(FACTORY, createToken(header, payload));
-    thrown.expectMessage("verifyIdToken() expects an ID token, but was given a custom token.");
-    idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyCookieFailure_MissingKeyId_CustomToken() throws Exception {
-    Header header = createHeader();
-    header.setKeyId(null);
-    Payload payload = createCookiePayload();
-    payload.setAudience(
-        "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit"
-            + ".v1.IdentityToolkit");
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(FACTORY, createToken(header, payload));
-    thrown.expectMessage(
-        "verifySessionCookie() expects a session cookie, but was given a custom token.");
-    cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyTokenFailure_IncorrectAlgorithm() throws Exception {
-    Header header = createHeader();
-    header.setAlgorithm("HS256");
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(header, createTokenPayload()));
-    thrown.expectMessage("Firebase ID token has incorrect algorithm.");
-    idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyCookieFailure_IncorrectAlgorithm() throws Exception {
-    Header header = createHeader();
-    header.setAlgorithm("HS256");
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(header, createCookiePayload()));
-    thrown.expectMessage("Firebase session cookie has incorrect algorithm.");
-    cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyTokenFailure_IncorrectAudience() throws Exception {
-    Payload payload = createTokenPayload();
-    payload.setAudience(
-        "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1."
-            + "IdentityToolkit");
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), payload));
-    thrown.expectMessage("Firebase ID token has incorrect \"aud\" (audience) claim.");
-    idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyCookieFailure_IncorrectAudience() throws Exception {
-    Payload payload = createCookiePayload();
-    payload.setAudience(
-        "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1."
-            + "IdentityToolkit");
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), payload));
-    thrown.expectMessage("Firebase session cookie has incorrect \"aud\" (audience) claim.");
-    cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyTokenFailure_IncorrectIssuer() throws Exception {
-    Payload payload = createTokenPayload();
-    payload.setIssuer("https://foobar.google.com/" + PROJECT_ID);
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), payload));
-    thrown.expectMessage("Firebase ID token has incorrect \"iss\" (issuer) claim.");
-    idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyCookieFailure_IncorrectIssuer() throws Exception {
-    Payload payload = createCookiePayload();
-    payload.setIssuer("https://foobar.google.com/" + PROJECT_ID);
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), payload));
-    thrown.expectMessage("Firebase session cookie has incorrect \"iss\" (issuer) claim.");
-    cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyTokenFailure_MissingSubject() throws Exception {
-    Payload payload = createTokenPayload();
-    payload.setSubject(null);
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), payload));
-    thrown.expectMessage("Firebase ID token has no \"sub\" (subject) claim.");
-    idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyCookieFailure_MissingSubject() throws Exception {
-    Payload payload = createCookiePayload();
-    payload.setSubject(null);
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), payload));
-    thrown.expectMessage("Firebase session cookie has no \"sub\" (subject) claim.");
-    cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyTokenFailure_EmptySubject() throws Exception {
-    Payload payload = createTokenPayload();
-    payload.setSubject("");
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), payload));
-    thrown.expectMessage("Firebase ID token has an empty string \"sub\" (subject) claim.");
-    idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyCookieFailure_EmptySubject() throws Exception {
-    Payload payload = createCookiePayload();
-    payload.setSubject("");
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), payload));
-    thrown.expectMessage("Firebase session cookie has an empty string \"sub\" (subject) claim.");
-    cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyTokenFailure_LongSubject() throws Exception {
-    Payload payload = createTokenPayload();
-    payload.setSubject(
-        "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv"
-            + "wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz");
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), payload));
-    thrown.expectMessage(
-        "Firebase ID token has \"sub\" (subject) claim longer than 128 characters.");
-    idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyCookieFailure_LongSubject() throws Exception {
-    Payload payload = createCookiePayload();
-    payload.setSubject(
-        "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv"
-            + "wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz");
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), payload));
-    thrown.expectMessage(
-        "Firebase session cookie has \"sub\" (subject) claim longer than 128 characters.");
-    cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyTokenFailure_NotYetIssued() throws Exception {
-    Payload payload = createTokenPayload();
-    payload.setIssuedAtTimeSeconds(System.currentTimeMillis() / 1000);
-    payload.setExpirationTimeSeconds(System.currentTimeMillis() / 1000 + 3600);
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), payload));
-    thrown.expectMessage("Firebase ID token has expired or is not yet valid.");
-    idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyCookieFailure_NotYetIssued() throws Exception {
-    Payload payload = createCookiePayload();
-    payload.setIssuedAtTimeSeconds(System.currentTimeMillis() / 1000);
-    payload.setExpirationTimeSeconds(System.currentTimeMillis() / 1000 + 3600);
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), payload));
-    thrown.expectMessage("Firebase session cookie has expired or is not yet valid.");
-    cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyTokenFailure_Expired() throws Exception {
-    Payload payload = createTokenPayload();
-    payload.setIssuedAtTimeSeconds(0L);
-    payload.setExpirationTimeSeconds(3600L);
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), payload));
-    thrown.expectMessage("Firebase ID token has expired or is not yet valid.");
-    idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyCookieFailure_Expired() throws Exception {
-    Payload payload = createCookiePayload();
-    payload.setIssuedAtTimeSeconds(0L);
-    payload.setExpirationTimeSeconds(3600L);
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), payload));
-    thrown.expectMessage("Firebase session cookie has expired or is not yet valid.");
-    cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyTokenFailure_WrongCert() throws Exception {
-    initCrypto(ServiceAccount.OWNER.getPrivateKey(), ServiceAccount.NONE.getCert());
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), createTokenPayload()));
-    thrown.expectMessage("Firebase ID token isn't signed by a valid public key.");
-    idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-
-    token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), createCookiePayload()));
-    thrown.expectMessage("Firebase session cookie isn't signed by a valid public key.");
-    cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-
-  @Test
-  public void verifyTokenCertificateError() throws Exception {
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), createTokenPayload()));
-
-    MockHttpTransport mockTransport = new MockHttpTransport() {
-      @Override
-      public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
-        throw new IOException("Expected error");
-      }
-    };
-    KeyManagers keyManagers = KeyManagers.getDefault(mockTransport, CLOCK);
-    FirebaseTokenVerifier verifier = FirebaseTokenVerifier.createIdTokenVerifier(
-        PROJECT_ID, keyManagers, CLOCK);
-    try {
-      verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-      Assert.fail("No exception thrown");
-    } catch (FirebaseAuthException expected) {
-      assertTrue(expected.getCause() instanceof IOException);
-      assertEquals("Expected error", expected.getCause().getMessage());
-    }
-
-    token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(
-            FACTORY, createToken(createHeader(), createCookiePayload()));
-    verifier = FirebaseTokenVerifier.createSessionCookieVerifier(PROJECT_ID, keyManagers, CLOCK);
-    try {
-      verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-      Assert.fail("No exception thrown");
-    } catch (FirebaseAuthException expected) {
-      assertTrue(expected.getCause() instanceof IOException);
-      assertEquals("Expected error", expected.getCause().getMessage());
-    }
-  }
-
-  @Test
-  public void legacyCustomToken() throws Exception {
-    initCrypto(ServiceAccount.OWNER.getPrivateKey(), ServiceAccount.NONE.getCert());
-    FirebaseToken token =
-        TestOnlyImplFirebaseAuthTrampolines.parseToken(FACTORY, LEGACY_CUSTOM_TOKEN);
-    thrown.expectMessage(
-        "verifyIdToken() expects an ID token, but was given a legacy custom token.");
-    idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
-  }
-}