Repository: firebase/firebase-admin-java
Pull request #362 (2020-02-10 20:01:38)

Error handling revamp for token verification APIs
Implementing the error handling revamp of the token verification APIs (`verifyIdToken()` and `verifySessionCookie()`)

1. Introduced several new Auth error codes to cover the new use cases.
2. Removed the old string error code from `FirebaseAuthException`. That's no longer used anywhere.
3. Added unit tests for token revocation checks, which we were missing until now.


Patch:
diff --git a/src/main/java/com/google/firebase/auth/AuthErrorCode.java b/src/main/java/com/google/firebase/auth/AuthErrorCode.java
index 83287d7..463c243 100644
--- a/src/main/java/com/google/firebase/auth/AuthErrorCode.java
+++ b/src/main/java/com/google/firebase/auth/AuthErrorCode.java
@@ -21,21 +21,56 @@ package com.google.firebase.auth;
  */
 public enum AuthErrorCode {
 
+  /**
+   * Failed to retrieve public key certificates required to verify JWTs.
+   */
+  CERTIFICATE_FETCH_FAILED,
+
   /**
    * A user already exists with the provided email.
    */
   EMAIL_ALREADY_EXISTS,
 
+  /**
+   * The specified ID token is expired.
+   */
+  EXPIRED_ID_TOKEN,
+
+  /**
+   * The specified session cookie is expired.
+   */
+  EXPIRED_SESSION_COOKIE,
+
   /**
    * The provided dynamic link domain is not configured or authorized for the current project.
    */
   INVALID_DYNAMIC_LINK_DOMAIN,
 
+  /**
+   * The specified ID token is invalid.
+   */
+  INVALID_ID_TOKEN,
+
+  /**
+   * The specified session cookie is invalid.
+   */
+  INVALID_SESSION_COOKIE,
+
   /**
    * A user already exists with the provided phone number.
    */
   PHONE_NUMBER_ALREADY_EXISTS,
 
+  /**
+   * The specified ID token has been revoked.
+   */
+  REVOKED_ID_TOKEN,
+
+  /**
+   * The specified session cookie has been revoked.
+   */
+  REVOKED_SESSION_COOKIE,
+
   /**
    * A user already exists with the provided UID.
    */
diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuthException.java b/src/main/java/com/google/firebase/auth/FirebaseAuthException.java
index bd7f60e..f50bc1f 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseAuthException.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseAuthException.java
@@ -32,7 +32,6 @@ import com.google.firebase.internal.Nullable;
 public class FirebaseAuthException extends FirebaseException {
 
   private final AuthErrorCode errorCode;
-  private final String deprecatedErrorCode;
 
   FirebaseAuthException(
       @NonNull ErrorCode errorCode,
@@ -42,12 +41,6 @@ public class FirebaseAuthException extends FirebaseException {
       AuthErrorCode authErrorCode) {
     super(errorCode, message, cause, response);
     this.errorCode = authErrorCode;
-    this.deprecatedErrorCode = null;
-  }
-
-  @Deprecated
-  public FirebaseAuthException(@NonNull String errorCode, @NonNull String detailMessage) {
-    this(errorCode, detailMessage, null);
   }
 
   @Deprecated
@@ -56,17 +49,10 @@ public class FirebaseAuthException extends FirebaseException {
     super(detailMessage, throwable);
     checkArgument(!Strings.isNullOrEmpty(errorCode));
     this.errorCode = null;
-    this.deprecatedErrorCode = errorCode;
   }
 
   @Nullable
   public AuthErrorCode getAuthErrorCode() {
     return errorCode;
   }
-
-  /** Returns an error code that may provide more information about the error. */
-  @Deprecated
-  public String getDeprecatedErrorCode() {
-    return deprecatedErrorCode;
-  }
 }
diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java
index dbb5628..cff1a6c 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java
@@ -82,6 +82,8 @@ final class FirebaseTokenUtils {
         .setJsonFactory(app.getOptions().getJsonFactory())
         .setPublicKeysManager(publicKeysManager)
         .setIdTokenVerifier(idTokenVerifier)
+        .setInvalidTokenErrorCode(AuthErrorCode.INVALID_ID_TOKEN)
+        .setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_ID_TOKEN)
         .build();
   }
 
@@ -100,6 +102,8 @@ final class FirebaseTokenUtils {
         .setShortName("session cookie")
         .setMethod("verifySessionCookie()")
         .setDocUrl("https://firebase.google.com/docs/auth/admin/manage-cookies")
+        .setInvalidTokenErrorCode(AuthErrorCode.INVALID_SESSION_COOKIE)
+        .setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_SESSION_COOKIE)
         .build();
   }
 
diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java
index c164173..b34cfb5 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java
@@ -28,10 +28,12 @@ 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 com.google.firebase.ErrorCode;
 import java.io.IOException;
 import java.math.BigDecimal;
 import java.security.GeneralSecurityException;
 import java.security.PublicKey;
+import java.util.List;
 
 /**
  * The default implementation of the {@link FirebaseTokenVerifier} interface. Uses the Google API
@@ -43,8 +45,6 @@ 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;
@@ -53,6 +53,8 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
   private final String shortName;
   private final String articledShortName;
   private final String docUrl;
+  private final AuthErrorCode invalidTokenErrorCode;
+  private final AuthErrorCode expiredTokenErrorCode;
 
   private FirebaseTokenVerifierImpl(Builder builder) {
     this.jsonFactory = checkNotNull(builder.jsonFactory);
@@ -65,6 +67,8 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
     this.shortName = builder.shortName;
     this.articledShortName = prefixWithIndefiniteArticle(this.shortName);
     this.docUrl = builder.docUrl;
+    this.invalidTokenErrorCode = checkNotNull(builder.invalidTokenErrorCode);
+    this.expiredTokenErrorCode = checkNotNull(builder.expiredTokenErrorCode);
   }
 
   /**
@@ -137,38 +141,28 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
           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);
+      throw newException(detailedError, invalidTokenErrorCode, e);
     }
   }
 
   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);
+    if (!isSignatureValid(token)) {
+      String message = String.format(
+          "Failed to verify the signature of Firebase %s. %s",
+          shortName,
+          getVerifyTokenMessage());
+      throw newException(message, invalidTokenErrorCode);
     }
   }
 
-  private String getErrorIfContentInvalid(final IdToken idToken) {
+  private void checkContents(final IdToken idToken) throws FirebaseAuthException {
     final Header header = idToken.getHeader();
     final Payload payload = idToken.getPayload();
 
+    final long currentTimeMillis = idTokenVerifier.getClock().currentTimeMillis();
     String errorMessage = null;
+    AuthErrorCode errorCode = invalidTokenErrorCode;
+
     if (header.getKeyId() == null) {
       errorMessage = getErrorForTokenWithoutKid(header, payload);
     } else if (!RS256.equals(header.getAlgorithm())) {
@@ -203,14 +197,35 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
       errorMessage = String.format(
           "Firebase %s has \"sub\" (subject) claim longer than 128 characters.",
           shortName);
-    } else if (!verifyTimestamps(idToken)) {
+    } else if (!idToken.verifyExpirationTime(
+        currentTimeMillis, idTokenVerifier.getAcceptableTimeSkewSeconds())) {
       errorMessage = String.format(
-          "Firebase %s has expired or is not yet valid. Get a fresh %s and try again.",
+          "Firebase %s has expired. Get a fresh %s and try again.",
           shortName,
           shortName);
+      // Also set the expired error code.
+      errorCode = expiredTokenErrorCode;
+    } else if (!idToken.verifyIssuedAtTime(
+        currentTimeMillis, idTokenVerifier.getAcceptableTimeSkewSeconds())) {
+      errorMessage = String.format(
+          "Firebase %s is not yet valid.",
+          shortName);
+    }
+
+    if (errorMessage != null) {
+      String detailedError = String.format("%s %s", errorMessage, getVerifyTokenMessage());
+      throw newException(detailedError, errorCode);
     }
+  }
+
+  private FirebaseAuthException newException(String message, AuthErrorCode errorCode) {
+    return newException(message, errorCode, null);
+  }
 
-    return errorMessage;
+  private FirebaseAuthException newException(
+      String message, AuthErrorCode errorCode, Throwable cause) {
+    return new FirebaseAuthException(
+        ErrorCode.INVALID_ARGUMENT, message, cause, null, errorCode);
   }
 
   private String getVerifyTokenMessage() {
@@ -224,15 +239,44 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
    * 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)) {
+  private boolean isSignatureValid(IdToken token) throws FirebaseAuthException {
+    for (PublicKey key : fetchPublicKeys()) {
+      if (isSignatureValid(token, key)) {
         return true;
       }
     }
+
     return false;
   }
 
+  private boolean isSignatureValid(IdToken token, PublicKey key) throws FirebaseAuthException {
+    try {
+      return token.verifySignature(key);
+    } catch (GeneralSecurityException e) {
+      // This doesn't happen under usual circumstances. Seems to only happen if the crypto
+      // setup of the runtime is incorrect in some way.
+      throw new FirebaseAuthException(
+          ErrorCode.UNKNOWN,
+          String.format("Unexpected error while verifying %s: %s", shortName, e.getMessage()),
+          e,
+          null,
+          invalidTokenErrorCode);
+    }
+  }
+
+  private List<PublicKey> fetchPublicKeys() throws FirebaseAuthException {
+    try {
+      return publicKeysManager.getPublicKeys();
+    } catch (GeneralSecurityException | IOException e) {
+      throw new FirebaseAuthException(
+          ErrorCode.UNKNOWN,
+          "Error while fetching public key certificates: " + e.getMessage(),
+          e,
+          null,
+          AuthErrorCode.CERTIFICATE_FETCH_FAILED);
+    }
+  }
+
   private String getErrorForTokenWithoutKid(IdToken.Header header, IdToken.Payload payload) {
     if (isCustomToken(payload)) {
       return String.format("%s expects %s, but was given a custom token.",
@@ -255,11 +299,6 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
         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());
   }
@@ -290,6 +329,8 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
     private String shortName;
     private IdTokenVerifier idTokenVerifier;
     private String docUrl;
+    private AuthErrorCode invalidTokenErrorCode;
+    private AuthErrorCode expiredTokenErrorCode;
 
     private Builder() { }
 
@@ -323,6 +364,16 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
       return this;
     }
 
+    public Builder setInvalidTokenErrorCode(AuthErrorCode invalidTokenErrorCode) {
+      this.invalidTokenErrorCode = invalidTokenErrorCode;
+      return this;
+    }
+
+    public Builder setExpiredTokenErrorCode(AuthErrorCode expiredTokenErrorCode) {
+      this.expiredTokenErrorCode = expiredTokenErrorCode;
+      return this;
+    }
+
     FirebaseTokenVerifierImpl build() {
       return new FirebaseTokenVerifierImpl(this);
     }
diff --git a/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java b/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java
index e53ad25..74cda69 100644
--- a/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java
+++ b/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java
@@ -20,30 +20,27 @@ import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.common.base.Strings;
+import com.google.firebase.ErrorCode;
 
 /**
  * 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 AuthErrorCode errorCode;
   private final String shortName;
 
   private RevocationCheckDecorator(
       FirebaseTokenVerifier tokenVerifier,
       FirebaseUserManager userManager,
-      String errorCode,
+      AuthErrorCode errorCode,
       String shortName) {
     this.tokenVerifier = checkNotNull(tokenVerifier);
     this.userManager = checkNotNull(userManager);
-    checkArgument(!Strings.isNullOrEmpty(errorCode));
+    this.errorCode = checkNotNull(errorCode);
     checkArgument(!Strings.isNullOrEmpty(shortName));
-    this.errorCode = errorCode;
     this.shortName = shortName;
   }
 
@@ -55,8 +52,14 @@ class RevocationCheckDecorator implements FirebaseTokenVerifier {
   public FirebaseToken verifyToken(String token) throws FirebaseAuthException {
     FirebaseToken firebaseToken = tokenVerifier.verifyToken(token);
     if (isRevoked(firebaseToken)) {
-      throw new FirebaseAuthException(errorCode, "Firebase " + shortName + " revoked");
+      throw new FirebaseAuthException(
+          ErrorCode.INVALID_ARGUMENT,
+          "Firebase " + shortName + " is revoked.",
+          null,
+          null,
+          errorCode);
     }
+
     return firebaseToken;
   }
 
@@ -69,12 +72,12 @@ class RevocationCheckDecorator implements FirebaseTokenVerifier {
   static RevocationCheckDecorator decorateIdTokenVerifier(
       FirebaseTokenVerifier tokenVerifier, FirebaseUserManager userManager) {
     return new RevocationCheckDecorator(
-        tokenVerifier, userManager, ID_TOKEN_REVOKED_ERROR, "id token");
+        tokenVerifier, userManager, AuthErrorCode.REVOKED_ID_TOKEN, "id token");
   }
 
   static RevocationCheckDecorator decorateSessionCookieVerifier(
       FirebaseTokenVerifier tokenVerifier, FirebaseUserManager userManager) {
     return new RevocationCheckDecorator(
-        tokenVerifier, userManager, SESSION_COOKIE_REVOKED_ERROR, "session cookie");
+        tokenVerifier, userManager, AuthErrorCode.REVOKED_SESSION_COOKIE, "session cookie");
   }
 }


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 804739e..9b8a582 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
@@ -439,8 +439,8 @@ public class FirebaseAuthIT {
       fail("expecting exception");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(RevocationCheckDecorator.ID_TOKEN_REVOKED_ERROR,
-                   ((FirebaseAuthException) e.getCause()).getDeprecatedErrorCode());
+      assertEquals(AuthErrorCode.REVOKED_ID_TOKEN,
+          ((FirebaseAuthException) e.getCause()).getAuthErrorCode());
     }
     idToken = signInWithCustomToken(customToken);
     decoded = auth.verifyIdTokenAsync(idToken, true).get();
@@ -473,8 +473,8 @@ public class FirebaseAuthIT {
       fail("expecting exception");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(RevocationCheckDecorator.SESSION_COOKIE_REVOKED_ERROR,
-          ((FirebaseAuthException) e.getCause()).getDeprecatedErrorCode());
+      assertEquals(AuthErrorCode.REVOKED_SESSION_COOKIE,
+          ((FirebaseAuthException) e.getCause()).getAuthErrorCode());
     }
 
     idToken = signInWithCustomToken(customToken);
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
index 3bc8ab2..bc84612 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
@@ -20,15 +20,19 @@ import static com.google.common.base.Preconditions.checkNotNull;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
 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.ErrorCode;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.TestOnlyImplFirebaseTrampolines;
@@ -53,6 +57,11 @@ public class FirebaseAuthTest {
       .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream()))
       .build();
 
+  private static final FirebaseAuthException testException = new FirebaseAuthException(
+      ErrorCode.INVALID_ARGUMENT, "Test error message", null, null, null);
+  private static final long VALID_SINCE = 1494364393;
+  public static final String TEST_USER = "testUser";
+
   @After
   public void cleanup() {
     TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
@@ -145,14 +154,14 @@ public class FirebaseAuthTest {
     assertNotNull(FirebaseAuth.getInstance(app));
   }
 
-  @Test(expected = IllegalArgumentException.class)
+  @Test(expected = NullPointerException.class)
   public void testAuthExceptionNullErrorCode() {
-    new FirebaseAuthException(null, "test");
+    new FirebaseAuthException(null, "test", null, null, null);
   }
 
   @Test(expected = IllegalArgumentException.class)
-  public void testAuthExceptionEmptyErrorCode() {
-    new FirebaseAuthException("", "test");
+  public void testAuthExceptionNullMessage() {
+    new FirebaseAuthException(ErrorCode.INTERNAL, null, null, null, null);
   }
 
   @Test
@@ -224,19 +233,48 @@ public class FirebaseAuthTest {
     assertEquals("idtoken", tokenVerifier.getLastTokenString());
   }
 
+  @Test
+  public void testVerifyIdTokenWithRevocationCheck() throws Exception {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
+        getFirebaseToken(VALID_SINCE + 1000));
+    FirebaseAuth auth = getAuthForIdTokenVerificationWithRevocationCheck(tokenVerifier);
+
+    FirebaseToken firebaseToken = auth.verifyIdToken("idtoken", true);
+
+    assertEquals("testUser", firebaseToken.getUid());
+    assertEquals("idtoken", tokenVerifier.getLastTokenString());
+  }
+
+  @Test
+  public void testVerifyIdTokenWithRevocationCheckFailure() {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
+        getFirebaseToken(VALID_SINCE - 1000));
+    FirebaseAuth auth = getAuthForIdTokenVerificationWithRevocationCheck(tokenVerifier);
+
+    try {
+      auth.verifyIdToken("idtoken", true);
+      fail("No error thrown for revoked ID token");
+    } catch (FirebaseAuthException e) {
+      assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCodeNew());
+      assertEquals("Firebase id token is revoked.", e.getMessage());
+      assertNull(e.getCause());
+      assertNull(e.getHttpResponse());
+      assertEquals(AuthErrorCode.REVOKED_ID_TOKEN, e.getAuthErrorCode());
+    }
+
+    assertEquals("idtoken", tokenVerifier.getLastTokenString());
+  }
+
   @Test
   public void testVerifyIdTokenFailure() {
-    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(
-        new FirebaseAuthException("TEST_CODE", "Test error message"));
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException();
     FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier);
 
     try {
       auth.verifyIdToken("idtoken");
       fail("No error thrown for invalid token");
     } catch (FirebaseAuthException authException) {
-      assertEquals("TEST_CODE", authException.getDeprecatedErrorCode());
-      assertEquals("Test error message", authException.getMessage());
-      assertEquals("idtoken", tokenVerifier.getLastTokenString());
+      assertSame(testException, authException);
     }
   }
 
@@ -254,8 +292,7 @@ public class FirebaseAuthTest {
 
   @Test
   public void testVerifyIdTokenAsyncFailure() throws InterruptedException {
-    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(
-        new FirebaseAuthException("TEST_CODE", "Test error message"));
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException();
     FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier);
 
     try {
@@ -263,16 +300,13 @@ public class FirebaseAuthTest {
       fail("No error thrown for invalid token");
     } catch (ExecutionException e) {
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-      assertEquals("TEST_CODE", authException.getDeprecatedErrorCode());
-      assertEquals("Test error message", authException.getMessage());
-      assertEquals("idtoken", tokenVerifier.getLastTokenString());
+      assertSame(testException, authException);
     }
   }
 
   @Test
   public void testVerifyIdTokenWithCheckRevokedAsyncFailure() throws InterruptedException {
-    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(
-        new FirebaseAuthException("TEST_CODE", "Test error message"));
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException();
     FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier);
 
     try {
@@ -280,9 +314,7 @@ public class FirebaseAuthTest {
       fail("No error thrown for invalid token");
     } catch (ExecutionException e) {
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-      assertEquals("TEST_CODE", authException.getDeprecatedErrorCode());
-      assertEquals("Test error message", authException.getMessage());
-      assertEquals("idtoken", tokenVerifier.getLastTokenString());
+      assertSame(testException, authException);
     }
   }
 
@@ -357,20 +389,49 @@ public class FirebaseAuthTest {
 
   @Test
   public void testVerifySessionCookieFailure() {
-    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(
-        new FirebaseAuthException("TEST_CODE", "Test error message"));
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException();
     FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier);
 
     try {
       auth.verifySessionCookie("idtoken");
       fail("No error thrown for invalid token");
     } catch (FirebaseAuthException authException) {
-      assertEquals("TEST_CODE", authException.getDeprecatedErrorCode());
-      assertEquals("Test error message", authException.getMessage());
-      assertEquals("idtoken", tokenVerifier.getLastTokenString());
+      assertSame(testException, authException);
     }
   }
 
+  @Test
+  public void testVerifySessionCookieWithRevocationCheck() throws Exception {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
+        getFirebaseToken(VALID_SINCE + 1000));
+    FirebaseAuth auth = getAuthForSessionCookieVerificationWithRevocationCheck(tokenVerifier);
+
+    FirebaseToken firebaseToken = auth.verifySessionCookie("cookie", true);
+
+    assertEquals("testUser", firebaseToken.getUid());
+    assertEquals("cookie", tokenVerifier.getLastTokenString());
+  }
+
+  @Test
+  public void testVerifySessionCookieWithRevocationCheckFailure() {
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
+        getFirebaseToken(VALID_SINCE - 1000));
+    FirebaseAuth auth = getAuthForSessionCookieVerificationWithRevocationCheck(tokenVerifier);
+
+    try {
+      auth.verifySessionCookie("cookie", true);
+      fail("No error thrown for revoked session cookie");
+    } catch (FirebaseAuthException e) {
+      assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCodeNew());
+      assertEquals("Firebase session cookie is revoked.", e.getMessage());
+      assertNull(e.getCause());
+      assertNull(e.getHttpResponse());
+      assertEquals(AuthErrorCode.REVOKED_SESSION_COOKIE, e.getAuthErrorCode());
+    }
+
+    assertEquals("cookie", tokenVerifier.getLastTokenString());
+  }
+
   @Test
   public void testVerifySessionCookieAsync() throws Exception {
     MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
@@ -385,8 +446,7 @@ public class FirebaseAuthTest {
 
   @Test
   public void testVerifySessionCookieAsyncFailure() throws InterruptedException {
-    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(
-        new FirebaseAuthException("TEST_CODE", "Test error message"));
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException();
     FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier);
 
     try {
@@ -394,16 +454,13 @@ public class FirebaseAuthTest {
       fail("No error thrown for invalid token");
     } catch (ExecutionException e) {
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-      assertEquals("TEST_CODE", authException.getDeprecatedErrorCode());
-      assertEquals("Test error message", authException.getMessage());
-      assertEquals("idtoken", tokenVerifier.getLastTokenString());
+      assertSame(testException, authException);
     }
   }
 
   @Test
   public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws InterruptedException {
-    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(
-        new FirebaseAuthException("TEST_CODE", "Test error message"));
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException();
     FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier);
 
     try {
@@ -411,9 +468,7 @@ public class FirebaseAuthTest {
       fail("No error thrown for invalid token");
     } catch (ExecutionException e) {
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-      assertEquals("TEST_CODE", authException.getDeprecatedErrorCode());
-      assertEquals("Test error message", authException.getMessage());
-      assertEquals("idtoken", tokenVerifier.getLastTokenString());
+      assertSame(testException, authException);
     }
   }
 
@@ -421,6 +476,16 @@ public class FirebaseAuthTest {
     return new FirebaseToken(ImmutableMap.<String, Object>of("sub", subject));
   }
 
+  private FirebaseToken getFirebaseToken(long issuedAt) {
+    return new FirebaseToken(ImmutableMap.<String, Object>of("sub", TEST_USER, "iat", issuedAt));
+  }
+
+  FirebaseAuth getAuthForIdTokenVerificationWithRevocationCheck(
+      FirebaseTokenVerifier tokenVerifier) {
+    FirebaseApp app = getFirebaseAppForUserRetrieval();
+    return getAuthForIdTokenVerification(app, Suppliers.ofInstance(tokenVerifier));
+  }
+
   private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVerifier) {
     return getAuthForIdTokenVerification(Suppliers.ofInstance(tokenVerifier));
   }
@@ -428,6 +493,12 @@ public class FirebaseAuthTest {
   private FirebaseAuth getAuthForIdTokenVerification(
       Supplier<? extends FirebaseTokenVerifier> tokenVerifierSupplier) {
     FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions);
+    return getAuthForIdTokenVerification(app, tokenVerifierSupplier);
+  }
+
+  private FirebaseAuth getAuthForIdTokenVerification(
+      FirebaseApp app,
+      Supplier<? extends FirebaseTokenVerifier> tokenVerifierSupplier) {
     FirebaseUserManager userManager = new FirebaseUserManager(app);
     return FirebaseAuth.builder()
         .setFirebaseApp(app)
@@ -436,6 +507,12 @@ public class FirebaseAuthTest {
         .build();
   }
 
+  FirebaseAuth getAuthForSessionCookieVerificationWithRevocationCheck(
+      FirebaseTokenVerifier tokenVerifier) {
+    FirebaseApp app = getFirebaseAppForUserRetrieval();
+    return getAuthForSessionCookieVerification(app, Suppliers.ofInstance(tokenVerifier));
+  }
+
   private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier tokenVerifier) {
     return getAuthForSessionCookieVerification(Suppliers.ofInstance(tokenVerifier));
   }
@@ -443,6 +520,12 @@ public class FirebaseAuthTest {
   private FirebaseAuth getAuthForSessionCookieVerification(
       Supplier<? extends FirebaseTokenVerifier> tokenVerifierSupplier) {
     FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions);
+    return getAuthForSessionCookieVerification(app, tokenVerifierSupplier);
+  }
+
+  private FirebaseAuth getAuthForSessionCookieVerification(
+      FirebaseApp app,
+      Supplier<? extends FirebaseTokenVerifier> tokenVerifierSupplier) {
     FirebaseUserManager userManager = new FirebaseUserManager(app);
     return FirebaseAuth.builder()
         .setFirebaseApp(app)
@@ -451,6 +534,18 @@ public class FirebaseAuthTest {
         .build();
   }
 
+  private FirebaseApp getFirebaseAppForUserRetrieval() {
+    String getUserResponse = TestUtils.loadResource("getUser.json");
+    MockHttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent(getUserResponse))
+        .build();
+    return FirebaseApp.initializeApp(new FirebaseOptions.Builder()
+        .setCredentials(new MockGoogleCredentials("test-token"))
+        .setHttpTransport(transport)
+        .setProjectId("test-project-id")
+        .build());
+  }
+
   private static class MockTokenVerifier implements FirebaseTokenVerifier {
 
     private String lastTokenString;
@@ -480,8 +575,8 @@ public class FirebaseAuthTest {
       return new MockTokenVerifier(result, null);
     }
 
-    static MockTokenVerifier fromException(FirebaseAuthException exception) {
-      return new MockTokenVerifier(null, exception);
+    static MockTokenVerifier fromException() {
+      return new MockTokenVerifier(null, testException);
     }
   }
 
diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java
index b10817a..83b28ec 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java
@@ -17,6 +17,7 @@
 package com.google.firebase.auth;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import com.google.api.client.auth.openidconnect.IdTokenVerifier;
@@ -29,15 +30,15 @@ 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.ErrorCode;
 import com.google.firebase.testing.ServiceAccount;
 import java.io.IOException;
+import java.security.GeneralSecurityException;
 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 {
 
@@ -54,9 +55,6 @@ public class FirebaseTokenVerifierImplTest {
 
   private static final String TEST_TOKEN_ISSUER = "https://test.token.issuer";
 
-  @Rule
-  public ExpectedException thrown = ExpectedException.none();
-
   private FirebaseTokenVerifier tokenVerifier;
   private TestTokenFactory tokenFactory;
 
@@ -79,108 +77,188 @@ public class FirebaseTokenVerifierImplTest {
   }
 
   @Test
-  public void testVerifyTokenWithoutKeyId() throws Exception {
+  public void testVerifyTokenWithoutKeyId() {
     String token = createTokenWithoutKeyId();
 
-    thrown.expectMessage("Firebase test token has no \"kid\" claim.");
-    tokenVerifier.verifyToken(token);
+    try {
+      tokenVerifier.verifyToken(token);
+    } catch (FirebaseAuthException e) {
+      String message = "Firebase test token has no \"kid\" claim. "
+          + "See https://test.doc.url for details on how to retrieve a test token.";
+      checkInvalidTokenException(e, message);
+    }
   }
 
   @Test
-  public void testVerifyTokenFirebaseCustomToken() throws Exception {
+  public void testVerifyTokenFirebaseCustomToken() {
     String token = createCustomToken();
 
-    thrown.expectMessage("verifyTestToken() expects a test token, but was given a custom token.");
-    tokenVerifier.verifyToken(token);
+    try {
+      tokenVerifier.verifyToken(token);
+    } catch (FirebaseAuthException e) {
+      String message = "verifyTestToken() expects a test token, but was given a custom token. "
+          + "See https://test.doc.url for details on how to retrieve a test token.";
+      checkInvalidTokenException(e, message);
+    }
   }
 
   @Test
-  public void testVerifyTokenIncorrectAlgorithm() throws Exception {
+  public void testVerifyTokenIncorrectAlgorithm() {
     String token = createTokenWithIncorrectAlgorithm();
 
-    thrown.expectMessage("Firebase test token has incorrect algorithm.");
-    tokenVerifier.verifyToken(token);
+    try {
+      tokenVerifier.verifyToken(token);
+    } catch (FirebaseAuthException e) {
+      String message = "Firebase test token has incorrect algorithm. "
+          + "Expected \"RS256\" but got \"HSA\". "
+          + "See https://test.doc.url for details on how to retrieve a test token.";
+      checkInvalidTokenException(e, message);
+    }
   }
 
   @Test
-  public void testVerifyTokenIncorrectAudience() throws Exception {
+  public void testVerifyTokenIncorrectAudience() {
     String token = createTokenWithIncorrectAudience();
 
-    thrown.expectMessage("Firebase test token has incorrect \"aud\" (audience) claim.");
-    tokenVerifier.verifyToken(token);
+    try {
+      tokenVerifier.verifyToken(token);
+    } catch (FirebaseAuthException e) {
+      String message = "Firebase test token has incorrect \"aud\" (audience) claim. "
+          + "Expected \"proj-test-101\" but got \"invalid-audience\". "
+          + "Make sure the test token comes from the same Firebase project as the service account "
+          + "used to authenticate this SDK. "
+          + "See https://test.doc.url for details on how to retrieve a test token.";
+      checkInvalidTokenException(e, message);
+    }
   }
 
   @Test
-  public void testVerifyTokenIncorrectIssuer() throws Exception {
+  public void testVerifyTokenIncorrectIssuer() {
     String token = createTokenWithIncorrectIssuer();
 
-    thrown.expectMessage("Firebase test token has incorrect \"iss\" (issuer) claim.");
-    tokenVerifier.verifyToken(token);
+    try {
+      tokenVerifier.verifyToken(token);
+    } catch (FirebaseAuthException e) {
+      String message = "Firebase test token has incorrect \"iss\" (issuer) claim. "
+          + "Expected \"https://test.token.issuer\" but got "
+          + "\"https://incorrect.issuer.prefix/proj-test-101\". Make sure the test token comes "
+          + "from the same Firebase project as the service account used to authenticate this SDK. "
+          + "See https://test.doc.url for details on how to retrieve a test token.";
+      checkInvalidTokenException(e, message);
+    }
   }
 
   @Test
-  public void testVerifyTokenMissingSubject() throws Exception {
+  public void testVerifyTokenMissingSubject() {
     String token = createTokenWithSubject(null);
 
-    thrown.expectMessage("Firebase test token has no \"sub\" (subject) claim.");
-    tokenVerifier.verifyToken(token);
+    try {
+      tokenVerifier.verifyToken(token);
+    } catch (FirebaseAuthException e) {
+      String message = "Firebase test token has no \"sub\" (subject) claim. "
+          + "See https://test.doc.url for details on how to retrieve a test token.";
+      checkInvalidTokenException(e, message);
+    }
   }
 
   @Test
-  public void testVerifyTokenEmptySubject() throws Exception {
+  public void testVerifyTokenEmptySubject() {
     String token = createTokenWithSubject("");
 
-    thrown.expectMessage("Firebase test token has an empty string \"sub\" (subject) claim.");
-    tokenVerifier.verifyToken(token);
+    try {
+      tokenVerifier.verifyToken(token);
+    } catch (FirebaseAuthException e) {
+      String message = "Firebase test token has an empty string \"sub\" (subject) claim. "
+          + "See https://test.doc.url for details on how to retrieve a test token.";
+      checkInvalidTokenException(e, message);
+    }
   }
 
   @Test
-  public void testVerifyTokenLongSubject() throws Exception {
+  public void testVerifyTokenLongSubject() {
     String token = createTokenWithSubject(Strings.repeat("a", 129));
 
-    thrown.expectMessage(
-        "Firebase test token has \"sub\" (subject) claim longer than 128 characters.");
-    tokenVerifier.verifyToken(token);
+    try {
+      tokenVerifier.verifyToken(token);
+    } catch (FirebaseAuthException e) {
+      String message = "Firebase test token has \"sub\" (subject) claim longer "
+          + "than 128 characters. "
+          + "See https://test.doc.url for details on how to retrieve a test token.";
+      checkInvalidTokenException(e, message);
+    }
   }
 
   @Test
-  public void testVerifyTokenIssuedAtInFuture() throws Exception {
+  public void testVerifyTokenIssuedAtInFuture() {
     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);
+    try {
+      tokenVerifier.verifyToken(token);
+    } catch (FirebaseAuthException e) {
+      String message = "Firebase test token is not yet valid. "
+          + "See https://test.doc.url for details on how to retrieve a test token.";
+      checkInvalidTokenException(e, message);
+    }
   }
 
   @Test
-  public void testVerifyTokenExpired() throws Exception {
+  public void testVerifyTokenExpired() {
     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);
+    try {
+      tokenVerifier.verifyToken(token);
+    } catch (FirebaseAuthException e) {
+      String message = "Firebase test token has expired. "
+          + "Get a fresh test token and try again. "
+          + "See https://test.doc.url for details on how to retrieve a test token.";
+      checkException(e, message, AuthErrorCode.EXPIRED_ID_TOKEN);
+    }
   }
 
   @Test
-  public void testVerifyTokenIncorrectCert() throws Exception {
+  public void testVerifyTokenSignatureMismatch() {
     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);
+    try {
+      tokenVerifier.verifyToken(token);
+    } catch (FirebaseAuthException e) {
+      String message = "Failed to verify the signature of Firebase test token. "
+          + "See https://test.doc.url for details on how to retrieve a test token.";
+      checkInvalidTokenException(e, message);
+    }
+  }
+
+  @Test
+  public void testMalformedCert() {
+    String token = tokenFactory.createToken();
+    GooglePublicKeysManager publicKeysManager = newPublicKeysManager("malformed.cert");
+    FirebaseTokenVerifier tokenVerifier = newTestTokenVerifier(publicKeysManager);
+
+    try {
+      tokenVerifier.verifyToken(token);
+    } catch (FirebaseAuthException e) {
+      String message = "Error while fetching public key certificates: Could not parse certificate";
+      assertEquals(ErrorCode.UNKNOWN, e.getErrorCodeNew());
+      assertTrue(e.getMessage().startsWith(message));
+      assertTrue(e.getCause() instanceof GeneralSecurityException);
+      assertNull(e.getHttpResponse());
+      assertEquals(AuthErrorCode.CERTIFICATE_FETCH_FAILED, e.getAuthErrorCode());
+    }
   }
 
   @Test
-  public void verifyTokenCertificateError() {
+  public void testCertificateFetchError() {
     MockHttpTransport failingTransport = new MockHttpTransport() {
       @Override
       public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
@@ -194,26 +272,57 @@ public class FirebaseTokenVerifierImplTest {
     try {
       idTokenVerifier.verifyToken(token);
       Assert.fail("No exception thrown");
-    } catch (FirebaseAuthException expected) {
-      assertTrue(expected.getCause() instanceof IOException);
-      assertEquals("Expected error", expected.getCause().getMessage());
+    } catch (FirebaseAuthException e) {
+      String message = "Error while fetching public key certificates: Expected error";
+      assertEquals(ErrorCode.UNKNOWN, e.getErrorCodeNew());
+      assertEquals(message, e.getMessage());
+      assertTrue(e.getCause() instanceof IOException);
+      assertNull(e.getHttpResponse());
+      assertEquals(AuthErrorCode.CERTIFICATE_FETCH_FAILED, e.getAuthErrorCode());
     }
   }
 
   @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);
+  public void testMalformedSignature() {
+    String token = tokenFactory.createToken();
+    String[] segments = token.split("\\.");
+    token = String.format("%s.%s.%s", segments[0], segments[1], "MalformedSignature");
+
+    try {
+      tokenVerifier.verifyToken(token);
+    } catch (FirebaseAuthException e) {
+      String message = "Failed to verify the signature of Firebase test token. "
+          + "See https://test.doc.url for details on how to retrieve a test token.";
+      checkInvalidTokenException(e, message);
+    }
   }
 
   @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");
+  public void testLegacyCustomToken() {
+    try {
+      tokenVerifier.verifyToken(LEGACY_CUSTOM_TOKEN);
+    } catch (FirebaseAuthException e) {
+      String message = "verifyTestToken() expects a test token, but was given a "
+          + "legacy custom token. "
+          + "See https://test.doc.url for details on how to retrieve a test token.";
+      checkInvalidTokenException(e, message);
+    }
+  }
+
+  @Test
+  public void testMalformedToken() {
+    try {
+      tokenVerifier.verifyToken("not.a.jwt");
+    } catch (FirebaseAuthException e) {
+      String message = "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.";
+      assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCodeNew());
+      assertEquals(message, e.getMessage());
+      assertTrue(e.getCause() instanceof IllegalArgumentException);
+      assertNull(e.getHttpResponse());
+      assertEquals(AuthErrorCode.INVALID_ID_TOKEN, e.getAuthErrorCode());
+    }
   }
 
   @Test(expected = NullPointerException.class)
@@ -272,6 +381,8 @@ public class FirebaseTokenVerifierImplTest {
         .setJsonFactory(TestTokenFactory.JSON_FACTORY)
         .setPublicKeysManager(publicKeysManager)
         .setIdTokenVerifier(newIdTokenVerifier())
+        .setInvalidTokenErrorCode(AuthErrorCode.INVALID_ID_TOKEN)
+        .setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_ID_TOKEN)
         .build();
   }
 
@@ -337,4 +448,16 @@ public class FirebaseTokenVerifierImplTest {
     payload.setExpirationTimeSeconds(expirationSeconds);
     return tokenFactory.createToken(payload);
   }
+
+  private void checkInvalidTokenException(FirebaseAuthException e, String message) {
+    checkException(e, message, AuthErrorCode.INVALID_ID_TOKEN);
+  }
+
+  private void checkException(FirebaseAuthException e, String message, AuthErrorCode errorCode) {
+    assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCodeNew());
+    assertEquals(message, e.getMessage());
+    assertNull(e.getCause());
+    assertNull(e.getHttpResponse());
+    assertEquals(errorCode, e.getAuthErrorCode());
+  }
 }
diff --git a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java
index 24b90ad..c26845d 100644
--- a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java
+++ b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java
@@ -18,6 +18,7 @@ package com.google.firebase.snippets;
 
 import com.google.common.io.BaseEncoding;
 import com.google.firebase.auth.ActionCodeSettings;
+import com.google.firebase.auth.AuthErrorCode;
 import com.google.firebase.auth.ErrorInfo;
 import com.google.firebase.auth.ExportedUserRecord;
 import com.google.firebase.auth.FirebaseAuth;
@@ -254,7 +255,7 @@ public class FirebaseAuthSnippets {
       // Token is valid and not revoked.
       String uid = decodedToken.getUid();
     } catch (FirebaseAuthException e) {
-      if (e.getDeprecatedErrorCode().equals("id-token-revoked")) {
+      if (e.getAuthErrorCode() == AuthErrorCode.REVOKED_ID_TOKEN) {
         // Token has been revoked. Inform the user to re-authenticate or signOut() the user.
       } else {
         // Token is invalid.