Repository: firebase/firebase-admin-java
Pull request #523 (2021-04-08 15:25:29)

feat(auth): Add auth emulator support
### Discussion

Add support to communicate with the Auth Emulator #493.

### Testing

  * Unit tests.
  * Manual testing.

### API Changes

  * N/A.



Patch:
diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
index fc9c8df..0967aca 100644
--- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
@@ -18,6 +18,7 @@ package com.google.firebase.auth;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.firebase.auth.internal.Utils.isEmulatorMode;
 
 import com.google.api.client.json.JsonFactory;
 import com.google.api.client.util.Clock;
@@ -309,7 +310,7 @@ public abstract class AbstractFirebaseAuth {
   @VisibleForTesting
   FirebaseTokenVerifier getIdTokenVerifier(boolean checkRevoked) {
     FirebaseTokenVerifier verifier = idTokenVerifier.get();
-    if (checkRevoked) {
+    if (checkRevoked || isEmulatorMode()) {
       FirebaseUserManager userManager = getUserManager();
       verifier = RevocationCheckDecorator.decorateIdTokenVerifier(verifier, userManager);
     }
@@ -389,7 +390,7 @@ public abstract class AbstractFirebaseAuth {
   @VisibleForTesting
   FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) {
     FirebaseTokenVerifier verifier = cookieVerifier.get();
-    if (checkRevoked) {
+    if (checkRevoked || isEmulatorMode()) {
       FirebaseUserManager userManager = getUserManager();
       verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager);
     }
diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java
index bbe741f..9a3e1ae 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java
@@ -29,6 +29,7 @@ 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 com.google.firebase.auth.internal.Utils;
 import com.google.firebase.internal.Nullable;
 import java.io.IOException;
 import java.math.BigDecimal;
@@ -94,9 +95,12 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
    */
   @Override
   public FirebaseToken verifyToken(String token) throws FirebaseAuthException {
+    boolean isEmulatorMode = Utils.isEmulatorMode();
     IdToken idToken = parse(token);
-    checkContents(idToken);
-    checkSignature(idToken);
+    checkContents(idToken, isEmulatorMode);
+    if (!isEmulatorMode) {
+      checkSignature(idToken);
+    }
     FirebaseToken firebaseToken = new FirebaseToken(idToken.getPayload());
     checkTenantId(firebaseToken);
     return firebaseToken;
@@ -160,7 +164,8 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
     }
   }
 
-  private void checkContents(final IdToken idToken) throws FirebaseAuthException {
+  private void checkContents(final IdToken idToken, boolean isEmulatorMode)
+      throws FirebaseAuthException {
     final Header header = idToken.getHeader();
     final Payload payload = idToken.getPayload();
 
@@ -168,9 +173,9 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier {
     String errorMessage = null;
     AuthErrorCode errorCode = invalidTokenErrorCode;
 
-    if (header.getKeyId() == null) {
+    if (!isEmulatorMode && header.getKeyId() == null) {
       errorMessage = getErrorForTokenWithoutKid(header, payload);
-    } else if (!RS256.equals(header.getAlgorithm())) {
+    } else if (!isEmulatorMode && !RS256.equals(header.getAlgorithm())) {
       errorMessage = String.format(
           "Firebase %s has incorrect algorithm. Expected \"%s\" but got \"%s\".",
           shortName,
diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
index 554d017..103360c 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -41,6 +41,7 @@ import com.google.firebase.auth.internal.GetAccountInfoResponse;
 import com.google.firebase.auth.internal.ListOidcProviderConfigsResponse;
 import com.google.firebase.auth.internal.ListSamlProviderConfigsResponse;
 import com.google.firebase.auth.internal.UploadAccountResponse;
+import com.google.firebase.auth.internal.Utils;
 import com.google.firebase.internal.ApiClientUtils;
 import com.google.firebase.internal.HttpRequestInfo;
 import com.google.firebase.internal.NonNull;
@@ -72,6 +73,8 @@ final class FirebaseUserManager {
 
   private static final String ID_TOOLKIT_URL =
       "https://identitytoolkit.googleapis.com/%s/projects/%s";
+  private static final String ID_TOOLKIT_URL_EMULATOR =
+          "http://%s/identitytoolkit.googleapis.com/%s/projects/%s";
 
   private final String userMgtBaseUrl;
   private final String idpConfigMgtBaseUrl;
@@ -85,8 +88,8 @@ final class FirebaseUserManager {
             + "set the project ID explicitly via FirebaseOptions. Alternatively you can also "
             + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.");
     this.jsonFactory = checkNotNull(builder.jsonFactory, "JsonFactory must not be null");
-    final String idToolkitUrlV1 = String.format(ID_TOOLKIT_URL, "v1", projectId);
-    final String idToolkitUrlV2 = String.format(ID_TOOLKIT_URL, "v2", projectId);
+    final String idToolkitUrlV1 = getIdToolkitUrl(projectId, "v1");
+    final String idToolkitUrlV2 = getIdToolkitUrl(projectId, "v2");
     final String tenantId = builder.tenantId;
     if (tenantId == null) {
       this.userMgtBaseUrl = idToolkitUrlV1;
@@ -100,6 +103,13 @@ final class FirebaseUserManager {
     this.httpClient = new AuthHttpClient(jsonFactory, builder.requestFactory);
   }
 
+  private String getIdToolkitUrl(String projectId, String version) {
+    if (Utils.isEmulatorMode()) {
+      return String.format(ID_TOOLKIT_URL_EMULATOR, Utils.getEmulatorHost(), version, projectId);
+    }
+    return String.format(ID_TOOLKIT_URL, version, projectId);
+  }
+
   @VisibleForTesting
   void setInterceptor(HttpResponseInterceptor interceptor) {
     httpClient.setInterceptor(interceptor);
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 f167b09..543f955 100644
--- a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java
+++ b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java
@@ -22,6 +22,7 @@ import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseException;
 import com.google.firebase.ImplFirebaseTrampolines;
 import com.google.firebase.auth.FirebaseAuthException;
+import com.google.firebase.auth.internal.Utils;
 import com.google.firebase.internal.AbstractPlatformErrorHandler;
 import com.google.firebase.internal.ApiClientUtils;
 import com.google.firebase.internal.ErrorHandlingHttpClient;
@@ -108,6 +109,25 @@ public class CryptoSigners {
     }
   }
 
+  /**
+   * A {@link CryptoSigner} implementation that doesn't sign data. For use with the Auth Emulator
+   * only
+   */
+  public static class EmulatorCryptoSigner implements CryptoSigner {
+
+    private static final String ACCOUNT = "firebase-auth-emulator@example.com";
+
+    @Override
+    public byte[] sign(byte[] payload) {
+      return "".getBytes();
+    }
+
+    @Override
+    public String getAccount() {
+      return ACCOUNT;
+    }
+  }
+
   private static class IAMErrorHandler
       extends AbstractPlatformErrorHandler<FirebaseAuthException> {
 
@@ -126,6 +146,10 @@ public class CryptoSigners {
    * documented at go/firebase-admin-sign.
    */
   public static CryptoSigner getCryptoSigner(FirebaseApp firebaseApp) throws IOException {
+    if (Utils.isEmulatorMode()) {
+      return new EmulatorCryptoSigner();
+    }
+
     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/Utils.java b/src/main/java/com/google/firebase/auth/internal/Utils.java
new file mode 100644
index 0000000..487892d
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/internal/Utils.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright  2021 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 com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+
+public class Utils {
+  @VisibleForTesting
+  public static final String AUTH_EMULATOR_HOST = "FIREBASE_AUTH_EMULATOR_HOST";
+
+  public static boolean isEmulatorMode() {
+    return !Strings.isNullOrEmpty(getEmulatorHost());
+  }
+
+  public static String getEmulatorHost() {
+    return System.getenv(AUTH_EMULATOR_HOST);
+  }
+
+}
diff --git a/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java
index 5b776a4..56ef04e 100644
--- a/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java
+++ b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java
@@ -31,6 +31,7 @@ import com.google.firebase.ImplFirebaseTrampolines;
 import com.google.firebase.auth.FirebaseAuthException;
 import com.google.firebase.auth.internal.AuthHttpClient;
 import com.google.firebase.auth.internal.ListTenantsResponse;
+import com.google.firebase.auth.internal.Utils;
 import com.google.firebase.internal.ApiClientUtils;
 import com.google.firebase.internal.HttpRequestInfo;
 import java.util.Map;
@@ -42,6 +43,9 @@ final class FirebaseTenantClient {
   private static final String ID_TOOLKIT_URL =
       "https://identitytoolkit.googleapis.com/%s/projects/%s";
 
+  private static final String ID_TOOLKIT_URL_EMULATOR =
+          "http://%s/identitytoolkit.googleapis.com/%s/projects/%s";
+
   private final String tenantMgtBaseUrl;
   private final AuthHttpClient httpClient;
 
@@ -58,10 +62,17 @@ final class FirebaseTenantClient {
         "Project ID is required to access the auth service. Use a service account credential or "
             + "set the project ID explicitly via FirebaseOptions. Alternatively you can also "
             + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.");
-    this.tenantMgtBaseUrl = String.format(ID_TOOLKIT_URL, "v2", projectId);
+    this.tenantMgtBaseUrl = getTenantMgtBaseUrl(projectId);
     this.httpClient = new AuthHttpClient(jsonFactory, requestFactory);
   }
 
+  private String getTenantMgtBaseUrl(String projectId) {
+    if (Utils.isEmulatorMode()) {
+      return String.format(ID_TOOLKIT_URL_EMULATOR, Utils.getEmulatorHost(), "v2", projectId);
+    }
+    return String.format(ID_TOOLKIT_URL, "v2", projectId);
+  }
+
   void setInterceptor(HttpResponseInterceptor interceptor) {
     httpClient.setInterceptor(interceptor);
   }


Test patch:
diff --git a/src/test/java/com/google/firebase/auth/EmulatorFirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/EmulatorFirebaseTokenVerifierImplTest.java
new file mode 100644
index 0000000..0cc12c6
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/EmulatorFirebaseTokenVerifierImplTest.java
@@ -0,0 +1,321 @@
+/*
+ * 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.firebase.auth.FirebaseTokenVerifierImplTestUtils.TEST_TOKEN_ISSUER;
+import static com.google.firebase.auth.FirebaseTokenVerifierImplTestUtils.checkInvalidTokenException;
+import static com.google.firebase.auth.FirebaseTokenVerifierImplTestUtils.fullyPopulatedBuilder;
+import static com.google.firebase.auth.FirebaseTokenVerifierImplTestUtils.newPublicKeysManager;
+import static com.google.firebase.auth.FirebaseTokenVerifierImplTestUtils.newTestTokenVerifier;
+import static com.google.firebase.auth.internal.Utils.AUTH_EMULATOR_HOST;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.firebase.ErrorCode;
+import com.google.firebase.testing.ServiceAccount;
+import com.google.firebase.testing.TestUtils;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class EmulatorFirebaseTokenVerifierImplTest {
+  private FirebaseTokenVerifier tokenVerifier;
+  private TestTokenFactory tokenFactory;
+
+  @Before
+  public void setUp() throws Exception {
+    // Set the Auth Emulator host prior to initialization
+    TestUtils.setEnvironmentVariables(ImmutableMap.of(AUTH_EMULATOR_HOST, "localhost:9099"));
+    ServiceAccount serviceAccount = ServiceAccount.EDITOR;
+    GooglePublicKeysManager publicKeysManager = newPublicKeysManager(serviceAccount.getCert());
+    this.tokenVerifier = newTestTokenVerifier(publicKeysManager);
+    this.tokenFactory = new TestTokenFactory(serviceAccount.getPrivateKey(), TEST_TOKEN_ISSUER);
+  }
+
+  @After
+  public void tearDown() {
+    TestUtils.unsetEnvironmentVariables(ImmutableSet.of(AUTH_EMULATOR_HOST));
+  }
+
+  @Test
+  public void testVerifyToken() throws Exception {
+    String token = tokenFactory.createUnsignedTokenForEmulator();
+
+    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();
+
+    // Should not throw, even if missing kid
+    tokenVerifier.verifyToken(token);
+  }
+
+  @Test
+  public void testVerifyTokenIncorrectAlgorithm() throws Exception {
+    String token = createTokenWithIncorrectAlgorithm();
+
+    // Should not throw, even if missing algorithm
+    tokenVerifier.verifyToken(token);
+  }
+
+  @Test
+  public void testVerifyTokenWithoutAlgorithm() throws Exception {
+    String token = createTokenWithoutAlgorithm();
+
+    // Should not throw, even if algorithm is none
+    tokenVerifier.verifyToken(token);
+  }
+
+  @Test
+  public void testVerifyTokenIncorrectAudience() {
+    String token = createTokenWithIncorrectAudience();
+
+    try {
+      tokenVerifier.verifyToken(token);
+      fail("No error thrown for incorrect audience");
+    } catch (FirebaseAuthException e) {
+      String message = String.format("Firebase test token has incorrect \"aud\" (audience) claim. "
+              + "Expected \"%s\" 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.",
+          TestTokenFactory.PROJECT_ID);
+      checkInvalidTokenException(e, message);
+    }
+  }
+
+  @Test
+  public void testVerifyTokenIncorrectIssuer() {
+    String token = createTokenWithIncorrectIssuer();
+
+    try {
+      tokenVerifier.verifyToken(token);
+      fail("No error thrown for incorrect issuer");
+    } catch (FirebaseAuthException e) {
+      String message = String.format("Firebase test token has incorrect \"iss\" (issuer) claim. "
+              + "Expected \"%s\" but got "
+              + "\"https://incorrect.issuer.prefix/%s\". 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.",
+          TEST_TOKEN_ISSUER, TestTokenFactory.PROJECT_ID);
+      checkInvalidTokenException(e, message);
+    }
+  }
+
+  @Test
+  public void testVerifyTokenMissingSubject() {
+    String token = createTokenWithSubject(null);
+
+    try {
+      tokenVerifier.verifyToken(token);
+      fail("No error thrown for missing subject");
+    } 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() {
+    String token = createTokenWithSubject("");
+
+    try {
+      tokenVerifier.verifyToken(token);
+      fail("No error thrown for empty subject");
+    } 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() {
+    String token = createTokenWithSubject(Strings.repeat("a", 129));
+
+    try {
+      tokenVerifier.verifyToken(token);
+      fail("No error thrown for long subject");
+    } 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() {
+    long tenMinutesIntoTheFuture = (TestTokenFactory.CLOCK.currentTimeMillis() / 1000)
+        + TimeUnit.MINUTES.toSeconds(10);
+    String token = createTokenWithTimestamps(
+        tenMinutesIntoTheFuture,
+        tenMinutesIntoTheFuture + TimeUnit.HOURS.toSeconds(1));
+
+    try {
+      tokenVerifier.verifyToken(token);
+      fail("No error thrown for token issued in future");
+    } 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() {
+    long twoHoursInPast = (TestTokenFactory.CLOCK.currentTimeMillis() / 1000)
+        - TimeUnit.HOURS.toSeconds(2);
+    String token = createTokenWithTimestamps(
+        twoHoursInPast,
+        twoHoursInPast + TimeUnit.HOURS.toSeconds(1));
+
+    try {
+      tokenVerifier.verifyToken(token);
+      fail("No error thrown for expired 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.";
+      FirebaseTokenVerifierImplTestUtils.checkException(e, message, AuthErrorCode.EXPIRED_ID_TOKEN);
+    }
+  }
+
+  @Test
+  public void testWithMalformedSignature() throws FirebaseAuthException {
+    String token = tokenFactory.createUnsignedTokenForEmulator();
+    String[] segments = token.split("\\.");
+    token = String.format("%s.%s.%s", segments[0], segments[1], "MalformedSignature");
+
+    // No need to assert. verifyToken should not throw
+    tokenVerifier.verifyToken(token);
+  }
+
+  @Test
+  public void testMalformedToken() {
+    try {
+      tokenVerifier.verifyToken("not.a.jwt");
+      fail("No error thrown for malformed token");
+    } 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.getErrorCode());
+      assertEquals(message, e.getMessage());
+      assertTrue(e.getCause() instanceof IllegalArgumentException);
+      assertNull(e.getHttpResponse());
+      assertEquals(AuthErrorCode.INVALID_ID_TOKEN, e.getAuthErrorCode());
+    }
+  }
+
+  @Test
+  public void testVerifyTokenWithTenantId() throws FirebaseAuthException {
+    FirebaseToken firebaseToken = tokenVerifier
+        .verifyToken(createTokenWithTenantId("TENANT_1"));
+
+    assertEquals(TEST_TOKEN_ISSUER, firebaseToken.getIssuer());
+    assertEquals(TestTokenFactory.UID, firebaseToken.getUid());
+    assertEquals("TENANT_1", firebaseToken.getTenantId());
+  }
+
+  @Test
+  public void testVerifyTokenWithMatchingTenantId() throws FirebaseAuthException {
+    FirebaseToken firebaseToken = fullyPopulatedBuilder()
+        .setTenantId("TENANT_1")
+        .build().verifyToken(createTokenWithTenantId("TENANT_1"));
+
+    assertEquals(TEST_TOKEN_ISSUER, firebaseToken.getIssuer());
+    assertEquals(TestTokenFactory.UID, firebaseToken.getUid());
+    assertEquals("TENANT_1", firebaseToken.getTenantId());
+  }
+
+  @Test
+  public void testVerifyTokenDifferentTenantIds() {
+    try {
+      fullyPopulatedBuilder()
+          .setTenantId("TENANT_1")
+          .build().verifyToken(createTokenWithTenantId("TENANT_2"));
+      fail("No error thrown for mismatched tenant IDs");
+    } catch (FirebaseAuthException e) {
+      assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode());
+      assertEquals(
+          "The tenant ID ('TENANT_2') of the token did not match the expected value ('TENANT_1')",
+          e.getMessage());
+    }
+  }
+
+  @Test
+  public void testVerifyTokenNoTenantId() {
+    try {
+      fullyPopulatedBuilder()
+          .setTenantId("TENANT_1")
+          .build()
+          .verifyToken(tokenFactory.createUnsignedTokenForEmulator());
+      fail("No error thrown for missing tenant ID in token");
+    } catch (FirebaseAuthException e) {
+      assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode());
+      assertEquals(
+          "The tenant ID ('') of the token did not match the expected value ('TENANT_1')",
+          e.getMessage());
+    }
+  }
+
+  private String createTokenWithIncorrectAudience() {
+    return tokenFactory.createTokenWithIncorrectAudience(true);
+  }
+
+  private String createTokenWithoutKeyId() {
+    return tokenFactory.createTokenWithoutKeyId(true);
+  }
+
+  private String createTokenWithIncorrectAlgorithm() {
+    return tokenFactory.createTokenWithIncorrectAlgorithm(true);
+  }
+
+  private String createTokenWithoutAlgorithm() {
+    return tokenFactory.createTokenWithoutAlgorithm(true);
+  }
+
+  private String createTokenWithIncorrectIssuer() {
+    return tokenFactory.createTokenWithIncorrectIssuer(true);
+  }
+
+  private String createTokenWithSubject(String sub) {
+    return tokenFactory.createTokenWithSubject(sub, true);
+  }
+
+  private String createTokenWithTenantId(String tenantId) {
+    return tokenFactory.createTokenWithTenantId(tenantId, true);
+  }
+
+  private String createTokenWithTimestamps(long issuedAtSeconds, long expirationSeconds) {
+    return tokenFactory.createTokenWithTimestamps(issuedAtSeconds, expirationSeconds, true);
+  }
+}
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
index bd7aa0f..d385e7e 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
@@ -31,6 +31,7 @@ import com.google.api.core.ApiFuture;
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.firebase.ErrorCode;
 
 import com.google.firebase.FirebaseApp;
@@ -56,9 +57,12 @@ public class FirebaseAuthTest {
       ErrorCode.INVALID_ARGUMENT, "Test error message", null, null, null);
   private static final long VALID_SINCE = 1494364393;
   private static final String TEST_USER = "testUser";
+  private static final String AUTH_EMULATOR = "localhost:9099";
 
   @After
   public void cleanup() {
+    // Cleanup for tests on Auth Emulator
+    TestUtils.unsetEnvironmentVariables(ImmutableSet.of("FIREBASE_AUTH_EMULATOR_HOST"));
     TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
   }
 
@@ -262,6 +266,45 @@ public class FirebaseAuthTest {
     assertEquals("idtoken", tokenVerifier.getLastTokenString());
   }
 
+  @Test
+  public void testVerifyIdTokenWithEmulator() throws Exception {
+    // Enable emulator mode
+    TestUtils.setEnvironmentVariables(
+        ImmutableMap.of("FIREBASE_AUTH_EMULATOR_HOST", AUTH_EMULATOR));
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
+        getFirebaseToken(VALID_SINCE + 1000));
+    FirebaseAuth auth = getAuthForIdTokenVerificationWithRevocationCheck(tokenVerifier);
+
+    FirebaseToken firebaseToken = auth.verifyIdToken("idtoken", false);
+
+    assertEquals("testUser", firebaseToken.getUid());
+    assertEquals("idtoken", tokenVerifier.getLastTokenString());
+  }
+
+  @Test
+  public void testVerifyIdTokenFailureWithEmulator() {
+    // Enable emulator mode
+    TestUtils.setEnvironmentVariables(
+        ImmutableMap.of("FIREBASE_AUTH_EMULATOR_HOST", AUTH_EMULATOR));
+    MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(
+        getFirebaseToken(VALID_SINCE - 1000));
+    FirebaseAuth auth = getAuthForIdTokenVerificationWithRevocationCheck(tokenVerifier);
+
+    try {
+      // Should throw if token is revoked, even if checkRevoked is false.
+      auth.verifyIdToken("idtoken", false);
+      fail("No error thrown for revoked ID token");
+    } catch (FirebaseAuthException e) {
+      assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
+      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(testException);
diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenUtilsTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenUtilsTest.java
index f452091..ccc1ad6 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseTokenUtilsTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseTokenUtilsTest.java
@@ -27,10 +27,13 @@ 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.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.TestOnlyImplFirebaseTrampolines;
+import com.google.firebase.testing.TestUtils;
 import org.junit.After;
 import org.junit.Rule;
 import org.junit.Test;
@@ -41,12 +44,14 @@ 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);
+  private static final String AUTH_EMULATOR = "localhost:9099";
 
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
   @After
   public void tearDown() {
+    TestUtils.unsetEnvironmentVariables(ImmutableSet.of("FIREBASE_AUTH_EMULATOR_HOST"));
     TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
   }
 
@@ -57,8 +62,8 @@ public class FirebaseTokenUtilsTest {
         .setProjectId(TEST_PROJECT_ID)
         .build());
 
-    FirebaseTokenVerifierImpl idTokenVerifier = FirebaseTokenUtils.createIdTokenVerifier(
-        app, CLOCK);
+    FirebaseTokenVerifierImpl idTokenVerifier =
+        FirebaseTokenUtils.createIdTokenVerifier(app, CLOCK);
 
     assertEquals("verifyIdToken()", idTokenVerifier.getMethod());
     assertEquals("ID token", idTokenVerifier.getShortName());
@@ -72,6 +77,26 @@ public class FirebaseTokenUtilsTest {
         "https://securetoken.google.com/test-project-id");
   }
 
+  @Test
+  public void testCreateIdTokenVerifierForEmulator() {
+    TestUtils.setEnvironmentVariables(
+        ImmutableMap.of("FIREBASE_AUTH_EMULATOR_HOST", AUTH_EMULATOR));
+    FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder()
+        .setCredentials(MOCK_CREDENTIALS)
+        .setProjectId(TEST_PROJECT_ID)
+        .build());
+
+    FirebaseTokenVerifierImpl emulatorVerifier =
+        FirebaseTokenUtils.createIdTokenVerifier(app, CLOCK);
+
+    assertEquals("ID token", emulatorVerifier.getShortName());
+    assertEquals("an ID token", emulatorVerifier.getArticledShortName());
+    assertEquals("https://firebase.google.com/docs/auth/admin/verify-id-tokens",
+        emulatorVerifier.getDocUrl());
+    verifyJwtVerifier(emulatorVerifier.getIdTokenVerifier(),
+        "https://securetoken.google.com/test-project-id");
+  }
+
   @Test
   public void testCreateIdTokenVerifierWithoutProjectId() {
     FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder()
@@ -89,8 +114,8 @@ public class FirebaseTokenUtilsTest {
         .setProjectId(TEST_PROJECT_ID)
         .build());
 
-    FirebaseTokenVerifierImpl cookieVerifier = FirebaseTokenUtils.createSessionCookieVerifier(
-        app, CLOCK);
+    FirebaseTokenVerifierImpl cookieVerifier =
+        FirebaseTokenUtils.createSessionCookieVerifier(app, CLOCK);
 
     assertEquals("verifySessionCookie()", cookieVerifier.getMethod());
     assertEquals("session cookie", cookieVerifier.getShortName());
@@ -103,6 +128,26 @@ public class FirebaseTokenUtilsTest {
         "https://session.firebase.google.com/test-project-id");
   }
 
+  @Test
+  public void testSessionCookieVerifierForEmulator() {
+    TestUtils.setEnvironmentVariables(
+        ImmutableMap.of("FIREBASE_AUTH_EMULATOR_HOST", AUTH_EMULATOR));
+    FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder()
+        .setCredentials(MOCK_CREDENTIALS)
+        .setProjectId(TEST_PROJECT_ID)
+        .build());
+
+    FirebaseTokenVerifierImpl emulatorCookieVerifier =
+        FirebaseTokenUtils.createSessionCookieVerifier(app, CLOCK);
+
+    assertEquals("session cookie", emulatorCookieVerifier.getShortName());
+    assertEquals("a session cookie", emulatorCookieVerifier.getArticledShortName());
+    assertEquals("https://firebase.google.com/docs/auth/admin/manage-cookies",
+        emulatorCookieVerifier.getDocUrl());
+    verifyJwtVerifier(emulatorCookieVerifier.getIdTokenVerifier(),
+        "https://session.firebase.google.com/test-project-id");
+  }
+
   @Test
   public void testCreateSessionCookieVerifierWithoutProjectId() {
     FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder()
diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java
index 189cb22..d02d2b5 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java
@@ -16,28 +16,28 @@
 
 package com.google.firebase.auth;
 
+import static com.google.firebase.auth.FirebaseTokenVerifierImplTestUtils.TEST_TOKEN_ISSUER;
+import static com.google.firebase.auth.FirebaseTokenVerifierImplTestUtils.checkException;
+import static com.google.firebase.auth.FirebaseTokenVerifierImplTestUtils.checkInvalidTokenException;
+import static com.google.firebase.auth.FirebaseTokenVerifierImplTestUtils.fullyPopulatedBuilder;
+import static com.google.firebase.auth.FirebaseTokenVerifierImplTestUtils.newPublicKeysManager;
+import static com.google.firebase.auth.FirebaseTokenVerifierImplTestUtils.newTestTokenVerifier;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
-import com.google.api.client.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.common.collect.ImmutableMap;
 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.Test;
 
@@ -54,8 +54,6 @@ public class FirebaseTokenVerifierImplTest {
           + "-P1wie318In0sInYiOjAsImlhdCI6MTQ4MDk4Mj"
           + "U2NH0.ZWEpoHgIPCAz8Q-cNFBS8jiqClTJ3j27yuRkQo-QxyI";
 
-  private static final String TEST_TOKEN_ISSUER = "https://test.token.issuer";
-
   private FirebaseTokenVerifier tokenVerifier;
   private TestTokenFactory tokenFactory;
 
@@ -83,6 +81,7 @@ public class FirebaseTokenVerifierImplTest {
 
     try {
       tokenVerifier.verifyToken(token);
+      fail("No error thrown for missing kid");
     } 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.";
@@ -96,6 +95,7 @@ public class FirebaseTokenVerifierImplTest {
 
     try {
       tokenVerifier.verifyToken(token);
+      fail("No error thrown for passing custom 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.";
@@ -109,6 +109,7 @@ public class FirebaseTokenVerifierImplTest {
 
     try {
       tokenVerifier.verifyToken(token);
+      fail("No error thrown for incorrect alg");
     } catch (FirebaseAuthException e) {
       String message = "Firebase test token has incorrect algorithm. "
           + "Expected \"RS256\" but got \"HSA\". "
@@ -117,12 +118,28 @@ public class FirebaseTokenVerifierImplTest {
     }
   }
 
+  @Test
+  public void testVerifyTokenWithoutAlgorithm() {
+    String token = createTokenWithoutAlgorithm();
+
+    try {
+      tokenVerifier.verifyToken(token);
+      fail("No error thrown for alg: 'none'");
+    } catch (FirebaseAuthException e) {
+      String message = "Firebase test token has incorrect algorithm. "
+          + "Expected \"RS256\" but got \"none\". "
+          + "See https://test.doc.url for details on how to retrieve a test token.";
+      checkInvalidTokenException(e, message);
+    }
+  }
+
   @Test
   public void testVerifyTokenIncorrectAudience() {
     String token = createTokenWithIncorrectAudience();
 
     try {
       tokenVerifier.verifyToken(token);
+      fail("No error thrown for incorrect audience");
     } catch (FirebaseAuthException e) {
       String message = "Firebase test token has incorrect \"aud\" (audience) claim. "
           + "Expected \"proj-test-101\" but got \"invalid-audience\". "
@@ -139,6 +156,7 @@ public class FirebaseTokenVerifierImplTest {
 
     try {
       tokenVerifier.verifyToken(token);
+      fail("No error thrown for incorrect issuer");
     } catch (FirebaseAuthException e) {
       String message = "Firebase test token has incorrect \"iss\" (issuer) claim. "
           + "Expected \"https://test.token.issuer\" but got "
@@ -155,6 +173,7 @@ public class FirebaseTokenVerifierImplTest {
 
     try {
       tokenVerifier.verifyToken(token);
+      fail("No error thrown for missing subject");
     } 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.";
@@ -168,6 +187,7 @@ public class FirebaseTokenVerifierImplTest {
 
     try {
       tokenVerifier.verifyToken(token);
+      fail("No error thrown for empty subject");
     } 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.";
@@ -181,6 +201,7 @@ public class FirebaseTokenVerifierImplTest {
 
     try {
       tokenVerifier.verifyToken(token);
+      fail("No error thrown for very long subject");
     } catch (FirebaseAuthException e) {
       String message = "Firebase test token has \"sub\" (subject) claim longer "
           + "than 128 characters. "
@@ -199,6 +220,7 @@ public class FirebaseTokenVerifierImplTest {
 
     try {
       tokenVerifier.verifyToken(token);
+      fail("No error thrown for token issued in the future");
     } 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.";
@@ -216,6 +238,7 @@ public class FirebaseTokenVerifierImplTest {
 
     try {
       tokenVerifier.verifyToken(token);
+      fail("No error thrown for expired token");
     } catch (FirebaseAuthException e) {
       String message = "Firebase test token has expired. "
           + "Get a fresh test token and try again. "
@@ -233,6 +256,7 @@ public class FirebaseTokenVerifierImplTest {
 
     try {
       tokenVerifier.verifyToken(token);
+      fail("No error thrown for mismatched signature");
     } 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.";
@@ -248,6 +272,7 @@ public class FirebaseTokenVerifierImplTest {
 
     try {
       tokenVerifier.verifyToken(token);
+      fail("No error thrown for malformed cert");
     } catch (FirebaseAuthException e) {
       String message = "Error while fetching public key certificates: Could not parse certificate";
       assertEquals(ErrorCode.UNKNOWN, e.getErrorCode());
@@ -272,7 +297,7 @@ public class FirebaseTokenVerifierImplTest {
 
     try {
       idTokenVerifier.verifyToken(token);
-      Assert.fail("No exception thrown");
+      fail("No error thrown for failing to fetch certificate");
     } catch (FirebaseAuthException e) {
       String message = "Error while fetching public key certificates: Expected error";
       assertEquals(ErrorCode.UNKNOWN, e.getErrorCode());
@@ -291,6 +316,7 @@ public class FirebaseTokenVerifierImplTest {
 
     try {
       tokenVerifier.verifyToken(token);
+      fail("No error thrown for malformed signature");
     } 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.";
@@ -302,6 +328,7 @@ public class FirebaseTokenVerifierImplTest {
   public void testLegacyCustomToken() {
     try {
       tokenVerifier.verifyToken(LEGACY_CUSTOM_TOKEN);
+      fail("No error thrown for passing legacy token");
     } catch (FirebaseAuthException e) {
       String message = "verifyTestToken() expects a test token, but was given a "
           + "legacy custom token. "
@@ -314,6 +341,7 @@ public class FirebaseTokenVerifierImplTest {
   public void testMalformedToken() {
     try {
       tokenVerifier.verifyToken("not.a.jwt");
+      fail("No error thrown for malformed token");
     } catch (FirebaseAuthException e) {
       String message = "Failed to parse Firebase test token. "
           + "Make sure you passed a string that represents a complete and valid JWT. "
@@ -359,6 +387,7 @@ public class FirebaseTokenVerifierImplTest {
 
     try {
       verifier.verifyToken(token);
+      fail("No error thrown for mismatched tenant IDs");
     } catch (FirebaseAuthException e) {
       assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode());
       assertEquals(
@@ -376,6 +405,7 @@ public class FirebaseTokenVerifierImplTest {
 
     try {
       verifier.verifyToken(token);
+      fail("No error thrown for missing tenant ID");
     } catch (FirebaseAuthException e) {
       assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode());
       assertEquals(
@@ -384,35 +414,6 @@ public class FirebaseTokenVerifierImplTest {
     }
   }
 
-  @Test
-  public void testVerifyTokenMissingTenantId() {
-    try {
-      fullyPopulatedBuilder()
-        .setTenantId("TENANT_ID")
-        .build()
-        .verifyToken(tokenFactory.createToken());
-    } catch (FirebaseAuthException e) {
-      assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode());
-      assertEquals(
-          "The tenant ID ('') of the token did not match the expected value ('TENANT_ID')",
-          e.getMessage());
-    }
-  }
-
-  @Test
-  public void testVerifyTokenUnexpectedTenantId() {
-    try {
-      fullyPopulatedBuilder()
-        .build()
-        .verifyToken(createTokenWithTenantId("TENANT_ID"));
-    } catch (FirebaseAuthException e) {
-      assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode());
-      assertEquals(
-          "The tenant ID ('TENANT_ID') of the token did not match the expected value ('')",
-          e.getMessage());
-    }
-  }
-
   @Test(expected = NullPointerException.class)
   public void testBuilderNoPublicKeysManager() {
     fullyPopulatedBuilder().setPublicKeysManager(null).build();
@@ -443,60 +444,12 @@ public class FirebaseTokenVerifierImplTest {
     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 fullyPopulatedBuilder()
-        .setPublicKeysManager(publicKeysManager)
-        .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()))
-        .setInvalidTokenErrorCode(AuthErrorCode.INVALID_ID_TOKEN)
-        .setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_ID_TOKEN)
-        .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);
+    return tokenFactory.createTokenWithoutKeyId(false);
   }
 
   private String createTokenWithSubject(String sub) {
-    Payload payload = tokenFactory.createTokenPayload();
-    payload.setSubject(sub);
-    return tokenFactory.createToken(payload);
+    return tokenFactory.createTokenWithSubject(sub, false);
   }
 
   private String createCustomToken() {
@@ -508,45 +461,26 @@ public class FirebaseTokenVerifierImplTest {
   }
 
   private String createTokenWithIncorrectAlgorithm() {
-    JsonWebSignature.Header header = tokenFactory.createHeader();
-    header.setAlgorithm("HSA");
-    return tokenFactory.createToken(header);
+    return tokenFactory.createTokenWithIncorrectAlgorithm(false);
+  }
+
+  private String createTokenWithoutAlgorithm() {
+    return tokenFactory.createTokenWithoutAlgorithm(false);
   }
 
   private String createTokenWithIncorrectAudience() {
-    Payload payload = tokenFactory.createTokenPayload();
-    payload.setAudience("invalid-audience");
-    return tokenFactory.createToken(payload);
+    return tokenFactory.createTokenWithIncorrectAudience(false);
   }
 
   private String createTokenWithIncorrectIssuer() {
-    Payload payload = tokenFactory.createTokenPayload();
-    payload.setIssuer("https://incorrect.issuer.prefix/" + TestTokenFactory.PROJECT_ID);
-    return tokenFactory.createToken(payload);
+    return tokenFactory.createTokenWithIncorrectIssuer(false);
   }
 
   private String createTokenWithTimestamps(long issuedAtSeconds, long expirationSeconds) {
-    Payload payload = tokenFactory.createTokenPayload();
-    payload.setIssuedAtTimeSeconds(issuedAtSeconds);
-    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.getErrorCode());
-    assertEquals(message, e.getMessage());
-    assertNull(e.getCause());
-    assertNull(e.getHttpResponse());
-    assertEquals(errorCode, e.getAuthErrorCode());
+    return tokenFactory.createTokenWithTimestamps(issuedAtSeconds, expirationSeconds, false);
   }
 
   private String createTokenWithTenantId(String tenantId) {
-    Payload payload = tokenFactory.createTokenPayload();
-    payload.set("firebase", ImmutableMap.of("tenant", tenantId));
-    return tokenFactory.createToken(payload);
+    return tokenFactory.createTokenWithTenantId(tenantId, false);
   }
 }
diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTestUtils.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTestUtils.java
new file mode 100644
index 0000000..5f23315
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTestUtils.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+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.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.common.collect.ImmutableList;
+import com.google.firebase.ErrorCode;
+import com.google.firebase.testing.ServiceAccount;
+
+class FirebaseTokenVerifierImplTestUtils {
+  public static final String TEST_TOKEN_ISSUER = "https://test.token.issuer";
+
+  static void checkInvalidTokenException(FirebaseAuthException e, String message) {
+    checkException(e, message, AuthErrorCode.INVALID_ID_TOKEN);
+  }
+
+  static void checkException(FirebaseAuthException e, String message, AuthErrorCode errorCode) {
+    assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
+    assertEquals(message, e.getMessage());
+    assertNull(e.getCause());
+    assertNull(e.getHttpResponse());
+    assertEquals(errorCode, e.getAuthErrorCode());
+  }
+
+  static 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);
+  }
+
+  static GooglePublicKeysManager newPublicKeysManager(HttpTransport transport) {
+    return new GooglePublicKeysManager.Builder(
+        transport, FirebaseTokenUtils.UNQUOTED_CTRL_CHAR_JSON_FACTORY)
+        .setClock(TestTokenFactory.CLOCK)
+        .setPublicCertsEncodedUrl("https://test.cert.url")
+        .build();
+  }
+
+  static FirebaseTokenVerifier newTestTokenVerifier(GooglePublicKeysManager publicKeysManager) {
+    return fullyPopulatedBuilder()
+        .setPublicKeysManager(publicKeysManager)
+        .build();
+  }
+
+  static 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()))
+        .setInvalidTokenErrorCode(AuthErrorCode.INVALID_ID_TOKEN)
+        .setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_ID_TOKEN)
+        .setIdTokenVerifier(newIdTokenVerifier());
+  }
+
+  static IdTokenVerifier newIdTokenVerifier() {
+    return new IdTokenVerifier.Builder()
+        .setClock(TestTokenFactory.CLOCK)
+        .setAudience(ImmutableList.of(TestTokenFactory.PROJECT_ID))
+        .setIssuer(TEST_TOKEN_ISSUER)
+        .build();
+  }
+}
diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
index 772f9ba..dead8ab 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
@@ -39,6 +39,7 @@ import com.google.common.base.Strings;
 import com.google.common.base.Supplier;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.firebase.ErrorCode;
 import com.google.firebase.FirebaseApp;
@@ -90,12 +91,17 @@ public class FirebaseUserManagerTest {
 
   private static final String TENANTS_BASE_URL = PROJECT_BASE_URL + "/tenants";
 
+  private static final String AUTH_EMULATOR = "localhost:8000";
+  private static final String PROJECT_BASE_URL_EMULATOR =
+          "http://" + AUTH_EMULATOR + "/identitytoolkit.googleapis.com/v2/projects/test-project-id";
+
   private static final String SAML_RESPONSE = TestUtils.loadResource("saml.json");
 
   private static final String OIDC_RESPONSE = TestUtils.loadResource("oidc.json");
 
   @After
   public void tearDown() {
+    TestUtils.unsetEnvironmentVariables(ImmutableSet.of("FIREBASE_AUTH_EMULATOR_HOST"));
     TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
   }
 
@@ -1517,7 +1523,7 @@ public class FirebaseUserManagerTest {
             .setClientId("CLIENT_ID")
             .setIssuer("https://oidc.com/issuer");
 
-    OidcProviderConfig config = 
+    OidcProviderConfig config =
         FirebaseAuth.getInstance().createOidcProviderConfigAsync(createRequest).get();
 
     checkOidcProviderConfig(config, "oidc.provider-id");
@@ -2678,6 +2684,33 @@ public class FirebaseUserManagerTest {
     checkUrl(interceptor, "DELETE", expectedUrl);
   }
 
+  @Test
+  public void testCreateOidcProviderFromEmulatorAuth() throws Exception {
+    TestUtils.setEnvironmentVariables(
+            ImmutableMap.of("FIREBASE_AUTH_EMULATOR_HOST", AUTH_EMULATOR));
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE);
+    OidcProviderConfig.CreateRequest createRequest =
+            new OidcProviderConfig.CreateRequest()
+                    .setProviderId("oidc.provider-id")
+                    .setDisplayName("DISPLAY_NAME")
+                    .setEnabled(true)
+                    .setClientId("CLIENT_ID")
+                    .setIssuer("https://oidc.com/issuer");
+
+    OidcProviderConfig config = FirebaseAuth.getInstance().createOidcProviderConfig(createRequest);
+
+    checkOidcProviderConfig(config, "oidc.provider-id");
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "POST", PROJECT_BASE_URL_EMULATOR + "/oauthIdpConfigs");
+    GenericJson parsed = parseRequestContent(interceptor);
+    assertEquals("DISPLAY_NAME", parsed.get("displayName"));
+    assertTrue((boolean) parsed.get("enabled"));
+    assertEquals("CLIENT_ID", parsed.get("clientId"));
+    assertEquals("https://oidc.com/issuer", parsed.get("issuer"));
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals("oidc.provider-id", url.getFirst("oauthIdpConfigId"));
+  }
+
   private static TestResponseInterceptor initializeAppForUserManagementWithStatusCode(
       int statusCode, String response) {
     FirebaseApp.initializeApp(FirebaseOptions.builder()
diff --git a/src/test/java/com/google/firebase/auth/TestTokenFactory.java b/src/test/java/com/google/firebase/auth/TestTokenFactory.java
index 1f774ee..0510dbb 100644
--- a/src/test/java/com/google/firebase/auth/TestTokenFactory.java
+++ b/src/test/java/com/google/firebase/auth/TestTokenFactory.java
@@ -20,8 +20,11 @@ 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.json.webtoken.JsonWebToken.Payload;
 import com.google.api.client.testing.http.FixedClock;
+import com.google.api.client.util.Base64;
 import com.google.api.client.util.Clock;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.io.BaseEncoding;
 import java.io.IOException;
 import java.security.GeneralSecurityException;
@@ -69,6 +72,30 @@ class TestTokenFactory {
     }
   }
 
+  public String createUnsignedTokenForEmulator() {
+    return createUnsignedTokenForEmulator(createHeaderForEmulator(), createTokenPayload());
+  }
+
+  public String createUnsignedTokenForEmulator(JsonWebSignature.Header header) {
+    return createUnsignedTokenForEmulator(header, createTokenPayload());
+  }
+
+  public String createUnsignedTokenForEmulator(JsonWebToken.Payload payload) {
+    return createUnsignedTokenForEmulator(createHeaderForEmulator(), payload);
+  }
+
+  public String createUnsignedTokenForEmulator(
+      JsonWebSignature.Header header, JsonWebSignature.Payload payload) {
+    try {
+      String encodedHeader = Base64.encodeBase64URLSafeString(JSON_FACTORY.toByteArray(header));
+      String encodedPayload = Base64.encodeBase64URLSafeString(JSON_FACTORY.toByteArray(payload));
+      // Unsigned token with no signature component
+      return String.format("%s.%s.", encodedHeader, encodedPayload);
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to create test token", e);
+    }
+  }
+
   public JsonWebSignature.Header createHeader() {
     JsonWebSignature.Header header = new JsonWebSignature.Header();
     header.setAlgorithm("RS256");
@@ -86,4 +113,69 @@ class TestTokenFactory {
     payload.setSubject(UID);
     return payload;
   }
+
+  public JsonWebSignature.Header createHeaderForEmulator() {
+    JsonWebSignature.Header header = new JsonWebSignature.Header();
+    header.setAlgorithm("none");
+    header.setType("JWT");
+    return header;
+  }
+
+  public String createTokenWithoutKeyId(boolean isEmulatorMode) {
+    JsonWebSignature.Header header = createHeader();
+    header.setKeyId(null);
+    return isEmulatorMode ? createUnsignedTokenForEmulator(header)
+        : createToken(header);
+  }
+
+  public String createTokenWithSubject(String sub, boolean isEmulatorMode) {
+    Payload payload = createTokenPayload();
+    payload.setSubject(sub);
+    return isEmulatorMode ? createUnsignedTokenForEmulator(payload)
+        : createToken(payload);
+  }
+
+  public String createTokenWithIncorrectAlgorithm(boolean isEmulatorMode) {
+    JsonWebSignature.Header header = createHeader();
+    header.setAlgorithm("HSA");
+    return isEmulatorMode ? createUnsignedTokenForEmulator(header)
+        : createToken(header);
+  }
+
+  public String createTokenWithoutAlgorithm(boolean isEmulatorMode) {
+    JsonWebSignature.Header header = createHeader();
+    header.setAlgorithm("none");
+    return isEmulatorMode ? createUnsignedTokenForEmulator(header)
+        : createToken(header);
+  }
+
+  public String createTokenWithIncorrectAudience(boolean isEmulatorMode) {
+    Payload payload = createTokenPayload();
+    payload.setAudience("invalid-audience");
+    return isEmulatorMode ? createUnsignedTokenForEmulator(payload)
+        : createToken(payload);
+  }
+
+  public String createTokenWithIncorrectIssuer(boolean isEmulatorMode) {
+    Payload payload = createTokenPayload();
+    payload.setIssuer("https://incorrect.issuer.prefix/" + TestTokenFactory.PROJECT_ID);
+    return isEmulatorMode ? createUnsignedTokenForEmulator(payload)
+        : createToken(payload);
+  }
+
+  public String createTokenWithTimestamps(long issuedAtSeconds, long expirationSeconds,
+      boolean isEmulatorMode) {
+    Payload payload = createTokenPayload();
+    payload.setIssuedAtTimeSeconds(issuedAtSeconds);
+    payload.setExpirationTimeSeconds(expirationSeconds);
+    return isEmulatorMode ? createUnsignedTokenForEmulator(payload)
+        : createToken(payload);
+  }
+
+  public String createTokenWithTenantId(String tenantId, boolean isEmulatorMode) {
+    Payload payload = createTokenPayload();
+    payload.set("firebase", ImmutableMap.of("tenant", tenantId));
+    return isEmulatorMode ? createUnsignedTokenForEmulator(payload)
+        : createToken(payload);
+  }
 }
diff --git a/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java b/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java
index fee4d8f..73477ad 100644
--- a/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java
+++ b/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java
@@ -219,6 +219,14 @@ public class CryptoSignersTest {
     assertArrayEquals("local-signed-bytes".getBytes(), data);
   }
 
+  @Test
+  public void testEmulatorCryptoSigner() throws Exception {
+    CryptoSigner signer = new CryptoSigners.EmulatorCryptoSigner();
+    byte[] data = signer.sign("foo".getBytes());
+    // No signed bytes returned
+    assertEquals(data.length, 0);
+  }
+
   @After
   public void tearDown() {
     TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
diff --git a/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java
index 4bc84cb..c760465 100644
--- a/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java
+++ b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java
@@ -35,6 +35,8 @@ import com.google.api.client.testing.http.MockHttpTransport;
 import com.google.api.client.testing.http.MockLowLevelHttpResponse;
 import com.google.auth.oauth2.GoogleCredentials;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.firebase.ErrorCode;
 import com.google.firebase.FirebaseApp;
@@ -52,6 +54,7 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+
 import org.junit.After;
 import org.junit.Test;
 
@@ -68,8 +71,14 @@ public class FirebaseTenantClientTest {
 
   private static final String TENANTS_BASE_URL = PROJECT_BASE_URL + "/tenants";
 
+  private static final String AUTH_EMULATOR = "localhost:8000";
+  private static final String PROJECT_BASE_URL_EMULATOR =
+          "http://" + AUTH_EMULATOR + "/identitytoolkit.googleapis.com/v2/projects/test-project-id";
+  private static final String TENANTS_BASE_URL_EMULATOR = PROJECT_BASE_URL_EMULATOR + "/tenants";
+
   @After
-  public void tearDown() {
+  public void tearDown() throws ReflectiveOperationException {
+    TestUtils.unsetEnvironmentVariables(ImmutableSet.of("FIREBASE_AUTH_EMULATOR_HOST"));
     TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
   }
 
@@ -312,6 +321,20 @@ public class FirebaseTenantClientTest {
     checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/UNKNOWN");
   }
 
+  @Test
+  public void testGetTenantFromAuthEmulator() throws Exception {
+    TestUtils.setEnvironmentVariables(
+            ImmutableMap.of("FIREBASE_AUTH_EMULATOR_HOST", AUTH_EMULATOR));
+    TestResponseInterceptor interceptor = initializeAppForTenantManagement(
+            TestUtils.loadResource("tenant.json"));
+
+    Tenant tenant = FirebaseAuth.getInstance().getTenantManager().getTenant("TENANT_1");
+
+    checkTenant(tenant, "TENANT_1");
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "GET", TENANTS_BASE_URL_EMULATOR + "/TENANT_1");
+  }
+
   private static void checkTenant(Tenant tenant, String tenantId) {
     assertEquals(tenantId, tenant.getTenantId());
     assertEquals("DISPLAY_NAME", tenant.getDisplayName());