Repository: firebase/firebase-admin-java
Pull request #404 (2020-05-04 21:01:57)

Add operation to list OIDC provider configs.
This adds an operation to list OIDC provider configs (can be done using either the tenant-aware or standard Firebase client). This is the last OIDC provider config operation that needs to be implemented.

This work is part of adding multi-tenancy support (see issue #332).

Please take note of the generics I have chosen to use in ListProviderConfigsPage. This should help reduce a lot of code duplication when it comes time to add support for SAML provider configs.


Patch:
diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
index 40166dd..de9575e 100644
--- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
@@ -29,8 +29,10 @@ import com.google.common.base.Suppliers;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.auth.FirebaseUserManager.EmailLinkType;
 import com.google.firebase.auth.FirebaseUserManager.UserImportRequest;
+import com.google.firebase.auth.ListProviderConfigsPage;
+import com.google.firebase.auth.ListProviderConfigsPage.DefaultOidcProviderConfigSource;
+import com.google.firebase.auth.ListUsersPage;
 import com.google.firebase.auth.ListUsersPage.DefaultUserSource;
-import com.google.firebase.auth.ListUsersPage.PageFactory;
 import com.google.firebase.auth.UserRecord;
 import com.google.firebase.auth.internal.FirebaseTokenFactory;
 import com.google.firebase.internal.CallableOperation;
@@ -500,8 +502,8 @@ public abstract class AbstractFirebaseAuth {
       @Nullable final String pageToken, final int maxResults) {
     checkNotDestroyed();
     final FirebaseUserManager userManager = getUserManager();
-    final PageFactory factory =
-        new PageFactory(new DefaultUserSource(userManager, jsonFactory), maxResults, pageToken);
+    final DefaultUserSource source = new DefaultUserSource(userManager, jsonFactory);
+    final ListUsersPage.Factory factory = new ListUsersPage.Factory(source, maxResults, pageToken);
     return new CallableOperation<ListUsersPage, FirebaseAuthException>() {
       @Override
       protected ListUsersPage execute() throws FirebaseAuthException {
@@ -1033,6 +1035,7 @@ public abstract class AbstractFirebaseAuth {
 
   /**
    * Similar to {@link #getOidcProviderConfig(String)} but performs the operation asynchronously.
+   * Page size will be limited to 100 provider configs.
    *
    * @param providerId A provider ID string.
    * @return An {@code ApiFuture} which will complete successfully with an
@@ -1058,6 +1061,78 @@ public abstract class AbstractFirebaseAuth {
     };
   }
 
+  /**
+   * Gets a page of OIDC Auth provider configs starting from the specified {@code pageToken}.
+   *
+   * @param pageToken A non-empty page token string, or null to retrieve the first page of provider
+   *     configs.
+   * @param maxResults Maximum number of provider configs to include in the returned page. This may
+   *     not exceed 100.
+   * @return A {@link ListProviderConfigsPage} instance.
+   * @throws IllegalArgumentException If the specified page token is empty, or max results value is
+   *     invalid.
+   * @throws FirebaseAuthException If an error occurs while retrieving user data.
+   */
+  public ListProviderConfigsPage<OidcProviderConfig> listOidcProviderConfigs(
+        @Nullable String pageToken, int maxResults) throws FirebaseAuthException {
+    return listOidcProviderConfigsOp(pageToken, maxResults).call();
+  }
+
+  /**
+   * Similar to {@link #listlistOidcProviderConfigs(String)} but performs the operation
+   * asynchronously. Page size will be limited to 100 provider configs.
+   *
+   * @param pageToken A non-empty page token string, or null to retrieve the first page of provider
+   *     configs.
+   * @return An {@code ApiFuture} which will complete successfully with a
+   *     {@link ListProviderConfigsPage} instance. If an error occurs while retrieving provider
+   *     config data, the future throws an exception.
+   * @throws IllegalArgumentException If the specified page token is empty.
+   */
+  public ApiFuture<ListProviderConfigsPage<OidcProviderConfig>> listOidcProviderConfigsAsync(
+      @Nullable String pageToken) {
+    return listOidcProviderConfigsAsync(
+        pageToken,
+        FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS);
+  }
+
+  /**
+   * Similar to {@link #listOidcProviderConfigs(String, int)} but performs the operation
+   * asynchronously.
+   *
+   * @param pageToken A non-empty page token string, or null to retrieve the first page of provider
+   *     configs.
+   * @param maxResults Maximum number of provider configs to include in the returned page. This may
+   *     not exceed 100.
+   * @return An {@code ApiFuture} which will complete successfully with a
+   *     {@link ListProviderConfigsPage} instance. If an error occurs while retrieving provider
+   *     config data, the future throws an exception.
+   * @throws IllegalArgumentException If the specified page token is empty, or max results value is
+   *     invalid.
+   */
+  public ApiFuture<ListProviderConfigsPage<OidcProviderConfig>> listOidcProviderConfigsAsync(
+      @Nullable String pageToken,
+      int maxResults) {
+    return listOidcProviderConfigsOp(pageToken, maxResults).callAsync(firebaseApp);
+  }
+
+  private CallableOperation<ListProviderConfigsPage<OidcProviderConfig>, FirebaseAuthException>
+      listOidcProviderConfigsOp(@Nullable final String pageToken, final int maxResults) {
+    checkNotDestroyed();
+    final FirebaseUserManager userManager = getUserManager();
+    final DefaultOidcProviderConfigSource source = new DefaultOidcProviderConfigSource(userManager);
+    final ListProviderConfigsPage.Factory<OidcProviderConfig> factory =
+        new ListProviderConfigsPage.Factory<OidcProviderConfig>(source, maxResults, pageToken);
+    return
+      new CallableOperation<ListProviderConfigsPage<OidcProviderConfig>, FirebaseAuthException>() {
+        @Override
+        protected ListProviderConfigsPage<OidcProviderConfig> execute()
+            throws FirebaseAuthException {
+          return factory.create();
+        }
+    };
+  }
+
   /**
    * Deletes the provider config identified by the specified provider ID.
    *
diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
index d3c4c61..2605d62 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -43,6 +43,7 @@ import com.google.firebase.auth.UserRecord;
 import com.google.firebase.auth.internal.DownloadAccountResponse;
 import com.google.firebase.auth.internal.GetAccountInfoResponse;
 import com.google.firebase.auth.internal.HttpErrorResponse;
+import com.google.firebase.auth.internal.ListOidcProviderConfigsResponse;
 import com.google.firebase.auth.internal.ListTenantsResponse;
 import com.google.firebase.auth.internal.UploadAccountResponse;
 import com.google.firebase.internal.ApiClientUtils;
@@ -94,6 +95,7 @@ class FirebaseUserManager {
       .put("INVALID_DYNAMIC_LINK_DOMAIN", "invalid-dynamic-link-domain")
       .build();
 
+  static final int MAX_LIST_PROVIDER_CONFIGS_RESULTS = 100;
   static final int MAX_LIST_TENANTS_RESULTS = 1000;
   static final int MAX_LIST_USERS_RESULTS = 1000;
   static final int MAX_IMPORT_USERS = 1000;
@@ -338,6 +340,26 @@ class FirebaseUserManager {
     return sendRequest("GET", url, null, OidcProviderConfig.class);
   }
 
+  ListOidcProviderConfigsResponse listOidcProviderConfigs(int maxResults, String pageToken)
+      throws FirebaseAuthException {
+    ImmutableMap.Builder<String, Object> builder =
+        ImmutableMap.<String, Object>builder().put("pageSize", maxResults);
+    if (pageToken != null) {
+      checkArgument(!pageToken.equals(
+          ListTenantsPage.END_OF_LIST), "invalid end of list page token");
+      builder.put("nextPageToken", pageToken);
+    }
+
+    GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs");
+    url.putAll(builder.build());
+    ListOidcProviderConfigsResponse response =
+        sendRequest("GET", url, null, ListOidcProviderConfigsResponse.class);
+    if (response == null) {
+      throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve provider configs.");
+    }
+    return response;
+  }
+
   void deleteProviderConfig(String providerId) throws FirebaseAuthException {
     GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId));
     sendRequest("DELETE", url, null, GenericJson.class);
diff --git a/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java b/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java
new file mode 100644
index 0000000..2ab7f73
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.api.client.json.JsonFactory;
+import com.google.api.gax.paging.Page;
+import com.google.common.collect.ImmutableList;
+import com.google.firebase.auth.internal.DownloadAccountResponse;
+import com.google.firebase.auth.internal.ListOidcProviderConfigsResponse;
+import com.google.firebase.auth.internal.ListProviderConfigsResponse;
+import com.google.firebase.internal.NonNull;
+import com.google.firebase.internal.Nullable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+/**
+ * Represents a page of {@link ProviderConfig} instances.
+ *
+ * <p>Provides methods for iterating over the provider configs in the current page, and calling up
+ * subsequent pages of provider configs.
+ *
+ * <p>Instances of this class are thread-safe and immutable.
+ */
+public class ListProviderConfigsPage<T extends ProviderConfig> implements Page<T> {
+
+  static final String END_OF_LIST = "";
+
+  private final ListProviderConfigsResponse<T> currentBatch;
+  private final ProviderConfigSource<T> source;
+  private final int maxResults;
+
+  private ListProviderConfigsPage(
+      @NonNull ListProviderConfigsResponse<T> currentBatch,
+      @NonNull ProviderConfigSource<T> source,
+      int maxResults) {
+    this.currentBatch = checkNotNull(currentBatch);
+    this.source = checkNotNull(source);
+    this.maxResults = maxResults;
+  }
+
+  /**
+   * Checks if there is another page of provider configs available to retrieve.
+   *
+   * @return true if another page is available, or false otherwise.
+   */
+  @Override
+  public boolean hasNextPage() {
+    return !END_OF_LIST.equals(currentBatch.getPageToken());
+  }
+
+  /**
+   * Returns the string token that identifies the next page.
+   *
+   * <p>Never returns null. Returns empty string if there are no more pages available to be
+   * retrieved.
+   *
+   * @return A non-null string token (possibly empty, representing no more pages)
+   */
+  @NonNull
+  @Override
+  public String getNextPageToken() {
+    return currentBatch.getPageToken();
+  }
+
+  /**
+   * Returns the next page of provider configs.
+   *
+   * @return A new {@link ListProviderConfigsPage} instance, or null if there are no more pages.
+   */
+  @Nullable
+  @Override
+  public ListProviderConfigsPage<T> getNextPage() {
+    if (hasNextPage()) {
+      Factory<T> factory = new Factory<T>(source, maxResults, currentBatch.getPageToken());
+      try {
+        return factory.create();
+      } catch (FirebaseAuthException e) {
+        throw new RuntimeException(e);
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Returns an {@link Iterable} that facilitates transparently iterating over all the provider
+   * configs in the current Firebase project, starting from this page.
+   *
+   * <p>The {@link Iterator} instances produced by the returned {@link Iterable} never buffers more
+   * than one page of provider configs at a time. It is safe to abandon the iterators (i.e. break
+   * the loops) at any time.
+   *
+   * @return a new {@link Iterable} instance.
+   */
+  @NonNull
+  @Override
+  public Iterable<T> iterateAll() {
+    return new ProviderConfigIterable(this);
+  }
+
+  /**
+   * Returns an {@link Iterable} over the provider configs in this page.
+   *
+   * @return a {@link Iterable} instance.
+   */
+  @NonNull
+  @Override
+  public Iterable<T> getValues() {
+    return currentBatch.getProviderConfigs();
+  }
+
+  private static class ProviderConfigIterable<T extends ProviderConfig> implements Iterable<T> {
+
+    private final ListProviderConfigsPage<T> startingPage;
+
+    ProviderConfigIterable(@NonNull ListProviderConfigsPage<T> startingPage) {
+      this.startingPage = checkNotNull(startingPage, "starting page must not be null");
+    }
+
+    @Override
+    @NonNull
+    public Iterator<T> iterator() {
+      return new ProviderConfigIterator<T>(startingPage);
+    }
+
+    /**
+     * An {@link Iterator} that cycles through provider configs, one at a time.
+     *
+     * <p>It buffers the last retrieved batch of provider configs in memory. The {@code maxResults}
+     * parameter is an upper bound on the batch size.
+     */
+    private static class ProviderConfigIterator<T extends ProviderConfig> implements Iterator<T> {
+
+      private ListProviderConfigsPage<T> currentPage;
+      private List<T> batch;
+      private int index = 0;
+
+      private ProviderConfigIterator(ListProviderConfigsPage<T> startingPage) {
+        setCurrentPage(startingPage);
+      }
+
+      @Override
+      public boolean hasNext() {
+        if (index == batch.size()) {
+          if (currentPage.hasNextPage()) {
+            setCurrentPage(currentPage.getNextPage());
+          } else {
+            return false;
+          }
+        }
+
+        return index < batch.size();
+      }
+
+      @Override
+      public T next() {
+        if (!hasNext()) {
+          throw new NoSuchElementException();
+        }
+        return batch.get(index++);
+      }
+
+      @Override
+      public void remove() {
+        throw new UnsupportedOperationException("remove operation not supported");
+      }
+
+      private void setCurrentPage(ListProviderConfigsPage<T> page) {
+        this.currentPage = checkNotNull(page);
+        this.batch = ImmutableList.copyOf(page.getValues());
+        this.index = 0;
+      }
+    }
+  }
+
+  /**
+   * Represents a source of provider config data that can be queried to load a batch of provider
+   * configs.
+   */
+  interface ProviderConfigSource<T extends ProviderConfig> {
+    @NonNull
+    ListProviderConfigsResponse<T> fetch(int maxResults, String pageToken)
+      throws FirebaseAuthException;
+  }
+
+  static class DefaultOidcProviderConfigSource implements ProviderConfigSource<OidcProviderConfig> {
+
+    private final FirebaseUserManager userManager;
+
+    DefaultOidcProviderConfigSource(FirebaseUserManager userManager) {
+      this.userManager = checkNotNull(userManager, "user manager must not be null");
+    }
+
+    @Override
+    public ListOidcProviderConfigsResponse fetch(int maxResults, String pageToken)
+        throws FirebaseAuthException {
+      return userManager.listOidcProviderConfigs(maxResults, pageToken);
+    }
+  }
+
+  // TODO(micahstairs): Add DefaultSamlProviderConfigSource class.
+
+  /**
+   * A simple factory class for {@link ProviderConfigsPage} instances.
+   *
+   * <p>Performs argument validation before attempting to load any provider config data (which is
+   * expensive, and hence may be performed asynchronously on a separate thread).
+   */
+  static class Factory<T extends ProviderConfig> {
+
+    private final ProviderConfigSource<T> source;
+    private final int maxResults;
+    private final String pageToken;
+
+    Factory(@NonNull ProviderConfigSource<T> source) {
+      this(source, FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS, null);
+    }
+
+    Factory(
+        @NonNull ProviderConfigSource source,
+        int maxResults,
+        @Nullable String pageToken) {
+      checkArgument(
+          maxResults > 0 && maxResults <= FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS,
+          "maxResults must be a positive integer that does not exceed %s",
+          FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS);
+      checkArgument(!END_OF_LIST.equals(pageToken), "invalid end of list page token");
+      this.source = checkNotNull(source, "source must not be null");
+      this.maxResults = maxResults;
+      this.pageToken = pageToken;
+    }
+
+    ListProviderConfigsPage<T> create() throws FirebaseAuthException {
+      ListProviderConfigsResponse<T> batch = source.fetch(maxResults, pageToken);
+      return new ListProviderConfigsPage<T>(batch, source, maxResults);
+    }
+  }
+}
+
diff --git a/src/main/java/com/google/firebase/auth/ListTenantsPage.java b/src/main/java/com/google/firebase/auth/ListTenantsPage.java
index 5b07c16..851d545 100644
--- a/src/main/java/com/google/firebase/auth/ListTenantsPage.java
+++ b/src/main/java/com/google/firebase/auth/ListTenantsPage.java
@@ -113,7 +113,7 @@ public class ListTenantsPage implements Page<Tenant> {
   }
 
   /**
-   * Returns an {@link Iterable} over the users in this page.
+   * Returns an {@link Iterable} over the tenants in this page.
    *
    * @return a {@link Iterable} instance.
    */
diff --git a/src/main/java/com/google/firebase/auth/ListUsersPage.java b/src/main/java/com/google/firebase/auth/ListUsersPage.java
index f406366..ba727af 100644
--- a/src/main/java/com/google/firebase/auth/ListUsersPage.java
+++ b/src/main/java/com/google/firebase/auth/ListUsersPage.java
@@ -80,7 +80,7 @@ public class ListUsersPage implements Page<ExportedUserRecord> {
   @Override
   public ListUsersPage getNextPage() {
     if (hasNextPage()) {
-      PageFactory factory = new PageFactory(source, maxResults, currentBatch.getNextPageToken());
+      Factory factory = new Factory(source, maxResults, currentBatch.getNextPageToken());
       try {
         return factory.create();
       } catch (FirebaseAuthException e) {
@@ -237,17 +237,17 @@ public class ListUsersPage implements Page<ExportedUserRecord> {
    * before attempting to load any user data (which is expensive, and hence may be performed
    * asynchronously on a separate thread).
    */
-  static class PageFactory {
+  static class Factory {
 
     private final UserSource source;
     private final int maxResults;
     private final String pageToken;
 
-    PageFactory(@NonNull UserSource source) {
+    Factory(@NonNull UserSource source) {
       this(source, FirebaseUserManager.MAX_LIST_USERS_RESULTS, null);
     }
 
-    PageFactory(@NonNull UserSource source, int maxResults, @Nullable String pageToken) {
+    Factory(@NonNull UserSource source, int maxResults, @Nullable String pageToken) {
       checkArgument(maxResults > 0 && maxResults <= FirebaseUserManager.MAX_LIST_USERS_RESULTS,
           "maxResults must be a positive integer that does not exceed %s",
           FirebaseUserManager.MAX_LIST_USERS_RESULTS);
diff --git a/src/main/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponse.java
new file mode 100644
index 0000000..dc1df5c
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponse.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth.internal;
+
+import com.google.api.client.util.Key;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.firebase.auth.OidcProviderConfig;
+import com.google.firebase.auth.Tenant;
+import java.util.List;
+
+/**
+ * JSON data binding for ListOAuthIdpConfigsResponse messages sent by Google identity toolkit
+ * service.
+ */
+public final class ListOidcProviderConfigsResponse
+    implements ListProviderConfigsResponse<OidcProviderConfig> {
+
+  @Key("oauthIdpConfigs")
+  private List<OidcProviderConfig> providerConfigs;
+
+  @Key("nextPageToken")
+  private String pageToken;
+
+  @VisibleForTesting
+  public ListOidcProviderConfigsResponse(
+      List<OidcProviderConfig> providerConfigs,
+      String pageToken) {
+    this.providerConfigs = providerConfigs;
+    this.pageToken = pageToken;
+  }
+
+  public ListOidcProviderConfigsResponse() { }
+
+  @Override
+  public List<OidcProviderConfig> getProviderConfigs() {
+    return providerConfigs == null ? ImmutableList.<OidcProviderConfig>of() : providerConfigs;
+  }
+
+  @Override
+  public boolean hasProviderConfigs() {
+    return providerConfigs != null && !providerConfigs.isEmpty();
+  }
+
+  @Override
+  public String getPageToken() {
+    return pageToken == null ? "" : pageToken;
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java
new file mode 100644
index 0000000..c81205a
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth.internal;
+
+import com.google.api.client.util.Key;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.firebase.auth.ListProviderConfigsPage;
+import com.google.firebase.auth.ProviderConfig;
+import com.google.firebase.auth.Tenant;
+import java.util.List;
+
+/**
+ * Interface for config list response messages sent by Google identity toolkit service.
+ */
+public interface ListProviderConfigsResponse<T extends ProviderConfig> {
+
+  public List<T> getProviderConfigs();
+
+  public boolean hasProviderConfigs();
+
+  public String getPageToken();
+}
diff --git a/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java
index 4977061..b612793 100644
--- a/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java
+++ b/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java
@@ -19,7 +19,6 @@ package com.google.firebase.auth.internal;
 import com.google.api.client.util.Key;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
-import com.google.firebase.auth.ListTenantsPage;
 import com.google.firebase.auth.Tenant;
 import java.util.List;
 


Test patch:
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
index b7a2d6d..290499d 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
@@ -66,6 +66,8 @@ import java.util.concurrent.atomic.AtomicReference;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+// TODO(micahstairs): Move tenant-aware tests into a seperate class, so that we only need to
+// create and destroy the tenant once.
 public class FirebaseAuthIT {
 
   private static final String VERIFY_CUSTOM_TOKEN_URL =
@@ -1066,6 +1068,129 @@ public class FirebaseAuthIT {
     }
   }
 
+  @Test
+  public void testListOidcProviderConfigs() throws Exception {
+    final List<String> providerIds = new ArrayList<>();
+
+    try {
+      // Create provider configs
+      for (int i = 0; i < 3; i++) {
+        String providerId = "oidc.provider-id" + i;
+        providerIds.add(providerId);
+        OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest()
+            .setProviderId(providerId)
+            .setClientId("CLIENT_ID")
+            .setIssuer("https://oidc.com/issuer");
+        auth.createOidcProviderConfig(createRequest);
+      }
+
+      // Test list by batches
+      final AtomicInteger collected = new AtomicInteger(0);
+      ListProviderConfigsPage<OidcProviderConfig> page =
+          auth.listOidcProviderConfigsAsync(null).get();
+      while (page != null) {
+        for (OidcProviderConfig providerConfig : page.getValues()) {
+          if (checkProviderConfig(providerIds, providerConfig)) {
+            collected.incrementAndGet();
+          }
+        }
+        page = page.getNextPage();
+      }
+      assertEquals(providerIds.size(), collected.get());
+
+      // Test iterate all
+      collected.set(0);
+      page = auth.listOidcProviderConfigsAsync(null).get();
+      for (OidcProviderConfig providerConfig : page.iterateAll()) {
+        if (checkProviderConfig(providerIds, providerConfig)) {
+          collected.incrementAndGet();
+        }
+      }
+      assertEquals(providerIds.size(), collected.get());
+
+      // Test iterate async
+      collected.set(0);
+      final Semaphore semaphore = new Semaphore(0);
+      final AtomicReference<Throwable> error = new AtomicReference<>();
+      ApiFuture<ListProviderConfigsPage<OidcProviderConfig>> pageFuture =
+          auth.listOidcProviderConfigsAsync(null);
+      ApiFutures.addCallback(
+          pageFuture,
+          new ApiFutureCallback<ListProviderConfigsPage<OidcProviderConfig>>() {
+            @Override
+            public void onFailure(Throwable t) {
+              error.set(t);
+              semaphore.release();
+            }
+
+            @Override
+            public void onSuccess(ListProviderConfigsPage<OidcProviderConfig> result) {
+              for (OidcProviderConfig providerConfig : result.iterateAll()) {
+                if (checkProviderConfig(providerIds, providerConfig)) {
+                  collected.incrementAndGet();
+                }
+              }
+              semaphore.release();
+            }
+          }, MoreExecutors.directExecutor());
+      semaphore.acquire();
+      assertEquals(providerIds.size(), collected.get());
+      assertNull(error.get());
+    } finally {
+      // Delete provider configs
+      for (String providerId : providerIds) {
+        auth.deleteProviderConfigAsync(providerId).get();
+      }
+    }
+  }
+
+  @Test
+  public void testTenantAwareListOidcProviderConfigs() throws Exception {
+    // Create tenant to use
+    TenantManager tenantManager = auth.getTenantManager();
+    Tenant.CreateRequest tenantCreateRequest =
+        new Tenant.CreateRequest().setDisplayName("DisplayName");
+    String tenantId = tenantManager.createTenant(tenantCreateRequest).getTenantId();
+    TenantAwareFirebaseAuth tenantAwareAuth = auth.getTenantManager().getAuthForTenant(tenantId);
+
+    try {
+      final List<String> providerIds = new ArrayList<>();
+      try {
+
+        // Create provider configs
+        for (int i = 0; i < 3; i++) {
+          String providerId = "oidc.provider-id" + i;
+          providerIds.add(providerId);
+          OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest()
+              .setProviderId(providerId)
+              .setClientId("CLIENT_ID")
+              .setIssuer("https://oidc.com/issuer");
+          tenantAwareAuth.createOidcProviderConfig(createRequest);
+        }
+
+        // List provider configs
+        final AtomicInteger collected = new AtomicInteger(0);
+        ListProviderConfigsPage<OidcProviderConfig> page =
+            tenantAwareAuth.listOidcProviderConfigsAsync(null).get();
+        for (OidcProviderConfig providerConfig : page.iterateAll()) {
+          if (checkProviderConfig(providerIds, providerConfig)) {
+            collected.incrementAndGet();
+          }
+        }
+        assertEquals(providerIds.size(), collected.get());
+
+      } finally {
+        // Delete provider configs
+        for (String providerId : providerIds) {
+          tenantAwareAuth.deleteProviderConfigAsync(providerId).get();
+        }
+      }
+    } finally {
+      // Delete tenant
+      tenantManager.deleteTenantAsync(tenantId).get();
+    }
+  }
+
   private Map<String, String> parseLinkParameters(String link) throws Exception {
     Map<String, String> result = new HashMap<>();
     int queryBegin = link.indexOf('?');
@@ -1196,6 +1321,15 @@ public class FirebaseAuthIT {
     }
   }
 
+  private boolean checkProviderConfig(List<String> providerIds, OidcProviderConfig config) {
+    if (providerIds.contains(config.getProviderId())) {
+      assertEquals("CLIENT_ID", config.getClientId());
+      assertEquals("https://oidc.com/issuer", config.getIssuer());
+      return true;
+    }
+    return false;
+  }
+
 
   private static void assertOidcProviderConfigDoesNotExist(
       AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception {
diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
index ed6ea15..31738df 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
@@ -1400,7 +1400,7 @@ public class FirebaseUserManagerTest {
 
     OidcProviderConfig config = FirebaseAuth.getInstance().createOidcProviderConfig(createRequest);
 
-    checkOidcProviderConfig(config);
+    checkOidcProviderConfig(config, "oidc.provider-id");
     checkRequestHeaders(interceptor);
     checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs");
     GenericJson parsed = parseRequestContent(interceptor);
@@ -1416,11 +1416,11 @@ public class FirebaseUserManagerTest {
   public void testCreateOidcProviderMinimal() throws Exception {
     TestResponseInterceptor interceptor = initializeAppForUserManagement(
         TestUtils.loadResource("oidc.json"));
-    // Only the 'enabled' field can be omitted from an OIDC provider config creation request.
+    // Only the 'enabled' and 'displayName' fields can be omitted from an OIDC provider config
+    // creation request.
     OidcProviderConfig.CreateRequest createRequest =
         new OidcProviderConfig.CreateRequest()
             .setProviderId("oidc.provider-id")
-            .setDisplayName("DISPLAY_NAME")
             .setClientId("CLIENT_ID")
             .setIssuer("https://oidc.com/issuer");
 
@@ -1428,7 +1428,7 @@ public class FirebaseUserManagerTest {
     checkRequestHeaders(interceptor);
     checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs");
     GenericJson parsed = parseRequestContent(interceptor);
-    assertEquals("DISPLAY_NAME", parsed.get("displayName"));
+    assertNull(parsed.get("displayName"));
     assertNull(parsed.get("enabled"));
     assertEquals("CLIENT_ID", parsed.get("clientId"));
     assertEquals("https://oidc.com/issuer", parsed.get("issuer"));
@@ -1504,7 +1504,7 @@ public class FirebaseUserManagerTest {
 
     OidcProviderConfig config = FirebaseAuth.getInstance().updateOidcProviderConfig(request);
 
-    checkOidcProviderConfig(config);
+    checkOidcProviderConfig(config, "oidc.provider-id");
     checkRequestHeaders(interceptor);
     checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id");
     GenericUrl url = interceptor.getResponse().getRequest().getUrl();
@@ -1525,7 +1525,7 @@ public class FirebaseUserManagerTest {
 
     OidcProviderConfig config = FirebaseAuth.getInstance().updateOidcProviderConfig(request);
 
-    checkOidcProviderConfig(config);
+    checkOidcProviderConfig(config, "oidc.provider-id");
     checkRequestHeaders(interceptor);
     checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id");
     GenericUrl url = interceptor.getResponse().getRequest().getUrl();
@@ -1580,7 +1580,7 @@ public class FirebaseUserManagerTest {
 
     OidcProviderConfig config = tenantAwareAuth.updateOidcProviderConfig(request);
 
-    checkOidcProviderConfig(config);
+    checkOidcProviderConfig(config, "oidc.provider-id");
     checkRequestHeaders(interceptor);
     String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/oidc.provider-id";
     checkUrl(interceptor, "PATCH", expectedUrl);
@@ -1601,7 +1601,7 @@ public class FirebaseUserManagerTest {
     OidcProviderConfig config =
         FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id");
 
-    checkOidcProviderConfig(config);
+    checkOidcProviderConfig(config, "oidc.provider-id");
     checkRequestHeaders(interceptor);
     checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id");
   }
@@ -1630,11 +1630,80 @@ public class FirebaseUserManagerTest {
 
     OidcProviderConfig config = tenantAwareAuth.getOidcProviderConfig("oidc.provider-id");
 
-    checkOidcProviderConfig(config);
+    checkOidcProviderConfig(config, "oidc.provider-id");
     checkRequestHeaders(interceptor);
     checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/oidc.provider-id");
   }
 
+  @Test
+  public void testListOidcProviderConfigs() throws Exception {
+    final TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("listOidc.json"));
+    ListProviderConfigsPage<OidcProviderConfig> page =
+        FirebaseAuth.getInstance().listOidcProviderConfigsAsync(null, 99).get();
+
+    ImmutableList<OidcProviderConfig> providerConfigs = ImmutableList.copyOf(page.getValues());
+    assertEquals(2, providerConfigs.size());
+    checkOidcProviderConfig(providerConfigs.get(0), "oidc.provider-id1");
+    checkOidcProviderConfig(providerConfigs.get(1), "oidc.provider-id2");
+    assertEquals("", page.getNextPageToken());
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs");
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals(99, url.getFirst("pageSize"));
+    assertNull(url.getFirst("nextPageToken"));
+  }
+
+  @Test
+  public void testListOidcProviderConfigsWithPageToken() throws Exception {
+    final TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("listOidc.json"));
+    ListProviderConfigsPage<OidcProviderConfig> page =
+        FirebaseAuth.getInstance().listOidcProviderConfigsAsync("token", 99).get();
+
+    ImmutableList<OidcProviderConfig> providerConfigs = ImmutableList.copyOf(page.getValues());
+    assertEquals(2, providerConfigs.size());
+    checkOidcProviderConfig(providerConfigs.get(0), "oidc.provider-id1");
+    checkOidcProviderConfig(providerConfigs.get(1), "oidc.provider-id2");
+    assertEquals("", page.getNextPageToken());
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs");
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals(99, url.getFirst("pageSize"));
+    assertEquals("token", url.getFirst("nextPageToken"));
+  }
+
+  @Test
+  public void testListZeroOidcProviderConfigs() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
+    ListProviderConfigsPage<OidcProviderConfig> page =
+        FirebaseAuth.getInstance().listOidcProviderConfigsAsync(null).get();
+    assertTrue(Iterables.isEmpty(page.getValues()));
+    assertEquals("", page.getNextPageToken());
+    checkRequestHeaders(interceptor);
+  }
+
+  @Test
+  public void testTenantAwareListOidcProviderConfigs() throws Exception {
+    final TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement(
+        "TENANT_ID",
+        TestUtils.loadResource("listOidc.json"));
+    TenantAwareFirebaseAuth tenantAwareAuth =
+        FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID");
+    ListProviderConfigsPage<OidcProviderConfig> page =
+        tenantAwareAuth.listOidcProviderConfigsAsync(null, 99).get();
+
+    ImmutableList<OidcProviderConfig> providerConfigs = ImmutableList.copyOf(page.getValues());
+    assertEquals(2, providerConfigs.size());
+    checkOidcProviderConfig(providerConfigs.get(0), "oidc.provider-id1");
+    checkOidcProviderConfig(providerConfigs.get(1), "oidc.provider-id2");
+    assertEquals("", page.getNextPageToken());
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs");
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals(99, url.getFirst("pageSize"));
+    assertNull(url.getFirst("nextPageToken"));
+  }
 
   @Test
   public void testDeleteProviderConfig() throws Exception {
@@ -1790,8 +1859,8 @@ public class FirebaseUserManagerTest {
     assertFalse(tenant.isEmailLinkSignInEnabled());
   }
 
-  private static void checkOidcProviderConfig(OidcProviderConfig config) {
-    assertEquals("oidc.provider-id", config.getProviderId());
+  private static void checkOidcProviderConfig(OidcProviderConfig config, String providerId) {
+    assertEquals(providerId, config.getProviderId());
     assertEquals("DISPLAY_NAME", config.getDisplayName());
     assertTrue(config.isEnabled());
     assertEquals("CLIENT_ID", config.getClientId());
diff --git a/src/test/java/com/google/firebase/auth/ListUsersPageTest.java b/src/test/java/com/google/firebase/auth/ListUsersPageTest.java
index fb4a7d2..5e84806 100644
--- a/src/test/java/com/google/firebase/auth/ListUsersPageTest.java
+++ b/src/test/java/com/google/firebase/auth/ListUsersPageTest.java
@@ -27,6 +27,7 @@ import com.google.api.client.googleapis.util.Utils;
 import com.google.api.client.json.JsonFactory;
 import com.google.common.collect.ImmutableList;
 import com.google.common.io.BaseEncoding;
+import com.google.firebase.auth.ListUsersPage;
 import com.google.firebase.auth.ListUsersPage.ListUsersResult;
 import com.google.firebase.auth.internal.DownloadAccountResponse;
 import java.io.IOException;
@@ -45,7 +46,7 @@ public class ListUsersPageTest {
   @Test
   public void testSinglePage() throws FirebaseAuthException, IOException {
     TestUserSource source = new TestUserSource(3);
-    ListUsersPage page = new ListUsersPage.PageFactory(source).create();
+    ListUsersPage page = new ListUsersPage.Factory(source).create();
     assertFalse(page.hasNextPage());
     assertEquals(ListUsersPage.END_OF_LIST, page.getNextPageToken());
     assertNull(page.getNextPage());
@@ -68,7 +69,7 @@ public class ListUsersPageTest {
             newUser("user2", REDACTED_BASE64)),
         ListUsersPage.END_OF_LIST);
     TestUserSource source = new TestUserSource(result);
-    ListUsersPage page = new ListUsersPage.PageFactory(source).create();
+    ListUsersPage page = new ListUsersPage.Factory(source).create();
     assertFalse(page.hasNextPage());
     assertEquals(ListUsersPage.END_OF_LIST, page.getNextPageToken());
     assertNull(page.getNextPage());
@@ -89,7 +90,7 @@ public class ListUsersPageTest {
         ImmutableList.of(newUser("user0"), newUser("user1"), newUser("user2")),
         "token");
     TestUserSource source = new TestUserSource(result);
-    ListUsersPage page1 = new ListUsersPage.PageFactory(source).create();
+    ListUsersPage page1 = new ListUsersPage.Factory(source).create();
     assertTrue(page1.hasNextPage());
     assertEquals("token", page1.getNextPageToken());
     ImmutableList<ExportedUserRecord> users = ImmutableList.copyOf(page1.getValues());
@@ -136,7 +137,7 @@ public class ListUsersPageTest {
   @Test
   public void testListUsersIterable() throws FirebaseAuthException, IOException {
     TestUserSource source = new TestUserSource(3);
-    ListUsersPage page = new ListUsersPage.PageFactory(source).create();
+    ListUsersPage page = new ListUsersPage.Factory(source).create();
     Iterable<ExportedUserRecord> users = page.iterateAll();
 
     int iterations = 0;
@@ -162,7 +163,7 @@ public class ListUsersPageTest {
   @Test
   public void testListUsersIterator() throws FirebaseAuthException, IOException {
     TestUserSource source = new TestUserSource(3);
-    ListUsersPage page = new ListUsersPage.PageFactory(source).create();
+    ListUsersPage page = new ListUsersPage.Factory(source).create();
     Iterable<ExportedUserRecord> users = page.iterateAll();
     Iterator<ExportedUserRecord> iterator = users.iterator();
     int iterations = 0;
@@ -192,7 +193,7 @@ public class ListUsersPageTest {
         ImmutableList.of(newUser("user0"), newUser("user1"), newUser("user2")),
         "token");
     TestUserSource source = new TestUserSource(result);
-    ListUsersPage page = new ListUsersPage.PageFactory(source).create();
+    ListUsersPage page = new ListUsersPage.Factory(source).create();
     int iterations = 0;
     for (ExportedUserRecord user : page.iterateAll()) {
       assertEquals("user" + iterations, user.getUid());
@@ -218,7 +219,7 @@ public class ListUsersPageTest {
         ImmutableList.of(newUser("user0"), newUser("user1"), newUser("user2")),
         "token");
     TestUserSource source = new TestUserSource(result);
-    ListUsersPage page = new ListUsersPage.PageFactory(source).create();
+    ListUsersPage page = new ListUsersPage.Factory(source).create();
     Iterator<ExportedUserRecord> users = page.iterateAll().iterator();
     int iterations = 0;
     while (users.hasNext()) {
@@ -251,7 +252,7 @@ public class ListUsersPageTest {
         ImmutableList.<ExportedUserRecord>of(),
         ListUsersPage.END_OF_LIST);
     TestUserSource source = new TestUserSource(result);
-    ListUsersPage page = new ListUsersPage.PageFactory(source).create();
+    ListUsersPage page = new ListUsersPage.Factory(source).create();
     assertFalse(page.hasNextPage());
     assertEquals(ListUsersPage.END_OF_LIST, page.getNextPageToken());
     assertNull(page.getNextPage());
@@ -265,7 +266,7 @@ public class ListUsersPageTest {
         ImmutableList.<ExportedUserRecord>of(),
         ListUsersPage.END_OF_LIST);
     TestUserSource source = new TestUserSource(result);
-    ListUsersPage page = new ListUsersPage.PageFactory(source).create();
+    ListUsersPage page = new ListUsersPage.Factory(source).create();
     for (ExportedUserRecord user : page.iterateAll()) {
       fail("Should not be able to iterate, but got: " + user);
     }
@@ -279,7 +280,7 @@ public class ListUsersPageTest {
         ListUsersPage.END_OF_LIST);
     TestUserSource source = new TestUserSource(result);
 
-    ListUsersPage page = new ListUsersPage.PageFactory(source).create();
+    ListUsersPage page = new ListUsersPage.Factory(source).create();
     Iterator<ExportedUserRecord> iterator = page.iterateAll().iterator();
     while (iterator.hasNext()) {
       fail("Should not be able to iterate");
@@ -294,7 +295,7 @@ public class ListUsersPageTest {
         ListUsersPage.END_OF_LIST);
     TestUserSource source = new TestUserSource(result);
 
-    ListUsersPage page = new ListUsersPage.PageFactory(source).create();
+    ListUsersPage page = new ListUsersPage.Factory(source).create();
     Iterator<ExportedUserRecord> iterator = page.iterateAll().iterator();
     while (iterator.hasNext()) {
       assertNotNull(iterator.next());
@@ -308,14 +309,14 @@ public class ListUsersPageTest {
 
   @Test(expected = NullPointerException.class)
   public void testNullSource() {
-    new ListUsersPage.PageFactory(null);
+    new ListUsersPage.Factory(null);
   }
 
   @Test
   public void testInvalidPageToken() throws IOException {
     TestUserSource source = new TestUserSource(1);
     try {
-      new ListUsersPage.PageFactory(source, 1000, "");
+      new ListUsersPage.Factory(source, 1000, "");
       fail("No error thrown for empty page token");
     } catch (IllegalArgumentException expected) {
       // expected
@@ -326,21 +327,21 @@ public class ListUsersPageTest {
   public void testInvalidMaxResults() throws IOException {
     TestUserSource source = new TestUserSource(1);
     try {
-      new ListUsersPage.PageFactory(source, 1001, "");
+      new ListUsersPage.Factory(source, 1001, "");
       fail("No error thrown for maxResult > 1000");
     } catch (IllegalArgumentException expected) {
       // expected
     }
 
     try {
-      new ListUsersPage.PageFactory(source, 0, "next");
+      new ListUsersPage.Factory(source, 0, "next");
       fail("No error thrown for maxResult = 0");
     } catch (IllegalArgumentException expected) {
       // expected
     }
 
     try {
-      new ListUsersPage.PageFactory(source, -1, "next");
+      new ListUsersPage.Factory(source, -1, "next");
       fail("No error thrown for maxResult < 0");
     } catch (IllegalArgumentException expected) {
       // expected
diff --git a/src/test/resources/listOidc.json b/src/test/resources/listOidc.json
new file mode 100644
index 0000000..0c13ea4
--- /dev/null
+++ b/src/test/resources/listOidc.json
@@ -0,0 +1,15 @@
+{
+  "oauthIdpConfigs" : [ {
+    "name": "projects/projectId/oauthIdpConfigs/oidc.provider-id1",
+    "displayName" : "DISPLAY_NAME",
+    "enabled" : true,
+    "clientId" : "CLIENT_ID",
+    "issuer" : "https://oidc.com/issuer"
+  }, {
+    "name": "projects/projectId/oauthIdpConfigs/oidc.provider-id2",
+    "displayName" : "DISPLAY_NAME",
+    "enabled" : true,
+    "clientId" : "CLIENT_ID",
+    "issuer" : "https://oidc.com/issuer"
+  } ]
+}