Repository: firebase/firebase-admin-java
Pull request #345 (2020-01-09 19:35:51)

feat(auth): Link federatedid

Michael-Frank: any progress on this? This feature would be really great to have.

rsgowman: > any progress on this? This feature would be really great to have.

Yes: We finally got all the approvals necessary to launch this and have started to do so. The node.js port went out last month (https://firebase.google.com/support/release-notes/admin/node#version_950_-_10_february_2021) and the other ports, incl this one, should be released in the coming weeks.
google-cla[bot]: All (the pull request submitter and all commit authors) CLAs are signed, **but** one or more commits were authored or co-authored by someone other than the pull request submitter.

We need to confirm that all authors are ok with their commits being contributed to this project.  Please have them confirm that by leaving a comment that contains only `@googlebot I consent.` in this pull request.

*Note to project maintainer:* There may be cases where the author cannot leave a comment, or the comment is not properly detected as consent.  In those cases, you can manually confirm consent of the commit author(s), and set the `cla` label to `yes` (if enabled on your project).

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

<!-- need_author_consent -->
google-cla[bot]: All (the pull request submitter and all commit authors) CLAs are signed, **but** one or more commits were authored or co-authored by someone other than the pull request submitter.

We need to confirm that all authors are ok with their commits being contributed to this project.  Please have them confirm that by leaving a comment that contains only `@googlebot I consent.` in this pull request.

*Note to project maintainer:* There may be cases where the author cannot leave a comment, or the comment is not properly detected as consent.  In those cases, you can manually confirm consent of the commit author(s), and set the `cla` label to `yes` (if enabled on your project).

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

<!-- need_author_consent -->

Patch:
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
new file mode 100644
index 0000000..c9259a6
--- /dev/null
+++ b/.github/workflows/nightly.yml
@@ -0,0 +1,83 @@
+# 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.
+
+name: Nightly Builds
+
+on:
+  # Runs every day at 06:10 AM (PT) and 08:10 PM (PT) / 04:10 AM (UTC) and 02:10 PM (UTC)
+  # or on 'firebase_nightly_build' repository dispatch event.
+  schedule:
+    - cron: "10 4,14 * * *"
+  repository_dispatch:
+    types: [firebase_nightly_build]
+
+jobs:
+  nightly:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - name: Checkout source for staging
+      uses: actions/checkout@v2
+      with:
+        ref: ${{ github.event.client_payload.ref || github.ref }}
+
+    - name: Set up JDK 1.7
+      uses: actions/setup-java@v1
+      with:
+        java-version: 1.7
+
+    - name: Compile, test and package
+      run: ./.github/scripts/package_artifacts.sh
+      env:
+        FIREBASE_SERVICE_ACCT_KEY: ${{ secrets.FIREBASE_SERVICE_ACCT_KEY }}
+        FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }}
+
+    # Attach the packaged artifacts to the workflow output. These can be manually
+    # downloaded for later inspection if necessary.
+    - name: Archive artifacts
+      uses: actions/upload-artifact@v1
+      with:
+        name: dist
+        path: dist
+
+    - name: Send email on failure
+      if: failure()
+      uses: firebase/firebase-admin-node/.github/actions/send-email@master
+      with:
+        api-key: ${{ secrets.OSS_BOT_MAILGUN_KEY }}
+        domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }}
+        from: 'GitHub <admin-github@${{ secrets.OSS_BOT_MAILGUN_DOMAIN }}>'
+        to: ${{ secrets.FIREBASE_ADMIN_GITHUB_EMAIL }}
+        subject: 'Nightly build ${{github.run_id}} of ${{github.repository}} failed!'
+        html: >
+          <b>Nightly workflow ${{github.run_id}} failed on: ${{github.repository}}</b>
+          <br /><br />Navigate to the 
+          <a href="https://github.com/firebase/firebase-admin-java/actions/runs/${{github.run_id}}">failed workflow</a>.
+      continue-on-error: true
+
+    - name: Send email on cancelled
+      if: cancelled()
+      uses: firebase/firebase-admin-node/.github/actions/send-email@master
+      with:
+        api-key: ${{ secrets.OSS_BOT_MAILGUN_KEY }}
+        domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }}
+        from: 'GitHub <admin-github@${{ secrets.OSS_BOT_MAILGUN_DOMAIN }}>'
+        to: ${{ secrets.FIREBASE_ADMIN_GITHUB_EMAIL }}
+        subject: 'Nightly build ${{github.run_id}} of ${{github.repository}} cancelled!'
+        html: >
+          <b>Nightly workflow ${{github.run_id}} cancelled on: ${{github.repository}}</b>
+          <br /><br />Navigate to the 
+          <a href="https://github.com/firebase/firebase-admin-java/actions/runs/${{github.run_id}}">cancelled workflow</a>.
+      continue-on-error: true
diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
index fc9c8df..5dab1d8 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);
     }
@@ -553,6 +554,64 @@ public abstract class AbstractFirebaseAuth {
     };
   }
 
+  /**
+   * Gets the user data for the user corresponding to a given provider id.
+   *
+   * @param providerId Identifier for the given federated provider, for example,
+   *     "google.com" for the Google provider.
+   * @param uid The user identifier with the given provider.
+   * @return A {@link UserRecord} instance.
+   * @throws IllegalArgumentException If the uid is null or empty, or if
+   *     the providerId is null, empty, or does not belong to a federated provider.
+   * @throws FirebaseAuthException If an error occurs while retrieving user data.
+   */
+  public UserRecord getUserByProviderUid(
+      @NonNull String providerId, @NonNull String uid) throws FirebaseAuthException {
+    return getUserByProviderUidOp(providerId, uid).call();
+  }
+
+  /**
+   * Gets the user data for the user corresponding to a given provider id.
+   *
+   * @param providerId Identifer for the given federated provider, for example,
+   *     "google.com" for the Google provider.
+   * @param uid The user identifier with the given provider.
+   * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord}
+   *     instance. If an error occurs while retrieving user data or if the provider ID and uid
+   *     do not correspond to a user, the future throws a {@link FirebaseAuthException}.
+   * @throws IllegalArgumentException If the uid is null or empty, or if
+   *     the provider ID is null, empty, or does not belong to a federated provider.
+   */
+  public ApiFuture<UserRecord> getUserByProviderUidAsync(
+      @NonNull String providerId, @NonNull String uid) {
+    return getUserByProviderUidOp(providerId, uid).callAsync(firebaseApp);
+  }
+
+  private CallableOperation<UserRecord, FirebaseAuthException> getUserByProviderUidOp(
+      final String providerId, final String uid) {
+    checkArgument(!Strings.isNullOrEmpty(providerId), "providerId must not be null or empty");
+    checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
+
+    // Although we don't really advertise it, we want to also handle
+    // non-federated idps with this call. So if we detect one of them, we'll
+    // reroute this request appropriately.
+    if (providerId == "phone") {
+      return this.getUserByPhoneNumberOp(uid);
+    } else if (providerId == "email") {
+      return this.getUserByEmailOp(uid);
+    }
+
+    checkArgument(!providerId.equals("password")
+        && !providerId.equals("anonymous"), "providerId must belong to a federated provider");
+    final FirebaseUserManager userManager = getUserManager();
+    return new CallableOperation<UserRecord, FirebaseAuthException>() {
+      @Override
+      protected UserRecord execute() throws FirebaseAuthException {
+        return userManager.getUserByProviderUid(providerId, uid);
+      }
+    };
+  }
+
   /**
    * Gets a page of users starting from the specified {@code pageToken}. Page size is limited to
    * 1000 users.
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..1955278 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);
@@ -145,6 +155,15 @@ final class FirebaseUserManager {
     return results;
   }
 
+  UserRecord getUserByProviderUid(
+      String providerId, String uid) throws FirebaseAuthException {
+    final Map<String, Object> payload = ImmutableMap.<String, Object>of(
+        "federatedUserId", ImmutableList.of(
+            ImmutableMap.<String, Object>builder()
+            .put("rawId", uid).put("providerId", providerId).build()));
+    return lookupUserAccount(payload, uid);
+  }
+
   String createUser(UserRecord.CreateRequest request) throws FirebaseAuthException {
     GenericJson response = post("/accounts", request.getProperties(), GenericJson.class);
     return (String) response.get("localId");
diff --git a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java
index 2693178..0a4f7c0 100644
--- a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java
+++ b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java
@@ -18,8 +18,11 @@ package com.google.firebase.auth;
 
 import static com.google.common.base.Preconditions.checkArgument;
 
+import com.google.api.client.json.GenericJson;
 import com.google.api.client.util.Key;
 import com.google.common.base.Strings;
+import java.util.HashMap;
+import java.util.Map;
 
 /**
  * Contains metadata associated with an OIDC Auth provider.
@@ -31,17 +34,35 @@ public final class OidcProviderConfig extends ProviderConfig {
   @Key("clientId")
   private String clientId;
 
+  @Key("clientSecret")
+  private String clientSecret;
+
   @Key("issuer")
   private String issuer;
 
+  @Key("responseType")
+  private GenericJson responseType;
+
   public String getClientId() {
     return clientId;
   }
 
+  public String getClientSecret() {
+    return clientSecret;
+  }
+
   public String getIssuer() {
     return issuer;
   }
 
+  public boolean isCodeResponseType() {
+    return (responseType.containsKey("code") && (boolean) responseType.get("code"));
+  }
+
+  public boolean isIdTokenResponseType() {
+    return (responseType.containsKey("idToken") && (boolean) responseType.get("idToken"));
+  }
+
   /**
    * Returns a new {@link UpdateRequest}, which can be used to update the attributes of this
    * provider config.
@@ -58,6 +79,13 @@ public final class OidcProviderConfig extends ProviderConfig {
         "Invalid OIDC provider ID (must be prefixed with 'oidc.'): " + providerId);
   }
 
+  static Map<String, Boolean> ensureResponseType(Map<String,Object> properties) {
+    if (properties.get("responseType") == null) {
+      properties.put("responseType", new HashMap<String, Boolean>());
+    }
+    return (Map<String, Boolean>) properties.get("responseType");
+  }
+
   /**
    * A specification class for creating a new OIDC Auth provider.
    *
@@ -99,6 +127,19 @@ public final class OidcProviderConfig extends ProviderConfig {
       return this;
     }
 
+    /**
+     * Sets the client secret for the new provider. This is required for the code flow.
+     *
+     * @param clientSecret A non-null, non-empty client secret string.
+     * @throws IllegalArgumentException If the client secret is null or empty.
+     */
+    public CreateRequest setClientSecret(String clientSecret) {
+      checkArgument(!Strings.isNullOrEmpty(clientSecret),
+          "Client Secret must not be null or empty.");
+      properties.put("clientSecret", clientSecret);
+      return this;
+    }
+
     /**
      * Sets the issuer for the new provider.
      *
@@ -113,6 +154,36 @@ public final class OidcProviderConfig extends ProviderConfig {
       return this;
     }
 
+    /**
+     * Sets whether to enable the code response flow for the new provider. By default, this is not
+     * enabled if no response type is specified.
+     *
+     * <p>A client secret must be set for this response type.
+     *
+     * <p>Having both the code and ID token response flows is currently not supported.
+     *
+     * @param enabled A boolean signifying whether the code response type is supported.
+     */
+    public CreateRequest setCodeResponseType(boolean enabled) {
+      Map<String, Boolean> map = ensureResponseType(properties);
+      map.put("code", enabled);
+      return this;
+    }
+
+    /**
+     * Sets whether to enable the ID token response flow for the new provider. By default, this is
+     * enabled if no response type is specified.
+     *
+     * <p>Having both the code and ID token response flows is currently not supported.
+     *
+     * @param enabled A boolean signifying whether the ID token response type is supported.
+     */
+    public CreateRequest setIdTokenResponseType(boolean enabled) {
+      Map<String, Boolean> map = ensureResponseType(properties);
+      map.put("idToken", enabled);
+      return this;
+    }
+
     CreateRequest getThis() {
       return this;
     }
@@ -156,6 +227,19 @@ public final class OidcProviderConfig extends ProviderConfig {
       return this;
     }
 
+    /**
+     * Sets the client secret for the new provider. This is required for the code flow.
+     *
+     * @param clientSecret A non-null, non-empty client secret string.
+     * @throws IllegalArgumentException If the client secret is null or empty.
+     */
+    public UpdateRequest setClientSecret(String clientSecret) {
+      checkArgument(!Strings.isNullOrEmpty(clientSecret),
+          "Client Secret must not be null or empty.");
+      properties.put("clientSecret", clientSecret);
+      return this;
+    }
+
     /**
      * Sets the issuer for the existing provider.
      *
@@ -170,6 +254,36 @@ public final class OidcProviderConfig extends ProviderConfig {
       return this;
     }
 
+    /**
+     * Sets whether to enable the code response flow for the new provider. By default, this is not
+     * enabled if no response type is specified.
+     *
+     * <p>A client secret must be set for this response type.
+     *
+     * <p>Having both the code and ID token response flows is currently not supported.
+     *
+     * @param enabled A boolean signifying whether the code response type is supported.
+     */
+    public UpdateRequest setCodeResponseType(boolean enabled) {
+      Map<String, Boolean> map = ensureResponseType(properties);
+      map.put("code", enabled);
+      return this;
+    }
+
+    /**
+     * Sets whether to enable the ID token response flow for the new provider. By default, this is
+     * enabled if no response type is specified.
+     *
+     * <p>Having both the code and ID token response flows is currently not supported.
+     *
+     * @param enabled A boolean signifying whether the ID token response type is supported.
+     */
+    public UpdateRequest setIdTokenResponseType(boolean enabled) {
+      Map<String, Boolean> map = ensureResponseType(properties);
+      map.put("idToken", enabled);
+      return this;
+    }
+
     UpdateRequest getThis() {
       return this;
     }
diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java
index 0af08f6..dea8fc9 100644
--- a/src/main/java/com/google/firebase/auth/UserRecord.java
+++ b/src/main/java/com/google/firebase/auth/UserRecord.java
@@ -475,6 +475,29 @@ public class UserRecord implements UserInfo {
       if (phone != null) {
         checkPhoneNumber(phone);
       }
+
+      if (phone == null && properties.containsKey("deleteProvider")) {
+        Object deleteProvider = properties.get("deleteProvider");
+        if (deleteProvider != null) {
+          // Due to java's type erasure, we can't fully check the type. :(
+          @SuppressWarnings("unchecked")
+          Iterable<String> deleteProviderIterable = (Iterable<String>)deleteProvider;
+
+          // If we've been told to unlink the phone provider both via setting phoneNumber to null
+          // *and* by setting providersToUnlink to include 'phone', then we'll reject that. Though
+          // it might also be reasonable to relax this restriction and just unlink it.
+          for (String dp : deleteProviderIterable) {
+            if (dp == "phone") {
+              throw new IllegalArgumentException(
+                  "Both UpdateRequest.setPhoneNumber(null) and "
+                  + "UpdateRequest.setProvidersToUnlink(['phone']) were set. To unlink from a "
+                  + "phone provider, only specify UpdateRequest.setPhoneNumber(null).");
+
+            }
+          }
+        }
+      }
+
       properties.put("phoneNumber", phone);
       return this;
     }
@@ -548,6 +571,52 @@ public class UserRecord implements UserInfo {
       return this;
     }
 
+    /**
+     * Links this user to the specified provider.
+     *
+     * <p>Linking a provider to an existing user account does not invalidate the
+     * refresh token of that account. In other words, the existing account
+     * would continue to be able to access resources, despite not having used
+     * the newly linked provider to log in. If you wish to force the user to
+     * authenticate with this new provider, you need to (a) revoke their
+     * refresh token (see
+     * https://firebase.google.com/docs/auth/admin/manage-sessions#revoke_refresh_tokens),
+     * and (b) ensure no other authentication methods are present on this
+     * account.
+     *
+     * @param providerToLink provider info to be linked to this user\'s account.
+     */
+    public UpdateRequest setProviderToLink(@NonNull UserProvider providerToLink) {
+      properties.put("linkProviderUserInfo", checkNotNull(providerToLink));
+      return this;
+    }
+
+    /**
+     * Unlinks this user from the specified providers.
+     *
+     * @param providerIds list of identifiers for the identity providers.
+     */
+    public UpdateRequest setProvidersToUnlink(Iterable<String> providerIds) {
+      checkNotNull(providerIds);
+      for (String id : providerIds) {
+        checkArgument(!Strings.isNullOrEmpty(id), "providerIds must not be null or empty");
+
+        if (id == "phone" && properties.containsKey("phoneNumber")
+            && properties.get("phoneNumber") == null) {
+          // If we've been told to unlink the phone provider both via setting phoneNumber to null
+          // *and* by setting providersToUnlink to include 'phone', then we'll reject that. Though
+          // it might also be reasonable to relax this restriction and just unlink it.
+          throw new IllegalArgumentException(
+              "Both UpdateRequest.setPhoneNumber(null) and "
+              + "UpdateRequest.setProvidersToUnlink(['phone']) were set. To unlink from a phone "
+              + "provider, only specify UpdateRequest.setPhoneNumber(null).");
+        }
+      }
+
+      properties.put("deleteProvider", providerIds);
+      return this;
+    }
+
     UpdateRequest setValidSince(long epochSeconds) {
       checkValidSince(epochSeconds);
       properties.put("validSince", epochSeconds);
@@ -569,7 +638,20 @@ public class UserRecord implements UserInfo {
       }
 
       if (copy.containsKey("phoneNumber") && copy.get("phoneNumber") == null) {
-        copy.put("deleteProvider", ImmutableList.of("phone"));
+        Object deleteProvider = copy.get("deleteProvider");
+        if (deleteProvider != null) {
+          // Due to java's type erasure, we can't fully check the type. :(
+          @SuppressWarnings("unchecked")
+          Iterable<String> deleteProviderIterable = (Iterable<String>)deleteProvider;
+
+          copy.put("deleteProvider", new ImmutableList.Builder<String>()
+              .addAll(deleteProviderIterable)
+              .add("phone")
+              .build());
+        } else {
+          copy.put("deleteProvider", ImmutableList.of("phone"));
+        }
+
         copy.remove("phoneNumber");
       }
 
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/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
index 35fa21d..0d65d51 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
@@ -18,6 +18,7 @@ package com.google.firebase.auth;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
@@ -265,6 +266,35 @@ public class FirebaseAuthIT {
     checkRecreateUser(randomUser.getUid());
   }
 
+  @Test
+  public void testLookupUserByPhone() throws Exception {
+    UserRecord user1 = createTemporaryUser();
+    UserRecord user2 = importTemporaryUser();
+
+    UserRecord lookedUpRecord = auth.getUserByPhoneNumberAsync(
+        user1.getPhoneNumber()).get();
+    assertEquals(user1.getUid(), lookedUpRecord.getUid());
+
+    lookedUpRecord = auth.getUserByPhoneNumberAsync(user2.getPhoneNumber()).get();
+    assertEquals(user2.getUid(), lookedUpRecord.getUid());
+  }
+
+  @Test
+  public void testLookupUserByProviderUid() throws Exception {
+    UserRecord user = importTemporaryUser();
+
+    UserRecord lookedUpRecord = auth.getUserByProviderUidAsync(
+        "google.com", user.getUid() + "_google.com").get();
+    assertEquals(user.getUid(), lookedUpRecord.getUid());
+    assertEquals(2, lookedUpRecord.getProviderData().length);
+    List<String> providers = new ArrayList<>();
+    for (UserInfo provider : lookedUpRecord.getProviderData()) {
+      providers.add(provider.getProviderId());
+    }
+    assertTrue(providers.contains("phone"));
+    assertTrue(providers.contains("google.com"));
+  }
+
   @Test
   public void testUserLifecycle() throws Exception {
     // Create user
@@ -307,6 +337,65 @@ public class FirebaseAuthIT {
     assertEquals(2, userRecord.getProviderData().length);
     assertTrue(userRecord.getCustomClaims().isEmpty());
 
+    // Link user to IDP providers
+    request = userRecord.updateRequest()
+        .setProviderToLink(
+            UserProvider
+                .builder()
+                .setUid("testuid")
+                .setProviderId("google.com")
+                .setEmail("test@example.com")
+                .setDisplayName("Test User")
+                .setPhotoUrl("https://test.com/user.png")
+                .build());
+    userRecord = auth.updateUserAsync(request).get();
+    assertEquals(uid, userRecord.getUid());
+    assertEquals("Updated Name", userRecord.getDisplayName());
+    assertEquals(randomUser.getEmail(), userRecord.getEmail());
+    assertEquals(randomUser.getPhoneNumber(), userRecord.getPhoneNumber());
+    assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl());
+    assertTrue(userRecord.isEmailVerified());
+    assertFalse(userRecord.isDisabled());
+    assertEquals(3, userRecord.getProviderData().length);
+    List<String> providers = new ArrayList<>();
+    for (UserInfo provider : userRecord.getProviderData()) {
+      providers.add(provider.getProviderId());
+    }
+    assertTrue(providers.contains("google.com"));
+    assertTrue(userRecord.getCustomClaims().isEmpty());
+
+    // Unlink phone provider
+    request = userRecord.updateRequest().setProvidersToUnlink(ImmutableList.of("phone"));
+    userRecord = auth.updateUserAsync(request).get();
+    assertNull(userRecord.getPhoneNumber());
+    assertEquals(2, userRecord.getProviderData().length);
+    providers.clear();
+    for (UserInfo provider : userRecord.getProviderData()) {
+      providers.add(provider.getProviderId());
+    }
+    assertFalse(providers.contains("phone"));
+    assertEquals(uid, userRecord.getUid());
+    assertEquals("Updated Name", userRecord.getDisplayName());
+    assertEquals(randomUser.getEmail(), userRecord.getEmail());
+    assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl());
+    assertTrue(userRecord.isEmailVerified());
+    assertFalse(userRecord.isDisabled());
+    assertTrue(userRecord.getCustomClaims().isEmpty());
+
+    // Unlink IDP provider
+    request = userRecord.updateRequest().setProvidersToUnlink(ImmutableList.of("google.com"));
+    userRecord = auth.updateUserAsync(request).get();
+    assertEquals(1, userRecord.getProviderData().length);
+    assertNotEquals("google.com", userRecord.getProviderData()[0].getProviderId());
+    assertEquals(uid, userRecord.getUid());
+    assertEquals("Updated Name", userRecord.getDisplayName());
+    assertEquals(randomUser.getEmail(), userRecord.getEmail());
+    assertNull(userRecord.getPhoneNumber());
+    assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl());
+    assertTrue(userRecord.isEmailVerified());
+    assertFalse(userRecord.isDisabled());
+    assertTrue(userRecord.getCustomClaims().isEmpty());
+
     // Get user by email
     userRecord = auth.getUserByEmailAsync(userRecord.getEmail()).get();
     assertEquals(uid, userRecord.getUid());
@@ -703,12 +792,19 @@ public class FirebaseAuthIT {
             .setDisplayName("DisplayName")
             .setEnabled(true)
             .setClientId("ClientId")
-            .setIssuer("https://oidc.com/issuer"));
+            .setClientSecret("ClientSecret")
+            .setIssuer("https://oidc.com/issuer")
+            .setCodeResponseType(true)
+            .setIdTokenResponseType(false));
+
     assertEquals(providerId, config.getProviderId());
     assertEquals("DisplayName", config.getDisplayName());
     assertTrue(config.isEnabled());
     assertEquals("ClientId", config.getClientId());
+    assertEquals("ClientSecret", config.getClientSecret());
     assertEquals("https://oidc.com/issuer", config.getIssuer());
+    assertTrue(config.isCodeResponseType());
+    assertFalse(config.isIdTokenResponseType());
 
     // Get provider config
     config = auth.getOidcProviderConfigAsync(providerId).get();
@@ -716,7 +812,10 @@ public class FirebaseAuthIT {
     assertEquals("DisplayName", config.getDisplayName());
     assertTrue(config.isEnabled());
     assertEquals("ClientId", config.getClientId());
+    assertEquals("ClientSecret", config.getClientSecret());
     assertEquals("https://oidc.com/issuer", config.getIssuer());
+    assertTrue(config.isCodeResponseType());
+    assertFalse(config.isIdTokenResponseType());
 
     // Update provider config
     OidcProviderConfig.UpdateRequest updateRequest =
@@ -724,13 +823,20 @@ public class FirebaseAuthIT {
             .setDisplayName("NewDisplayName")
             .setEnabled(false)
             .setClientId("NewClientId")
-            .setIssuer("https://oidc.com/new-issuer");
+            .setClientSecret("NewClientSecret")
+            .setIssuer("https://oidc.com/new-issuer")
+            .setCodeResponseType(false)
+            .setIdTokenResponseType(true);
+
     config = auth.updateOidcProviderConfigAsync(updateRequest).get();
     assertEquals(providerId, config.getProviderId());
     assertEquals("NewDisplayName", config.getDisplayName());
     assertFalse(config.isEnabled());
     assertEquals("NewClientId", config.getClientId());
+    assertEquals("NewClientSecret", config.getClientSecret());
     assertEquals("https://oidc.com/new-issuer", config.getIssuer());
+    assertTrue(config.isIdTokenResponseType());
+    assertFalse(config.isCodeResponseType());
 
     // Delete provider config
     temporaryProviderConfig.deleteOidcProviderConfig(providerId);
@@ -929,6 +1035,54 @@ public class FirebaseAuthIT {
     assertNull(error.get());
   }
 
+  /**
+   * Create a temporary user. This user will automatically be cleaned up after testing completes.
+   */
+  private UserRecord createTemporaryUser() throws Exception {
+    RandomUser randomUser = UserTestUtils.generateRandomUserInfo();
+
+    UserRecord.CreateRequest user = new UserRecord.CreateRequest()
+        .setUid(randomUser.getUid())
+        .setDisplayName("Random User")
+        .setEmail(randomUser.getEmail())
+        .setEmailVerified(true)
+        .setPhoneNumber(randomUser.getPhoneNumber())
+        .setPhotoUrl("https://example.com/photo.png")
+        .setPassword("password");
+
+    return temporaryUser.create(user);
+  }
+
+  /**
+   * Import a temporary user. This user will automatically be cleaned up after testing completes.
+   */
+  private UserRecord importTemporaryUser() throws Exception {
+    RandomUser randomUser = UserTestUtils.generateRandomUserInfo();
+
+    ImportUserRecord.Builder builder = ImportUserRecord.builder()
+        .setUid(randomUser.getUid())
+        .setDisabled(false)
+        .setEmail(randomUser.getEmail())
+        .setEmailVerified(true)
+        .setPhoneNumber(randomUser.getPhoneNumber())
+        .setUserMetadata(
+            new UserMetadata(/* creationTimestamp= */ 20L, /* lastSignInTimestamp= */ 20L,
+                /* lastRefreshTimestamp= */ 20L))
+        .addUserProvider(
+            UserProvider.builder()
+            .setProviderId("google.com")
+            .setUid(randomUser.getUid() + "_google.com")
+            .build());
+
+    ImportUserRecord user = builder.build();
+    UserImportResult result = auth.importUsersAsync(ImmutableList.of(user)).get();
+    assertEquals(result.getSuccessCount(), 1);
+    assertEquals(result.getFailureCount(), 0);
+    temporaryUser.registerUid(randomUser.getUid());
+
+    return auth.getUserAsync(randomUser.getUid()).get();
+  }
+
   private Map<String, String> parseLinkParameters(String link) throws Exception {
     Map<String, String> result = new HashMap<>();
     int queryBegin = link.indexOf('?');
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..4014e05 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;
@@ -84,18 +85,30 @@ public class FirebaseUserManagerTest {
 
   private static final Map<String, Object> ACTION_CODE_SETTINGS_MAP =
           ACTION_CODE_SETTINGS.getProperties();
+  private static final UserProvider USER_PROVIDER = UserProvider.builder()
+        .setUid("testuid")
+        .setProviderId("facebook.com")
+        .setEmail("test@example.com")
+        .setDisplayName("Test User")
+        .setPhotoUrl("https://test.com/user.png")
+        .build();
 
   private static final String PROJECT_BASE_URL =
       "https://identitytoolkit.googleapis.com/v2/projects/test-project-id";
 
   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();
   }
 
@@ -349,6 +362,56 @@ public class FirebaseUserManagerTest {
     }
   }
 
+  @Test
+  public void testGetUserByProviderUidWithInvalidProviderId() throws Exception {
+    initializeAppForUserManagement();
+    try {
+      FirebaseAuth.getInstance().getUserByProviderUidAsync("", "uid").get();
+      fail("No error thrown for invalid request");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testGetUserByProviderUidWithInvalidProviderUid() throws Exception {
+    initializeAppForUserManagement();
+    try {
+      FirebaseAuth.getInstance().getUserByProviderUidAsync("id", "").get();
+      fail("No error thrown for invalid request");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testGetUserByProviderUidWithValidInput() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("getUser.json"));
+    UserRecord userRecord = FirebaseAuth.getInstance()
+        .getUserByProviderUidAsync("google.com", "google_uid").get();
+    checkUserRecord(userRecord);
+    checkRequestHeaders(interceptor);
+  }
+
+  @Test
+  public void testGetUserByProviderUidWithPhone() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("getUser.json"));
+    UserRecord userRecord = FirebaseAuth.getInstance()
+        .getUserByProviderUidAsync("phone", "+1234567890").get();
+    checkUserRecord(userRecord);
+    checkRequestHeaders(interceptor);
+  }
+
+  @Test
+  public void testGetUserByProviderUidWithEmail() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("getUser.json"));
+    UserRecord userRecord = FirebaseAuth.getInstance()
+        .getUserByProviderUidAsync("email", "testuser@example.com").get();
+    checkUserRecord(userRecord);
+    checkRequestHeaders(interceptor);
+  }
+
   @Test
   public void testListUsers() throws Exception {
     final TestResponseInterceptor interceptor = initializeAppForUserManagement(
@@ -1091,8 +1154,10 @@ public class FirebaseUserManagerTest {
         .setEmailVerified(true)
         .setPassword("secret")
         .setCustomClaims(claims)
+        .setProviderToLink(USER_PROVIDER)
+        .setProvidersToUnlink(ImmutableList.of("google.com"))
         .getProperties(JSON_FACTORY);
-    assertEquals(8, map.size());
+    assertEquals(10, map.size());
     assertEquals(update.getUid(), map.get("localId"));
     assertEquals("Display Name", map.get("displayName"));
     assertEquals("http://test.com/example.png", map.get("photoUrl"));
@@ -1101,6 +1166,8 @@ public class FirebaseUserManagerTest {
     assertTrue((Boolean) map.get("emailVerified"));
     assertEquals("secret", map.get("password"));
     assertEquals(JSON_FACTORY.toString(claims), map.get("customAttributes"));
+    assertEquals(USER_PROVIDER, map.get("linkProviderUserInfo"));
+    assertEquals(ImmutableList.of("google.com"), map.get("deleteProvider"));
   }
 
   @Test
@@ -1138,6 +1205,64 @@ public class FirebaseUserManagerTest {
     assertEquals("{}", map.get("customAttributes"));
   }
 
+  @Test
+  public void testLinkProvider() {
+    UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test");
+    Map<String, Object> map = update
+        .setProviderToLink(USER_PROVIDER)
+        .getProperties(Utils.getDefaultJsonFactory());
+    assertEquals(2, map.size());
+    assertEquals(update.getUid(), map.get("localId"));
+    assertEquals(USER_PROVIDER, map.get("linkProviderUserInfo"));
+  }
+
+  @Test
+  public void testDeleteProvider() {
+    UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test");
+    Map<String, Object> map = update
+        .setProvidersToUnlink(ImmutableList.of("google.com"))
+        .getProperties(Utils.getDefaultJsonFactory());
+    assertEquals(2, map.size());
+    assertEquals(update.getUid(), map.get("localId"));
+    assertEquals(ImmutableList.of("google.com"), map.get("deleteProvider"));
+  }
+
+  @Test
+  public void testDeleteProviderAndPhone() {
+    UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test");
+    Map<String, Object> map = update
+        .setProvidersToUnlink(ImmutableList.of("google.com"))
+        .setPhoneNumber(null)
+        .getProperties(Utils.getDefaultJsonFactory());
+    assertEquals(2, map.size());
+    assertEquals(update.getUid(), map.get("localId"));
+    assertEquals(ImmutableList.of("google.com", "phone"), map.get("deleteProvider"));
+  }
+
+  @Test
+  public void testDoubleDeletePhoneProvider() throws Exception {
+    UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("uid")
+        .setPhoneNumber(null);
+
+    try {
+      update.setProvidersToUnlink(ImmutableList.of("phone"));
+      fail("No error thrown for double delete phone provider");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testDoubleDeletePhoneProviderReverseOrder() throws Exception {
+    UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("uid")
+        .setProvidersToUnlink(ImmutableList.of("phone"));
+
+    try {
+      update.setPhoneNumber(null);
+      fail("No error thrown for double delete phone provider");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
   @Test
   public void testDeleteDisplayName() {
     Map<String, Object> map = new UserRecord.UpdateRequest("test")
@@ -1490,7 +1615,10 @@ public class FirebaseUserManagerTest {
             .setDisplayName("DISPLAY_NAME")
             .setEnabled(true)
             .setClientId("CLIENT_ID")
-            .setIssuer("https://oidc.com/issuer");
+            .setClientSecret("CLIENT_SECRET")
+            .setIssuer("https://oidc.com/issuer")
+            .setCodeResponseType(true)
+            .setIdTokenResponseType(true);
 
     OidcProviderConfig config = FirebaseAuth.getInstance().createOidcProviderConfig(createRequest);
 
@@ -1501,7 +1629,13 @@ public class FirebaseUserManagerTest {
     assertEquals("DISPLAY_NAME", parsed.get("displayName"));
     assertTrue((boolean) parsed.get("enabled"));
     assertEquals("CLIENT_ID", parsed.get("clientId"));
+    assertEquals("CLIENT_SECRET", parsed.get("clientSecret"));
     assertEquals("https://oidc.com/issuer", parsed.get("issuer"));
+
+    Map<String, Boolean> responseType = (Map<String, Boolean>) parsed.get("responseType");
+    assertTrue(responseType.get("code"));
+    assertTrue(responseType.get("idToken"));
+
     GenericUrl url = interceptor.getResponse().getRequest().getUrl();
     assertEquals("oidc.provider-id", url.getFirst("oauthIdpConfigId"));
   }
@@ -1515,9 +1649,12 @@ public class FirebaseUserManagerTest {
             .setDisplayName("DISPLAY_NAME")
             .setEnabled(true)
             .setClientId("CLIENT_ID")
-            .setIssuer("https://oidc.com/issuer");
+            .setClientSecret("CLIENT_SECRET")
+            .setIssuer("https://oidc.com/issuer")
+            .setCodeResponseType(true)
+            .setIdTokenResponseType(true);
 
-    OidcProviderConfig config = 
+    OidcProviderConfig config =
         FirebaseAuth.getInstance().createOidcProviderConfigAsync(createRequest).get();
 
     checkOidcProviderConfig(config, "oidc.provider-id");
@@ -1527,7 +1664,13 @@ public class FirebaseUserManagerTest {
     assertEquals("DISPLAY_NAME", parsed.get("displayName"));
     assertTrue((boolean) parsed.get("enabled"));
     assertEquals("CLIENT_ID", parsed.get("clientId"));
+    assertEquals("CLIENT_SECRET", parsed.get("clientSecret"));
     assertEquals("https://oidc.com/issuer", parsed.get("issuer"));
+
+    Map<String, Boolean> responseType = (Map<String, Boolean>) parsed.get("responseType");
+    assertTrue(responseType.get("code"));
+    assertTrue(responseType.get("idToken"));
+
     GenericUrl url = interceptor.getResponse().getRequest().getUrl();
     assertEquals("oidc.provider-id", url.getFirst("oauthIdpConfigId"));
   }
@@ -1730,7 +1873,10 @@ public class FirebaseUserManagerTest {
             .setDisplayName("DISPLAY_NAME")
             .setEnabled(true)
             .setClientId("CLIENT_ID")
-            .setIssuer("https://oidc.com/issuer");
+            .setClientSecret("CLIENT_SECRET")
+            .setIssuer("https://oidc.com/issuer")
+            .setCodeResponseType(true)
+            .setIdTokenResponseType(true);
 
     OidcProviderConfig config = tenantAwareAuth.updateOidcProviderConfig(request);
 
@@ -1739,12 +1885,18 @@ public class FirebaseUserManagerTest {
     String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/oidc.provider-id";
     checkUrl(interceptor, "PATCH", expectedUrl);
     GenericUrl url = interceptor.getResponse().getRequest().getUrl();
-    assertEquals("clientId,displayName,enabled,issuer", url.getFirst("updateMask"));
+    assertEquals("clientId,clientSecret,displayName,enabled,issuer,responseType.code,"
+        + "responseType.idToken", url.getFirst("updateMask"));
     GenericJson parsed = parseRequestContent(interceptor);
     assertEquals("DISPLAY_NAME", parsed.get("displayName"));
     assertTrue((boolean) parsed.get("enabled"));
     assertEquals("CLIENT_ID", parsed.get("clientId"));
+    assertEquals("CLIENT_SECRET", parsed.get("clientSecret"));
     assertEquals("https://oidc.com/issuer", parsed.get("issuer"));
+
+    Map<String, Boolean> responseType = (Map<String, Boolean>) parsed.get("responseType");
+    assertTrue(responseType.get("code"));
+    assertTrue(responseType.get("idToken"));
   }
 
   @Test
@@ -2678,6 +2830,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()
@@ -2761,7 +2940,7 @@ public class FirebaseUserManagerTest {
     assertEquals("http://www.example.com/testuser/photo.png", userRecord.getPhotoUrl());
     assertEquals(1234567890, userRecord.getUserMetadata().getCreationTimestamp());
     assertEquals(0, userRecord.getUserMetadata().getLastSignInTimestamp());
-    assertEquals(2, userRecord.getProviderData().length);
+    assertEquals(3, userRecord.getProviderData().length);
     assertFalse(userRecord.isDisabled());
     assertTrue(userRecord.isEmailVerified());
     assertEquals(1494364393000L, userRecord.getTokensValidAfterTimestamp());
@@ -2781,6 +2960,10 @@ public class FirebaseUserManagerTest {
     assertEquals("+1234567890", provider.getPhoneNumber());
     assertEquals("phone", provider.getProviderId());
 
+    provider = userRecord.getProviderData()[2];
+    assertEquals("google_uid", provider.getUid());
+    assertEquals("google.com", provider.getProviderId());
+
     Map<String, Object> claims = userRecord.getCustomClaims();
     assertEquals(2, claims.size());
     assertTrue((boolean) claims.get("admin"));
@@ -2792,7 +2975,10 @@ public class FirebaseUserManagerTest {
     assertEquals("DISPLAY_NAME", config.getDisplayName());
     assertTrue(config.isEnabled());
     assertEquals("CLIENT_ID", config.getClientId());
+    assertEquals("CLIENT_SECRET", config.getClientSecret());
     assertEquals("https://oidc.com/issuer", config.getIssuer());
+    assertTrue(config.isCodeResponseType());
+    assertFalse(config.isIdTokenResponseType());
   }
 
   private static void checkSamlProviderConfig(SamlProviderConfig config, String providerId) {
@@ -2824,5 +3010,4 @@ public class FirebaseUserManagerTest {
   private interface UserManagerOp {
     void call(FirebaseAuth auth) throws Exception;
   }
-
 }
diff --git a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java
index 1fb4ca3..b8a5d50 100644
--- a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java
+++ b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java
@@ -18,11 +18,13 @@ package com.google.firebase.auth;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import com.google.api.client.googleapis.util.Utils;
 import com.google.api.client.json.JsonFactory;
 import java.io.IOException;
+import java.util.HashMap;
 import java.util.Map;
 import org.junit.Test;
 
@@ -36,7 +38,12 @@ public class OidcProviderConfigTest {
         + "  'displayName': 'DISPLAY_NAME',"
         + "  'enabled':      true,"
         + "  'clientId':    'CLIENT_ID',"
-        + "  'issuer':      'https://oidc.com/issuer'"
+        + "  'clientSecret':'CLIENT_SECRET',"
+        + "  'issuer':      'https://oidc.com/issuer',"
+        + "  'responseType': {"
+        + "    'code':  true,"
+        + "    'idToken': false"
+        + "   }"
         + "}").replace("'", "\"");
 
   @Test
@@ -47,7 +54,35 @@ public class OidcProviderConfigTest {
     assertEquals("DISPLAY_NAME", config.getDisplayName());
     assertTrue(config.isEnabled());
     assertEquals("CLIENT_ID", config.getClientId());
+    assertEquals("CLIENT_SECRET", config.getClientSecret());
     assertEquals("https://oidc.com/issuer", config.getIssuer());
+    assertTrue(config.isCodeResponseType());
+    assertFalse(config.isIdTokenResponseType());
+  }
+
+  @Test
+  public void testEnsureResponseType() {
+    Map<String, Object> properties = new HashMap<>();
+
+    Map<String, Boolean> responseType = OidcProviderConfig.ensureResponseType(properties);
+
+    assertNotNull(responseType);
+    assertEquals(responseType, properties.get("responseType"));
+  }
+
+  @Test
+  public void testEnsureResponseTypeAlreadyPresent() {
+    Map<String, Object> properties = new HashMap<>();
+    Map<String, Boolean> responseType = new HashMap<>();
+    responseType.put("code", true);
+    properties.put("responseType", responseType);
+
+    Map<String, Boolean> returnedResponseType = OidcProviderConfig.ensureResponseType(properties);
+
+    assertEquals(responseType, returnedResponseType);
+    assertTrue(returnedResponseType.get("code"));
+    assertEquals(returnedResponseType.size(), 1);
+    assertEquals(responseType, properties.get("responseType"));
   }
 
   @Test
@@ -58,15 +93,23 @@ public class OidcProviderConfigTest {
       .setDisplayName("DISPLAY_NAME")
       .setEnabled(false)
       .setClientId("CLIENT_ID")
-      .setIssuer("https://oidc.com/issuer");
+      .setClientSecret("CLIENT_SECRET")
+      .setIssuer("https://oidc.com/issuer")
+      .setCodeResponseType(true)
+      .setIdTokenResponseType(false);
 
     assertEquals("oidc.provider-id", createRequest.getProviderId());
     Map<String,Object> properties = createRequest.getProperties();
-    assertEquals(properties.size(), 4);
+    assertEquals(properties.size(), 6);
     assertEquals("DISPLAY_NAME", (String) properties.get("displayName"));
     assertFalse((boolean) properties.get("enabled"));
     assertEquals("CLIENT_ID", (String) properties.get("clientId"));
+    assertEquals("CLIENT_SECRET", properties.get("clientSecret"));
     assertEquals("https://oidc.com/issuer", (String) properties.get("issuer"));
+
+    Map<String, Boolean> responseType = (Map<String, Boolean>) properties.get("responseType");
+    assertTrue(responseType.get("code"));
+    assertFalse(responseType.get("idToken"));
   }
 
   @Test(expected = IllegalArgumentException.class)
@@ -99,6 +142,16 @@ public class OidcProviderConfigTest {
     new OidcProviderConfig.CreateRequest().setIssuer("not a valid url");
   }
 
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestMissingClientSecret() {
+    new OidcProviderConfig.CreateRequest().setClientSecret(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestEmptyClientSecret() {
+    new OidcProviderConfig.CreateRequest().setClientSecret("");
+  }
+
   @Test
   public void testUpdateRequestFromOidcProviderConfig() throws IOException {
     OidcProviderConfig config = jsonFactory.fromString(OIDC_JSON_STRING, OidcProviderConfig.class);
@@ -117,15 +170,23 @@ public class OidcProviderConfigTest {
       .setDisplayName("DISPLAY_NAME")
       .setEnabled(false)
       .setClientId("CLIENT_ID")
-      .setIssuer("https://oidc.com/issuer");
+      .setClientSecret("CLIENT_SECRET")
+      .setIssuer("https://oidc.com/issuer")
+      .setCodeResponseType(true)
+      .setIdTokenResponseType(false);
 
     assertEquals("oidc.provider-id", updateRequest.getProviderId());
     Map<String,Object> properties = updateRequest.getProperties();
-    assertEquals(properties.size(), 4);
+    assertEquals(properties.size(), 6);
     assertEquals("DISPLAY_NAME", (String) properties.get("displayName"));
     assertFalse((boolean) properties.get("enabled"));
     assertEquals("CLIENT_ID", (String) properties.get("clientId"));
+    assertEquals("CLIENT_SECRET", (String) properties.get("clientSecret"));
     assertEquals("https://oidc.com/issuer", (String) properties.get("issuer"));
+
+    Map<String, Boolean> responseType = (Map<String, Boolean>) properties.get("responseType");
+    assertTrue(responseType.get("code"));
+    assertFalse(responseType.get("idToken"));
   }
 
   @Test(expected = IllegalArgumentException.class)
@@ -157,4 +218,14 @@ public class OidcProviderConfigTest {
   public void testUpdateRequestInvalidIssuerUrl() {
     new OidcProviderConfig.UpdateRequest("oidc.provider-id").setIssuer("not a valid url");
   }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestMissingClientSecret() {
+    new OidcProviderConfig.UpdateRequest("oidc.provider-id").setClientSecret(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestEmptyClientSecret() {
+    new OidcProviderConfig.UpdateRequest("oidc.provider-id").setClientSecret("");
+  }
 }
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());
diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java
index 1ebac21..3c4578e 100644
--- a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java
@@ -279,18 +279,28 @@ public class TenantAwareFirebaseAuthIT {
               .setDisplayName("DisplayName")
               .setEnabled(true)
               .setClientId("ClientId")
-              .setIssuer("https://oidc.com/issuer"));
+              .setClientSecret("ClientSecret")
+              .setIssuer("https://oidc.com/issuer")
+              .setCodeResponseType(true)
+              .setIdTokenResponseType(false));
+
     assertEquals(providerId, config.getProviderId());
     assertEquals("DisplayName", config.getDisplayName());
     assertEquals("ClientId", config.getClientId());
+    assertEquals("ClientSecret", config.getClientSecret());
     assertEquals("https://oidc.com/issuer", config.getIssuer());
+    assertTrue(config.isCodeResponseType());
+    assertFalse(config.isIdTokenResponseType());
 
     // Get provider config
     config = tenantAwareAuth.getOidcProviderConfigAsync(providerId).get();
     assertEquals(providerId, config.getProviderId());
     assertEquals("DisplayName", config.getDisplayName());
     assertEquals("ClientId", config.getClientId());
+    assertEquals("ClientSecret", config.getClientSecret());
     assertEquals("https://oidc.com/issuer", config.getIssuer());
+    assertTrue(config.isCodeResponseType());
+    assertFalse(config.isIdTokenResponseType());
 
     // Update provider config
     OidcProviderConfig.UpdateRequest updateRequest =
@@ -298,13 +308,20 @@ public class TenantAwareFirebaseAuthIT {
             .setDisplayName("NewDisplayName")
             .setEnabled(false)
             .setClientId("NewClientId")
-            .setIssuer("https://oidc.com/new-issuer");
+            .setClientSecret("NewClientSecret")
+            .setIssuer("https://oidc.com/new-issuer")
+            .setCodeResponseType(false)
+            .setIdTokenResponseType(true);
+
     config = tenantAwareAuth.updateOidcProviderConfigAsync(updateRequest).get();
     assertEquals(providerId, config.getProviderId());
     assertEquals("NewDisplayName", config.getDisplayName());
     assertFalse(config.isEnabled());
     assertEquals("NewClientId", config.getClientId());
+    assertEquals("NewClientSecret", config.getClientSecret());
     assertEquals("https://oidc.com/new-issuer", config.getIssuer());
+    assertFalse(config.isCodeResponseType());
+    assertTrue(config.isIdTokenResponseType());
 
     // Delete provider config
     temporaryProviderConfig.deleteOidcProviderConfig(providerId);
diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigIT.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigIT.java
index bb90c6c..54b7438 100644
--- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigIT.java
+++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigIT.java
@@ -53,6 +53,11 @@ public class FirebaseRemoteConfigIT {
     remoteConfig = FirebaseRemoteConfig.getInstance(IntegrationTestUtils.ensureDefaultApp());
   }
 
+  @Test
+  public void testToTriggerNightlyEmailNotifications() {
+    assertEquals("a", "b");
+  }
+
   @Test
   public void testValidateTemplate() throws FirebaseRemoteConfigException {
     final Template inputTemplate = remoteConfig.getTemplate();
diff --git a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java
index e86496a..e96170c 100644
--- a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java
+++ b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java
@@ -94,6 +94,16 @@ public class FirebaseAuthSnippets {
     // [END get_user_by_phone]
   }
 
+  public static void getUserByProviderUId(
+      String providerId, String uid) throws FirebaseAuthException {
+    // [START get_user_by_provider_uid]
+    UserRecord userRecord = FirebaseAuth.getInstance().getUserByProviderUid(
+        providerId, uid);
+    // See the UserRecord reference doc for the contents of userRecord.
+    System.out.println("Successfully fetched user data: " + userRecord.getUid());
+    // [END get_user_by_provider_uid]
+  }
+
   public static void createUser() throws FirebaseAuthException {
     // [START create_user]
     CreateRequest request = new CreateRequest()
diff --git a/src/test/resources/getUser.json b/src/test/resources/getUser.json
index 3d57f10..8d9ff32 100644
--- a/src/test/resources/getUser.json
+++ b/src/test/resources/getUser.json
@@ -17,6 +17,9 @@
       "providerId" : "phone",
       "phoneNumber" : "+1234567890",
       "rawId" : "+1234567890"
+    }, {
+      "providerId" : "google.com",
+      "rawId" : "google_uid"
     } ],
     "photoUrl" : "http://www.example.com/testuser/photo.png",
     "passwordHash" : "passwordhash",
diff --git a/src/test/resources/listOidc.json b/src/test/resources/listOidc.json
index 0c13ea4..82a2f76 100644
--- a/src/test/resources/listOidc.json
+++ b/src/test/resources/listOidc.json
@@ -4,12 +4,22 @@
     "displayName" : "DISPLAY_NAME",
     "enabled" : true,
     "clientId" : "CLIENT_ID",
-    "issuer" : "https://oidc.com/issuer"
+    "clientSecret" : "CLIENT_SECRET",
+    "issuer" : "https://oidc.com/issuer",
+    "responseType" : {
+      "code": true,
+      "idToken": false
+    }
   }, {
     "name": "projects/projectId/oauthIdpConfigs/oidc.provider-id2",
     "displayName" : "DISPLAY_NAME",
     "enabled" : true,
     "clientId" : "CLIENT_ID",
-    "issuer" : "https://oidc.com/issuer"
+    "clientSecret" : "CLIENT_SECRET",
+    "issuer" : "https://oidc.com/issuer",
+    "responseType" : {
+      "code": true,
+      "idToken": false
+    }
   } ]
 }
diff --git a/src/test/resources/listUsers.json b/src/test/resources/listUsers.json
index 47e1697..c2f99c1 100644
--- a/src/test/resources/listUsers.json
+++ b/src/test/resources/listUsers.json
@@ -16,6 +16,9 @@
       "providerId" : "phone",
       "phoneNumber" : "+1234567890",
       "rawId" : "+1234567890"
+    }, {
+      "providerId" : "google.com",
+      "rawId" : "google_uid"
     } ],
     "photoUrl" : "http://www.example.com/testuser/photo.png",
     "passwordHash" : "passwordHash",
@@ -43,6 +46,9 @@
       "providerId" : "phone",
       "phoneNumber" : "+1234567890",
       "rawId" : "+1234567890"
+    }, {
+      "providerId" : "google.com",
+      "rawId" : "google_uid"
     } ],
     "photoUrl" : "http://www.example.com/testuser/photo.png",
     "passwordHash" : "passwordHash",
diff --git a/src/test/resources/oidc.json b/src/test/resources/oidc.json
index e2f1845..0aed0df 100644
--- a/src/test/resources/oidc.json
+++ b/src/test/resources/oidc.json
@@ -3,5 +3,10 @@
   "displayName" : "DISPLAY_NAME",
   "enabled" : true,
   "clientId" : "CLIENT_ID",
-  "issuer" : "https://oidc.com/issuer"
+  "clientSecret" : "CLIENT_SECRET",
+  "issuer" : "https://oidc.com/issuer",
+  "responseType" : {
+    "code": true,
+    "idToken": false
+  }
 }