Repository: firebase/firebase-admin-java
Pull request #433 (2020-06-04 20:19:10)

Add provider config management operations.
This adds all of the OIDC and SAML provider config operations, as part of adding multi-tenancy support.

hiranya911: @kevinthecheung can you please review the API docs in following classes:

* AbstractFirebaseAuth
* ProviderConfig
* SamlProviderConfig
* OidcProviderConfig
* ListProviderConfigsPage

micahstairs: Alright, thanks for the review! I've addressed your feedback, and I've also rebased this branch so that we don't have any more merge conflicts with `tenant-mgt`.

Patch:
diff --git a/checkstyle.xml b/checkstyle.xml
index 6639181..77e2dba 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -42,15 +42,15 @@
     <module name="FileTabCharacter">
         <property name="eachLine" value="true"/>
     </module>
-    
+
     <module name="SuppressionCommentFilter">
       <property name="offCommentFormat" value="CSOFF\: ([\w\|]+)"/>
       <property name="onCommentFormat" value="CSON\: ([\w\|]+)"/>
       <property name="checkFormat" value="$1"/>
     </module>
 
-    <module name="TreeWalker">   
-        <module name="FileContentsHolder"/>     
+    <module name="TreeWalker">
+        <module name="FileContentsHolder"/>
         <module name="OuterTypeFilename"/>
         <module name="IllegalTokenText">
             <property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
@@ -229,6 +229,9 @@
             <property name="allowedAnnotations" value="Override, Test"/>
             <property name="allowThrowsTagsForSubclasses" value="true"/>
             <property name="allowMissingJavadoc" value="true"/>
+            <!-- Setting this property helps avoid some strange errors. For more information, see -->
+            <!-- https://stackoverflow.com/questions/27938039/unable-to-get-class-information-for-checkstyle. -->
+            <property name="suppressLoadErrors" value="true"/>
         </module>
         <module name="MethodName">
             <property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>
diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
index f779ae9..a864c4f 100644
--- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
@@ -29,10 +29,12 @@ 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.ListProviderConfigsPage.DefaultSamlProviderConfigSource;
+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.CreateRequest;
-import com.google.firebase.auth.UserRecord.UpdateRequest;
+import com.google.firebase.auth.UserRecord;
 import com.google.firebase.auth.internal.FirebaseTokenFactory;
 import com.google.firebase.internal.CallableOperation;
 import com.google.firebase.internal.NonNull;
@@ -323,7 +325,8 @@ public abstract class AbstractFirebaseAuth {
       @Override
       protected Void execute() throws FirebaseAuthException {
         int currentTimeSeconds = (int) (System.currentTimeMillis() / 1000);
-        UpdateRequest request = new UpdateRequest(uid).setValidSince(currentTimeSeconds);
+        UserRecord.UpdateRequest request =
+            new UserRecord.UpdateRequest(uid).setValidSince(currentTimeSeconds);
         userManager.updateUser(request, jsonFactory);
         return null;
       }
@@ -444,8 +447,8 @@ public abstract class AbstractFirebaseAuth {
   }
 
   /**
-   * Gets a page of users starting from the specified {@code pageToken}. Page size will be limited
-   * to 1000 users.
+   * Gets a page of users starting from the specified {@code pageToken}. Page size is limited to
+   * 1000 users.
    *
    * @param pageToken A non-empty page token string, or null to retrieve the first page of users.
    * @return A {@link ListUsersPage} instance.
@@ -503,8 +506,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 {
@@ -515,32 +518,33 @@ public abstract class AbstractFirebaseAuth {
 
   /**
    * Creates a new user account with the attributes contained in the specified {@link
-   * CreateRequest}.
+   * UserRecord.CreateRequest}.
    *
-   * @param request A non-null {@link CreateRequest} instance.
+   * @param request A non-null {@link UserRecord.CreateRequest} instance.
    * @return A {@link UserRecord} instance corresponding to the newly created account.
    * @throws NullPointerException if the provided request is null.
    * @throws FirebaseAuthException if an error occurs while creating the user account.
    */
-  public UserRecord createUser(@NonNull CreateRequest request) throws FirebaseAuthException {
+  public UserRecord createUser(@NonNull UserRecord.CreateRequest request)
+      throws FirebaseAuthException {
     return createUserOp(request).call();
   }
 
   /**
-   * Similar to {@link #createUser(CreateRequest)} but performs the operation asynchronously.
+   * Similar to {@link #createUser} but performs the operation asynchronously.
    *
-   * @param request A non-null {@link CreateRequest} instance.
+   * @param request A non-null {@link UserRecord.CreateRequest} instance.
    * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord}
    *     instance corresponding to the newly created account. If an error occurs while creating the
    *     user account, the future throws a {@link FirebaseAuthException}.
    * @throws NullPointerException if the provided request is null.
    */
-  public ApiFuture<UserRecord> createUserAsync(@NonNull CreateRequest request) {
+  public ApiFuture<UserRecord> createUserAsync(@NonNull UserRecord.CreateRequest request) {
     return createUserOp(request).callAsync(firebaseApp);
   }
 
   private CallableOperation<UserRecord, FirebaseAuthException> createUserOp(
-      final CreateRequest request) {
+      final UserRecord.CreateRequest request) {
     checkNotDestroyed();
     checkNotNull(request, "create request must not be null");
     final FirebaseUserManager userManager = getUserManager();
@@ -555,31 +559,32 @@ public abstract class AbstractFirebaseAuth {
 
   /**
    * Updates an existing user account with the attributes contained in the specified {@link
-   * UpdateRequest}.
+   * UserRecord.UpdateRequest}.
    *
-   * @param request A non-null {@link UpdateRequest} instance.
+   * @param request A non-null {@link UserRecord.UpdateRequest} instance.
    * @return A {@link UserRecord} instance corresponding to the updated user account.
    * @throws NullPointerException if the provided update request is null.
    * @throws FirebaseAuthException if an error occurs while updating the user account.
    */
-  public UserRecord updateUser(@NonNull UpdateRequest request) throws FirebaseAuthException {
+  public UserRecord updateUser(@NonNull UserRecord.UpdateRequest request)
+      throws FirebaseAuthException {
     return updateUserOp(request).call();
   }
 
   /**
-   * Similar to {@link #updateUser(UpdateRequest)} but performs the operation asynchronously.
+   * Similar to {@link #updateUser} but performs the operation asynchronously.
    *
-   * @param request A non-null {@link UpdateRequest} instance.
+   * @param request A non-null {@link UserRecord.UpdateRequest} instance.
    * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord}
    *     instance corresponding to the updated user account. If an error occurs while updating the
    *     user account, the future throws a {@link FirebaseAuthException}.
    */
-  public ApiFuture<UserRecord> updateUserAsync(@NonNull UpdateRequest request) {
+  public ApiFuture<UserRecord> updateUserAsync(@NonNull UserRecord.UpdateRequest request) {
     return updateUserOp(request).callAsync(firebaseApp);
   }
 
   private CallableOperation<UserRecord, FirebaseAuthException> updateUserOp(
-      final UpdateRequest request) {
+      final UserRecord.UpdateRequest request) {
     checkNotDestroyed();
     checkNotNull(request, "update request must not be null");
     final FirebaseUserManager userManager = getUserManager();
@@ -639,7 +644,8 @@ public abstract class AbstractFirebaseAuth {
     return new CallableOperation<Void, FirebaseAuthException>() {
       @Override
       protected Void execute() throws FirebaseAuthException {
-        final UpdateRequest request = new UpdateRequest(uid).setCustomClaims(claims);
+        final UserRecord.UpdateRequest request =
+            new UserRecord.UpdateRequest(uid).setCustomClaims(claims);
         userManager.updateUser(request, jsonFactory);
         return null;
       }
@@ -1054,18 +1060,6 @@ public abstract class AbstractFirebaseAuth {
         .callAsync(firebaseApp);
   }
 
-  FirebaseApp getFirebaseApp() {
-    return this.firebaseApp;
-  }
-
-  FirebaseTokenVerifier getCookieVerifier() {
-    return this.cookieVerifier.get();
-  }
-
-  FirebaseUserManager getUserManager() {
-    return this.userManager.get();
-  }
-
   private CallableOperation<String, FirebaseAuthException> generateEmailActionLinkOp(
       final EmailLinkType type, final String email, final ActionCodeSettings settings) {
     checkNotDestroyed();
@@ -1082,6 +1076,544 @@ public abstract class AbstractFirebaseAuth {
     };
   }
 
+  /**
+   * Creates a new OpenID Connect auth provider config with the attributes contained in the
+   * specified {@link OidcProviderConfig.CreateRequest}.
+   *
+   * @param request A non-null {@link OidcProviderConfig.CreateRequest} instance.
+   * @return An {@link OidcProviderConfig} instance corresponding to the newly created provider
+   *     config.
+   * @throws NullPointerException if the provided request is null.
+   * @throws IllegalArgumentException If the provider ID string is null or empty, or is not
+   *     prefixed with 'oidc.'.
+   * @throws FirebaseAuthException if an error occurs while creating the provider config.
+   */
+  public OidcProviderConfig createOidcProviderConfig(
+      @NonNull OidcProviderConfig.CreateRequest request) throws FirebaseAuthException {
+    return createOidcProviderConfigOp(request).call();
+  }
+
+  /**
+   * Similar to {@link #createOidcProviderConfig} but performs the operation asynchronously.
+   *
+   * @param request A non-null {@link OidcProviderConfig.CreateRequest} instance.
+   * @return An {@code ApiFuture} which will complete successfully with a {@link OidcProviderConfig}
+   *     instance corresponding to the newly created provider config. If an error occurs while
+   *     creating the provider config, the future throws a {@link FirebaseAuthException}.
+   * @throws NullPointerException if the provided request is null.
+   * @throws IllegalArgumentException If the provider ID string is null or empty, or is not
+   *     prefixed with 'oidc.'.
+   */
+  public ApiFuture<OidcProviderConfig> createOidcProviderConfigAsync(
+      @NonNull OidcProviderConfig.CreateRequest request) {
+    return createOidcProviderConfigOp(request).callAsync(firebaseApp);
+  }
+
+  private CallableOperation<OidcProviderConfig, FirebaseAuthException>
+      createOidcProviderConfigOp(final OidcProviderConfig.CreateRequest request) {
+    checkNotDestroyed();
+    checkNotNull(request, "Create request must not be null.");
+    OidcProviderConfig.checkOidcProviderId(request.getProviderId());
+    final FirebaseUserManager userManager = getUserManager();
+    return new CallableOperation<OidcProviderConfig, FirebaseAuthException>() {
+      @Override
+      protected OidcProviderConfig execute() throws FirebaseAuthException {
+        return userManager.createOidcProviderConfig(request);
+      }
+    };
+  }
+
+  /**
+   * Updates an existing OpenID Connect auth provider config with the attributes contained in the
+   * specified {@link OidcProviderConfig.UpdateRequest}.
+   *
+   * @param request A non-null {@link OidcProviderConfig.UpdateRequest} instance.
+   * @return A {@link OidcProviderConfig} instance corresponding to the updated provider config.
+   * @throws NullPointerException if the provided update request is null.
+   * @throws IllegalArgumentException If the provided update request is invalid.
+   * @throws FirebaseAuthException if an error occurs while updating the provider config.
+   */
+  public OidcProviderConfig updateOidcProviderConfig(
+      @NonNull OidcProviderConfig.UpdateRequest request) throws FirebaseAuthException {
+    return updateOidcProviderConfigOp(request).call();
+  }
+
+  /**
+   * Similar to {@link #updateOidcProviderConfig} but performs the operation asynchronously.
+   *
+   * @param request A non-null {@link OidcProviderConfig.UpdateRequest} instance.
+   * @return An {@code ApiFuture} which will complete successfully with a {@link OidcProviderConfig}
+   *     instance corresponding to the updated provider config. If an error occurs while updating
+   *     the provider config, the future throws a {@link FirebaseAuthException}.
+   * @throws NullPointerException if the provided update request is null.
+   * @throws IllegalArgumentException If the provided update request is invalid.
+   */
+  public ApiFuture<OidcProviderConfig> updateOidcProviderConfigAsync(
+      @NonNull OidcProviderConfig.UpdateRequest request) {
+    return updateOidcProviderConfigOp(request).callAsync(firebaseApp);
+  }
+
+  private CallableOperation<OidcProviderConfig, FirebaseAuthException> updateOidcProviderConfigOp(
+      final OidcProviderConfig.UpdateRequest request) {
+    checkNotDestroyed();
+    checkNotNull(request, "Update request must not be null.");
+    checkArgument(!request.getProperties().isEmpty(),
+        "Update request must have at least one property set.");
+    final FirebaseUserManager userManager = getUserManager();
+    return new CallableOperation<OidcProviderConfig, FirebaseAuthException>() {
+      @Override
+      protected OidcProviderConfig execute() throws FirebaseAuthException {
+        return userManager.updateOidcProviderConfig(request);
+      }
+    };
+  }
+
+  /**
+   * Gets the OpenID Connect auth provider corresponding to the specified provider ID.
+   *
+   * @param providerId A provider ID string.
+   * @return An {@link OidcProviderConfig} instance.
+   * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed
+   *     with 'oidc'.
+   * @throws FirebaseAuthException If an error occurs while retrieving the provider config.
+   */
+  public OidcProviderConfig getOidcProviderConfig(@NonNull String providerId)
+      throws FirebaseAuthException {
+    return getOidcProviderConfigOp(providerId).call();
+  }
+
+  /**
+   * Similar to {@link #getOidcProviderConfig(String)} but performs the operation asynchronously.
+   * Page size is limited to 100 provider configs.
+   *
+   * @param providerId A provider ID string.
+   * @return An {@code ApiFuture} which will complete successfully with an
+   *     {@link OidcProviderConfig} instance. If an error occurs while retrieving the provider
+   *     config or if the specified provider ID does not exist, the future throws a
+   *     {@link FirebaseAuthException}.
+   * @throws IllegalArgumentException If the provider ID string is null or empty, or is not
+   *     prefixed with 'oidc.'.
+   */
+  public ApiFuture<OidcProviderConfig> getOidcProviderConfigAsync(@NonNull String providerId) {
+    return getOidcProviderConfigOp(providerId).callAsync(firebaseApp);
+  }
+
+  private CallableOperation<OidcProviderConfig, FirebaseAuthException>
+      getOidcProviderConfigOp(final String providerId) {
+    checkNotDestroyed();
+    OidcProviderConfig.checkOidcProviderId(providerId);
+    final FirebaseUserManager userManager = getUserManager();
+    return new CallableOperation<OidcProviderConfig, FirebaseAuthException>() {
+      @Override
+      protected OidcProviderConfig execute() throws FirebaseAuthException {
+        return userManager.getOidcProviderConfig(providerId);
+      }
+    };
+  }
+
+  /**
+   * Gets a page of OpenID Connect auth provider configs starting from the specified
+   * {@code pageToken}. Page size is limited to 100 provider configs.
+   *
+   * @param pageToken A non-empty page token string, or null to retrieve the first page of provider
+   *     configs.
+   * @return A {@link ListProviderConfigsPage} instance.
+   * @throws IllegalArgumentException If the specified page token is empty
+   * @throws FirebaseAuthException If an error occurs while retrieving provider config data.
+   */
+  public ListProviderConfigsPage<OidcProviderConfig> listOidcProviderConfigs(
+        @Nullable String pageToken) throws FirebaseAuthException {
+    int maxResults = FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS;
+    return listOidcProviderConfigsOp(pageToken, maxResults).call();
+  }
+
+  /**
+   * Gets a page of OpenID Connect 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 provider config data.
+   */
+  public ListProviderConfigsPage<OidcProviderConfig> listOidcProviderConfigs(
+        @Nullable String pageToken, int maxResults) throws FirebaseAuthException {
+    return listOidcProviderConfigsOp(pageToken, maxResults).call();
+  }
+
+  /**
+   * Similar to {@link #listOidcProviderConfigs(String)} but performs the operation asynchronously.
+   * Page size is 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) {
+    int maxResults = FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS;
+    return listOidcProviderConfigsAsync(pageToken, maxResults);
+  }
+
+  /**
+   * 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 OpenID Connect auth provider config identified by the specified provider ID.
+   *
+   * @param providerId A provider ID string.
+   * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed
+   *     with 'oidc'.
+   * @throws FirebaseAuthException If an error occurs while deleting the provider config.
+   */
+  public void deleteOidcProviderConfig(@NonNull String providerId) throws FirebaseAuthException {
+    deleteOidcProviderConfigOp(providerId).call();
+  }
+
+  /**
+   * Similar to {@link #deleteOidcProviderConfig} but performs the operation asynchronously.
+   *
+   * @param providerId A provider ID string.
+   * @return An {@code ApiFuture} which will complete successfully when the specified provider
+   *     config has been deleted. If an error occurs while deleting the provider config, the future
+   *     throws a {@link FirebaseAuthException}.
+   * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed
+   *     with "oidc.".
+   */
+  public ApiFuture<Void> deleteOidcProviderConfigAsync(String providerId) {
+    return deleteOidcProviderConfigOp(providerId).callAsync(firebaseApp);
+  }
+
+  private CallableOperation<Void, FirebaseAuthException> deleteOidcProviderConfigOp(
+      final String providerId) {
+    checkNotDestroyed();
+    OidcProviderConfig.checkOidcProviderId(providerId);
+    final FirebaseUserManager userManager = getUserManager();
+    return new CallableOperation<Void, FirebaseAuthException>() {
+      @Override
+      protected Void execute() throws FirebaseAuthException {
+        userManager.deleteOidcProviderConfig(providerId);
+        return null;
+      }
+    };
+  }
+
+  /**
+   * Creates a new SAML Auth provider config with the attributes contained in the specified
+   * {@link SamlProviderConfig.CreateRequest}.
+   *
+   * @param request A non-null {@link SamlProviderConfig.CreateRequest} instance.
+   * @return An {@link SamlProviderConfig} instance corresponding to the newly created provider
+   *     config.
+   * @throws NullPointerException if the provided request is null.
+   * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed
+   *     with 'saml'.
+   * @throws FirebaseAuthException if an error occurs while creating the provider config.
+   */
+  public SamlProviderConfig createSamlProviderConfig(
+      @NonNull SamlProviderConfig.CreateRequest request) throws FirebaseAuthException {
+    return createSamlProviderConfigOp(request).call();
+  }
+
+  /**
+   * Similar to {@link #createSamlProviderConfig} but performs the operation asynchronously.
+   *
+   * @param request A non-null {@link SamlProviderConfig.CreateRequest} instance.
+   * @return An {@code ApiFuture} which will complete successfully with a {@link SamlProviderConfig}
+   *     instance corresponding to the newly created provider config. If an error occurs while
+   *     creating the provider config, the future throws a {@link FirebaseAuthException}.
+   * @throws NullPointerException if the provided request is null.
+   * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed
+   *     with 'saml'.
+   */
+  public ApiFuture<SamlProviderConfig> createSamlProviderConfigAsync(
+      @NonNull SamlProviderConfig.CreateRequest request) {
+    return createSamlProviderConfigOp(request).callAsync(firebaseApp);
+  }
+
+  private CallableOperation<SamlProviderConfig, FirebaseAuthException>
+      createSamlProviderConfigOp(final SamlProviderConfig.CreateRequest request) {
+    checkNotDestroyed();
+    checkNotNull(request, "Create request must not be null.");
+    SamlProviderConfig.checkSamlProviderId(request.getProviderId());
+    final FirebaseUserManager userManager = getUserManager();
+    return new CallableOperation<SamlProviderConfig, FirebaseAuthException>() {
+      @Override
+      protected SamlProviderConfig execute() throws FirebaseAuthException {
+        return userManager.createSamlProviderConfig(request);
+      }
+    };
+  }
+
+  /**
+   * Updates an existing SAML Auth provider config with the attributes contained in the specified
+   * {@link SamlProviderConfig.UpdateRequest}.
+   *
+   * @param request A non-null {@link SamlProviderConfig.UpdateRequest} instance.
+   * @return A {@link SamlProviderConfig} instance corresponding to the updated provider config.
+   * @throws NullPointerException if the provided update request is null.
+   * @throws IllegalArgumentException If the provided update request is invalid.
+   * @throws FirebaseAuthException if an error occurs while updating the provider config.
+   */
+  public SamlProviderConfig updateSamlProviderConfig(
+      @NonNull SamlProviderConfig.UpdateRequest request) throws FirebaseAuthException {
+    return updateSamlProviderConfigOp(request).call();
+  }
+
+  /**
+   * Similar to {@link #updateSamlProviderConfig} but performs the operation asynchronously.
+   *
+   * @param request A non-null {@link SamlProviderConfig.UpdateRequest} instance.
+   * @return An {@code ApiFuture} which will complete successfully with a {@link SamlProviderConfig}
+   *     instance corresponding to the updated provider config. If an error occurs while updating
+   *     the provider config, the future throws a {@link FirebaseAuthException}.
+   * @throws NullPointerException if the provided update request is null.
+   * @throws IllegalArgumentException If the provided update request is invalid.
+   */
+  public ApiFuture<SamlProviderConfig> updateSamlProviderConfigAsync(
+      @NonNull SamlProviderConfig.UpdateRequest request) {
+    return updateSamlProviderConfigOp(request).callAsync(firebaseApp);
+  }
+
+  private CallableOperation<SamlProviderConfig, FirebaseAuthException> updateSamlProviderConfigOp(
+      final SamlProviderConfig.UpdateRequest request) {
+    checkNotDestroyed();
+    checkNotNull(request, "Update request must not be null.");
+    checkArgument(!request.getProperties().isEmpty(),
+        "Update request must have at least one property set.");
+    final FirebaseUserManager userManager = getUserManager();
+    return new CallableOperation<SamlProviderConfig, FirebaseAuthException>() {
+      @Override
+      protected SamlProviderConfig execute() throws FirebaseAuthException {
+        return userManager.updateSamlProviderConfig(request);
+      }
+    };
+  }
+
+  /**
+   * Gets the SAML Auth provider config corresponding to the specified provider ID.
+   *
+   * @param providerId A provider ID string.
+   * @return An {@link SamlProviderConfig} instance.
+   * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed
+   *     with 'saml'.
+   * @throws FirebaseAuthException If an error occurs while retrieving the provider config.
+   */
+  public SamlProviderConfig getSamlProviderConfig(@NonNull String providerId)
+      throws FirebaseAuthException {
+    return getSamlProviderConfigOp(providerId).call();
+  }
+
+  /**
+   * Similar to {@link #getSamlProviderConfig(String)} but performs the operation asynchronously.
+   * Page size is limited to 100 provider configs.
+   *
+   * @param providerId A provider ID string.
+   * @return An {@code ApiFuture} which will complete successfully with an
+   *     {@link SamlProviderConfig} instance. If an error occurs while retrieving the provider
+   *     config or if the specified provider ID does not exist, the future throws a
+   *     {@link FirebaseAuthException}.
+   * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed
+   *     with 'saml'.
+   */
+  public ApiFuture<SamlProviderConfig> getSamlProviderConfigAsync(@NonNull String providerId) {
+    return getSamlProviderConfigOp(providerId).callAsync(firebaseApp);
+  }
+
+  private CallableOperation<SamlProviderConfig, FirebaseAuthException>
+      getSamlProviderConfigOp(final String providerId) {
+    checkNotDestroyed();
+    SamlProviderConfig.checkSamlProviderId(providerId);
+    final FirebaseUserManager userManager = getUserManager();
+    return new CallableOperation<SamlProviderConfig, FirebaseAuthException>() {
+      @Override
+      protected SamlProviderConfig execute() throws FirebaseAuthException {
+        return userManager.getSamlProviderConfig(providerId);
+      }
+    };
+  }
+
+  /**
+   * Gets a page of SAML Auth provider configs starting from the specified {@code pageToken}. Page
+   * size is limited to 100 provider configs.
+   *
+   * @param pageToken A non-empty page token string, or null to retrieve the first page of provider
+   *     configs.
+   * @return A {@link ListProviderConfigsPage} instance.
+   * @throws IllegalArgumentException If the specified page token is empty.
+   * @throws FirebaseAuthException If an error occurs while retrieving provider config data.
+   */
+  public ListProviderConfigsPage<SamlProviderConfig> listSamlProviderConfigs(
+        @Nullable String pageToken) throws FirebaseAuthException {
+    return listSamlProviderConfigs(
+        pageToken,
+        FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS);
+  }
+
+  /**
+   * Gets a page of SAML 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 provider config data.
+   */
+  public ListProviderConfigsPage<SamlProviderConfig> listSamlProviderConfigs(
+        @Nullable String pageToken, int maxResults) throws FirebaseAuthException {
+    return listSamlProviderConfigsOp(pageToken, maxResults).call();
+  }
+
+  /**
+   * Similar to {@link #listSamlProviderConfigs(String)} but performs the operation asynchronously.
+   * Page size is 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<SamlProviderConfig>> listSamlProviderConfigsAsync(
+      @Nullable String pageToken) {
+    int maxResults = FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS;
+    return listSamlProviderConfigsAsync(pageToken, maxResults);
+  }
+
+  /**
+   * Similar to {@link #listSamlProviderConfigs(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<SamlProviderConfig>> listSamlProviderConfigsAsync(
+      @Nullable String pageToken,
+      int maxResults) {
+    return listSamlProviderConfigsOp(pageToken, maxResults).callAsync(firebaseApp);
+  }
+
+  private CallableOperation<ListProviderConfigsPage<SamlProviderConfig>, FirebaseAuthException>
+      listSamlProviderConfigsOp(@Nullable final String pageToken, final int maxResults) {
+    checkNotDestroyed();
+    final FirebaseUserManager userManager = getUserManager();
+    final DefaultSamlProviderConfigSource source = new DefaultSamlProviderConfigSource(userManager);
+    final ListProviderConfigsPage.Factory<SamlProviderConfig> factory =
+        new ListProviderConfigsPage.Factory<SamlProviderConfig>(source, maxResults, pageToken);
+    return
+      new CallableOperation<ListProviderConfigsPage<SamlProviderConfig>, FirebaseAuthException>() {
+        @Override
+        protected ListProviderConfigsPage<SamlProviderConfig> execute()
+            throws FirebaseAuthException {
+          return factory.create();
+        }
+    };
+  }
+
+  /**
+   * Deletes the SAML Auth provider config identified by the specified provider ID.
+   *
+   * @param providerId A provider ID string.
+   * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed
+   *     with "saml.".
+   * @throws FirebaseAuthException If an error occurs while deleting the provider config.
+   */
+  public void deleteSamlProviderConfig(@NonNull String providerId) throws FirebaseAuthException {
+    deleteSamlProviderConfigOp(providerId).call();
+  }
+
+  /**
+   * Similar to {@link #deleteSamlProviderConfig} but performs the operation asynchronously.
+   *
+   * @param providerId A provider ID string.
+   * @return An {@code ApiFuture} which will complete successfully when the specified provider
+   *     config has been deleted. If an error occurs while deleting the provider config, the future
+   *     throws a {@link FirebaseAuthException}.
+   * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed
+   *     with "saml.".
+   */
+  public ApiFuture<Void> deleteSamlProviderConfigAsync(String providerId) {
+    return deleteSamlProviderConfigOp(providerId).callAsync(firebaseApp);
+  }
+
+  private CallableOperation<Void, FirebaseAuthException> deleteSamlProviderConfigOp(
+      final String providerId) {
+    checkNotDestroyed();
+    SamlProviderConfig.checkSamlProviderId(providerId);
+    final FirebaseUserManager userManager = getUserManager();
+    return new CallableOperation<Void, FirebaseAuthException>() {
+      @Override
+      protected Void execute() throws FirebaseAuthException {
+        userManager.deleteSamlProviderConfig(providerId);
+        return null;
+      }
+    };
+  }
+
+  FirebaseApp getFirebaseApp() {
+    return this.firebaseApp;
+  }
+
+  FirebaseTokenVerifier getCookieVerifier() {
+    return this.cookieVerifier.get();
+  }
+
+  FirebaseUserManager getUserManager() {
+    return this.userManager.get();
+  }
+
   protected <T> Supplier<T> threadSafeMemoize(final Supplier<T> supplier) {
     return Suppliers.memoize(
         new Supplier<T>() {
diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
index e5d39b8..c65e44d 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -44,6 +44,8 @@ import com.google.firebase.auth.internal.DownloadAccountResponse;
 import com.google.firebase.auth.internal.GetAccountInfoRequest;
 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.ListSamlProviderConfigsResponse;
 import com.google.firebase.auth.internal.ListTenantsResponse;
 import com.google.firebase.auth.internal.UploadAccountResponse;
 import com.google.firebase.internal.ApiClientUtils;
@@ -69,6 +71,7 @@ import java.util.Set;
  */
 class FirebaseUserManager {
 
+  static final String CONFIGURATION_NOT_FOUND_ERROR = "configuration-not-found";
   static final String TENANT_ID_MISMATCH_ERROR = "tenant-id-mismatch";
   static final String TENANT_NOT_FOUND_ERROR = "tenant-not-found";
   static final String USER_NOT_FOUND_ERROR = "user-not-found";
@@ -78,7 +81,7 @@ class FirebaseUserManager {
   // SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors
   private static final Map<String, String> ERROR_CODES = ImmutableMap.<String, String>builder()
       .put("CLAIMS_TOO_LARGE", "claims-too-large")
-      .put("CONFIGURATION_NOT_FOUND", "project-not-found")
+      .put("CONFIGURATION_NOT_FOUND", CONFIGURATION_NOT_FOUND_ERROR)
       .put("INSUFFICIENT_PERMISSION", "insufficient-permission")
       .put("DUPLICATE_EMAIL", "email-already-exists")
       .put("DUPLICATE_LOCAL_ID", "uid-already-exists")
@@ -97,6 +100,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_GET_ACCOUNTS_BATCH_SIZE = 100;
   static final int MAX_DELETE_ACCOUNTS_BATCH_SIZE = 1000;
@@ -112,6 +116,7 @@ class FirebaseUserManager {
   private static final String CLIENT_VERSION_HEADER = "X-Client-Version";
 
   private final String userMgtBaseUrl;
+  private final String idpConfigMgtBaseUrl;
   private final String tenantMgtBaseUrl;
   private final JsonFactory jsonFactory;
   private final HttpRequestFactory requestFactory;
@@ -126,15 +131,18 @@ class FirebaseUserManager {
         "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.");
-    String tenantId = builder.tenantId;
-    if (builder.tenantId == null) {
-      this.userMgtBaseUrl = String.format(ID_TOOLKIT_URL, "v1", projectId);
+    final String idToolkitUrlV1 = String.format(ID_TOOLKIT_URL, "v1", projectId);
+    final String idToolkitUrlV2 = String.format(ID_TOOLKIT_URL, "v2", projectId);
+    final String tenantId = builder.tenantId;
+    if (tenantId == null) {
+      this.userMgtBaseUrl = idToolkitUrlV1;
+      this.idpConfigMgtBaseUrl = idToolkitUrlV2;
     } else {
-      checkArgument(!tenantId.isEmpty(), "Tenant ID must not be empty");
-      this.userMgtBaseUrl =
-          String.format(ID_TOOLKIT_URL, "v1", projectId) + getTenantUrlSuffix(tenantId);
+      checkArgument(!tenantId.isEmpty(), "Tenant ID must not be empty.");
+      this.userMgtBaseUrl = idToolkitUrlV1 + getTenantUrlSuffix(tenantId);
+      this.idpConfigMgtBaseUrl = idToolkitUrlV2 + getTenantUrlSuffix(tenantId);
     }
-    this.tenantMgtBaseUrl = String.format(ID_TOOLKIT_URL, "v2", projectId);
+    this.tenantMgtBaseUrl = idToolkitUrlV2;
     this.jsonFactory = app.getOptions().getJsonFactory();
     this.requestFactory = builder.requestFactory == null
       ? ApiClientUtils.newAuthorizedRequestFactory(app) : builder.requestFactory;
@@ -295,20 +303,11 @@ class FirebaseUserManager {
 
   Tenant updateTenant(Tenant.UpdateRequest request) throws FirebaseAuthException {
     Map<String, Object> properties = request.getProperties();
-    checkArgument(!properties.isEmpty(), "Tenant update must have at least one property set");
     GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(request.getTenantId()));
-    url.put("updateMask", generateMask(properties));
+    url.put("updateMask", Joiner.on(",").join(generateMask(properties)));
     return sendRequest("PATCH", url, properties, Tenant.class);
   }
 
-  private static String generateMask(Map<String, Object> properties) {
-    // This implementation does not currently handle the case of nested properties. This is fine
-    // since we do not currently generate masks for any properties with nested values. When it
-    // comes time to implement this, we can check if a property has nested properties by checking
-    // if it is an instance of the Map class.
-    return Joiner.on(",").join(ImmutableSortedSet.copyOf(properties.keySet()));
-  }
-
   void deleteTenant(String tenantId) throws FirebaseAuthException {
     GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId));
     sendRequest("DELETE", url, null, GenericJson.class);
@@ -366,11 +365,128 @@ class FirebaseUserManager {
     throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create email action link");
   }
 
+  OidcProviderConfig createOidcProviderConfig(
+      OidcProviderConfig.CreateRequest request) throws FirebaseAuthException {
+    GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs");
+    url.set("oauthIdpConfigId", request.getProviderId());
+    return sendRequest("POST", url, request.getProperties(), OidcProviderConfig.class);
+  }
+
+  SamlProviderConfig createSamlProviderConfig(
+      SamlProviderConfig.CreateRequest request) throws FirebaseAuthException {
+    GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/inboundSamlConfigs");
+    url.set("inboundSamlConfigId", request.getProviderId());
+    return sendRequest("POST", url, request.getProperties(), SamlProviderConfig.class);
+  }
+
+  OidcProviderConfig updateOidcProviderConfig(OidcProviderConfig.UpdateRequest request)
+      throws FirebaseAuthException {
+    Map<String, Object> properties = request.getProperties();
+    GenericUrl url =
+        new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(request.getProviderId()));
+    url.put("updateMask", Joiner.on(",").join(generateMask(properties)));
+    return sendRequest("PATCH", url, properties, OidcProviderConfig.class);
+  }
+
+  SamlProviderConfig updateSamlProviderConfig(SamlProviderConfig.UpdateRequest request)
+      throws FirebaseAuthException {
+    Map<String, Object> properties = request.getProperties();
+    GenericUrl url =
+        new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(request.getProviderId()));
+    url.put("updateMask", Joiner.on(",").join(generateMask(properties)));
+    return sendRequest("PATCH", url, properties, SamlProviderConfig.class);
+  }
+
+  OidcProviderConfig getOidcProviderConfig(String providerId) throws FirebaseAuthException {
+    GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId));
+    return sendRequest("GET", url, null, OidcProviderConfig.class);
+  }
+
+  SamlProviderConfig getSamlProviderConfig(String providerId) throws FirebaseAuthException {
+    GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId));
+    return sendRequest("GET", url, null, SamlProviderConfig.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(
+          ListProviderConfigsPage.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;
+  }
+
+  ListSamlProviderConfigsResponse listSamlProviderConfigs(int maxResults, String pageToken)
+      throws FirebaseAuthException {
+    ImmutableMap.Builder<String, Object> builder =
+        ImmutableMap.<String, Object>builder().put("pageSize", maxResults);
+    if (pageToken != null) {
+      checkArgument(!pageToken.equals(
+          ListProviderConfigsPage.END_OF_LIST), "Invalid end of list page token.");
+      builder.put("nextPageToken", pageToken);
+    }
+
+    GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/inboundSamlConfigs");
+    url.putAll(builder.build());
+    ListSamlProviderConfigsResponse response =
+        sendRequest("GET", url, null, ListSamlProviderConfigsResponse.class);
+    if (response == null) {
+      throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve provider configs.");
+    }
+    return response;
+  }
+
+  void deleteOidcProviderConfig(String providerId) throws FirebaseAuthException {
+    GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId));
+    sendRequest("DELETE", url, null, GenericJson.class);
+  }
+
+  void deleteSamlProviderConfig(String providerId) throws FirebaseAuthException {
+    GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId));
+    sendRequest("DELETE", url, null, GenericJson.class);
+  }
+
+  private static Set<String> generateMask(Map<String, Object> properties) {
+    ImmutableSortedSet.Builder<String> maskBuilder = ImmutableSortedSet.naturalOrder();
+    for (Map.Entry<String, Object> entry : properties.entrySet()) {
+      if (entry.getValue() instanceof Map) {
+        Set<String> childMask = generateMask((Map<String, Object>) entry.getValue());
+        for (String childProperty : childMask) {
+          maskBuilder.add(entry.getKey() + "." + childProperty);
+        }
+      } else {
+        maskBuilder.add(entry.getKey());
+      }
+    }
+    return maskBuilder.build();
+  }
+
   private static String getTenantUrlSuffix(String tenantId) {
-    checkArgument(!Strings.isNullOrEmpty(tenantId));
+    checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty.");
     return "/tenants/" + tenantId;
   }
 
+  private static String getOidcUrlSuffix(String providerId) {
+    checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty.");
+    return "/oauthIdpConfigs/" + providerId;
+  }
+
+  private static String getSamlUrlSuffix(String providerId) {
+    checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty.");
+    return "/inboundSamlConfigs/" + providerId;
+  }
+
   private <T> T post(String path, Object content, Class<T> clazz) throws FirebaseAuthException {
     checkArgument(!Strings.isNullOrEmpty(path), "path must not be null or empty");
     checkNotNull(content, "content must not be null for POST requests");
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..4530c0f
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java
@@ -0,0 +1,270 @@
+/*
+ * 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.auth.internal.ListSamlProviderConfigsResponse;
+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);
+    }
+  }
+
+  static class DefaultSamlProviderConfigSource implements ProviderConfigSource<SamlProviderConfig> {
+
+    private final FirebaseUserManager userManager;
+
+    DefaultSamlProviderConfigSource(FirebaseUserManager userManager) {
+      this.userManager = checkNotNull(userManager, "User manager must not be null.");
+    }
+
+    @Override
+    public ListSamlProviderConfigsResponse fetch(int maxResults, String pageToken)
+        throws FirebaseAuthException {
+      return userManager.listSamlProviderConfigs(maxResults, pageToken);
+    }
+  }
+
+  /**
+   * 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 8a8c8b4..d637174 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/OidcProviderConfig.java b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java
new file mode 100644
index 0000000..90a49e6
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java
@@ -0,0 +1,179 @@
+/*
+ * 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 com.google.api.client.util.Key;
+import com.google.common.base.Strings;
+import com.google.firebase.auth.ProviderConfig.AbstractCreateRequest;
+import com.google.firebase.auth.ProviderConfig.AbstractUpdateRequest;
+
+/**
+ * Contains metadata associated with an OIDC Auth provider.
+ *
+ * <p>Instances of this class are immutable and thread safe.
+ */
+public final class OidcProviderConfig extends ProviderConfig {
+
+  @Key("clientId")
+  private String clientId;
+
+  @Key("issuer")
+  private String issuer;
+
+  public String getClientId() {
+    return clientId;
+  }
+
+  public String getIssuer() {
+    return issuer;
+  }
+
+  /**
+   * Returns a new {@link UpdateRequest}, which can be used to update the attributes of this
+   * provider config.
+   *
+   * @return A non-null {@link UpdateRequest} instance.
+   */
+  public UpdateRequest updateRequest() {
+    return new UpdateRequest(getProviderId());
+  }
+
+  static void checkOidcProviderId(String providerId) {
+    checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty.");
+    checkArgument(providerId.startsWith("oidc."),
+        "Invalid OIDC provider ID (must be prefixed with 'oidc.'): " + providerId);
+  }
+
+  /**
+   * A specification class for creating a new OIDC Auth provider.
+   *
+   * <p>Set the initial attributes of the new provider by calling various setter methods available
+   * in this class.
+   */
+  public static final class CreateRequest extends AbstractCreateRequest<CreateRequest> {
+
+    /**
+     * Creates a new {@link CreateRequest}, which can be used to create a new OIDC Auth provider.
+     *
+     * <p>The returned object should be passed to
+     * {@link AbstractFirebaseAuth#createOidcProviderConfig(CreateRequest)} to save the config.
+     */
+    public CreateRequest() { }
+
+    /**
+     * Sets the ID for the new provider.
+     *
+     * @param providerId A non-null, non-empty provider ID string.
+     * @throws IllegalArgumentException If the provider ID is null or empty, or is not prefixed with
+     *     'oidc.'.
+     */
+    @Override
+    public CreateRequest setProviderId(String providerId) {
+      checkOidcProviderId(providerId);
+      return super.setProviderId(providerId);
+    }
+
+    /**
+     * Sets the client ID for the new provider.
+     *
+     * @param clientId A non-null, non-empty client ID string.
+     * @throws IllegalArgumentException If the client ID is null or empty.
+     */
+    public CreateRequest setClientId(String clientId) {
+      checkArgument(!Strings.isNullOrEmpty(clientId), "Client ID must not be null or empty.");
+      properties.put("clientId", clientId);
+      return this;
+    }
+
+    /**
+     * Sets the issuer for the new provider.
+     *
+     * @param issuer A non-null, non-empty issuer URL string.
+     * @throws IllegalArgumentException If the issuer URL is null or empty, or if the format is
+     *     invalid.
+     */
+    public CreateRequest setIssuer(String issuer) {
+      checkArgument(!Strings.isNullOrEmpty(issuer), "Issuer must not be null or empty.");
+      assertValidUrl(issuer);
+      properties.put("issuer", issuer);
+      return this;
+    }
+
+    CreateRequest getThis() {
+      return this;
+    }
+  }
+
+  /**
+   * A specification class for updating an existing OIDC Auth provider.
+   *
+   * <p>An instance of this class can be obtained via a {@link OidcProviderConfig} object, or from
+   * a provider ID string. Specify the changes to be made to the provider config by calling the
+   * various setter methods available in this class.
+   */
+  public static final class UpdateRequest extends AbstractUpdateRequest<UpdateRequest> {
+
+    /**
+     * Creates a new {@link UpdateRequest}, which can be used to updates an existing OIDC Auth
+     * provider.
+     *
+     * <p>The returned object should be passed to
+     * {@link AbstractFirebaseAuth#updateOidcProviderConfig(CreateRequest)} to save the updated
+     * config.
+     *
+     * @param providerId A non-null, non-empty provider ID string.
+     * @throws IllegalArgumentException If the provider ID is null or empty, or is not prefixed with
+     *     "oidc.".
+     */
+    public UpdateRequest(String providerId) {
+      super(providerId);
+      checkOidcProviderId(providerId);
+    }
+
+    /**
+     * Sets the client ID for the exsting provider.
+     *
+     * @param clientId A non-null, non-empty client ID string.
+     * @throws IllegalArgumentException If the client ID is null or empty.
+     */
+    public UpdateRequest setClientId(String clientId) {
+      checkArgument(!Strings.isNullOrEmpty(clientId), "Client ID must not be null or empty.");
+      properties.put("clientId", clientId);
+      return this;
+    }
+
+    /**
+     * Sets the issuer for the existing provider.
+     *
+     * @param issuer A non-null, non-empty issuer URL string.
+     * @throws IllegalArgumentException If the issuer URL is null or empty, or if the format is
+     *     invalid.
+     */
+    public UpdateRequest setIssuer(String issuer) {
+      checkArgument(!Strings.isNullOrEmpty(issuer), "Issuer must not be null or empty.");
+      assertValidUrl(issuer);
+      properties.put("issuer", issuer);
+      return this;
+    }
+
+    UpdateRequest getThis() {
+      return this;
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/ProviderConfig.java b/src/main/java/com/google/firebase/auth/ProviderConfig.java
new file mode 100644
index 0000000..921a07b
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/ProviderConfig.java
@@ -0,0 +1,157 @@
+/*
+ * 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 com.google.api.client.util.Key;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The base class for Auth providers.
+ */
+public abstract class ProviderConfig {
+
+  @Key("name")
+  private String resourceName;
+
+  @Key("displayName")
+  private String displayName;
+
+  @Key("enabled")
+  private boolean enabled;
+
+  public String getProviderId() {
+    return resourceName.substring(resourceName.lastIndexOf("/") + 1);
+  }
+
+  public String getDisplayName() {
+    return displayName;
+  }
+
+  public boolean isEnabled() {
+    return enabled;
+  }
+
+  static void assertValidUrl(String url) throws IllegalArgumentException {
+    try {
+      new URL(url);
+    } catch (MalformedURLException e) {
+      throw new IllegalArgumentException(url + " is a malformed URL.", e);
+    }
+  }
+
+  /**
+   * A base specification class for creating a new provider.
+   *
+   * <p>Set the initial attributes of the new provider by calling various setter methods available
+   * in this class.
+   */
+  public abstract static class AbstractCreateRequest<T extends AbstractCreateRequest<T>> {
+
+    final Map<String,Object> properties = new HashMap<>();
+    String providerId;
+
+    T setProviderId(String providerId) {
+      this.providerId = providerId;
+      return getThis();
+    }
+
+    String getProviderId() {
+      return providerId;
+    }
+
+    /**
+     * Sets the display name for the new provider.
+     *
+     * @param displayName A non-null, non-empty display name string.
+     * @throws IllegalArgumentException If the display name is null or empty.
+     */
+    public T setDisplayName(String displayName) {
+      checkArgument(!Strings.isNullOrEmpty(displayName), "Display name must not be null or empty.");
+      properties.put("displayName", displayName);
+      return getThis();
+    }
+
+    /**
+     * Sets whether to allow the user to sign in with the provider.
+     *
+     * @param enabled A boolean indicating whether the user can sign in with the provider.
+     */
+    public T setEnabled(boolean enabled) {
+      properties.put("enabled", enabled);
+      return getThis();
+    }
+
+    Map<String, Object> getProperties() {
+      return ImmutableMap.copyOf(properties);
+    }
+
+    abstract T getThis();
+  }
+
+  /**
+   * A base class for updating the attributes of an existing provider.
+   */
+  public abstract static class AbstractUpdateRequest<T extends AbstractUpdateRequest<T>> {
+
+    final String providerId;
+    final Map<String,Object> properties = new HashMap<>();
+
+    AbstractUpdateRequest(String providerId) {
+      checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty.");
+      this.providerId = providerId;
+    }
+
+    String getProviderId() {
+      return providerId;
+    }
+
+    /**
+     * Sets the display name for the existing provider.
+     *
+     * @param displayName A non-null, non-empty display name string.
+     * @throws IllegalArgumentException If the display name is null or empty.
+     */
+    public T setDisplayName(String displayName) {
+      checkArgument(!Strings.isNullOrEmpty(displayName), "Display name must not be null or empty.");
+      properties.put("displayName", displayName);
+      return getThis();
+    }
+
+    /**
+     * Sets whether to allow the user to sign in with the provider.
+     *
+     * @param enabled A boolean indicating whether the user can sign in with the provider.
+     */
+    public T setEnabled(boolean enabled) {
+      properties.put("enabled", enabled);
+      return getThis();
+    }
+
+    Map<String, Object> getProperties() {
+      return ImmutableMap.copyOf(properties);
+    }
+
+    abstract T getThis();
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/SamlProviderConfig.java b/src/main/java/com/google/firebase/auth/SamlProviderConfig.java
new file mode 100644
index 0000000..e74478b
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/SamlProviderConfig.java
@@ -0,0 +1,345 @@
+/*
+ * 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.GenericJson;
+import com.google.api.client.util.Key;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.firebase.auth.ProviderConfig.AbstractCreateRequest;
+import com.google.firebase.auth.ProviderConfig.AbstractUpdateRequest;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Contains metadata associated with a SAML Auth provider.
+ *
+ * <p>Instances of this class are immutable and thread safe.
+ */
+public final class SamlProviderConfig extends ProviderConfig {
+
+  @Key("idpConfig")
+  private GenericJson idpConfig;
+
+  @Key("spConfig")
+  private GenericJson spConfig;
+
+  public String getIdpEntityId() {
+    return (String) idpConfig.get("idpEntityId");
+  }
+
+  public String getSsoUrl() {
+    return (String) idpConfig.get("ssoUrl");
+  }
+
+  public List<String> getX509Certificates() {
+    List<Map<String, String>> idpCertificates =
+        (List<Map<String, String>>) idpConfig.get("idpCertificates");
+    checkNotNull(idpCertificates);
+    ImmutableList.Builder<String> certificates = ImmutableList.<String>builder();
+    for (Map<String, String> idpCertificate : idpCertificates) {
+      certificates.add(idpCertificate.get("x509Certificate"));
+    }
+    return certificates.build();
+  }
+
+  public String getRpEntityId() {
+    return (String) spConfig.get("spEntityId");
+  }
+
+  public String getCallbackUrl() {
+    return (String) spConfig.get("callbackUri");
+  }
+
+  /**
+   * Returns a new {@link UpdateRequest}, which can be used to update the attributes of this
+   * provider config.
+   *
+   * @return a non-null {@link UpdateRequest} instance.
+   */
+  public UpdateRequest updateRequest() {
+    return new UpdateRequest(getProviderId());
+  }
+
+  static void checkSamlProviderId(String providerId) {
+    checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty.");
+    checkArgument(providerId.startsWith("saml."),
+        "Invalid SAML provider ID (must be prefixed with 'saml.'): " + providerId);
+  }
+
+  private static List<Object> ensureNestedList(Map<String, Object> outerMap, String id) {
+    List<Object> list = (List<Object>) outerMap.get(id);
+    if (list == null) {
+      list = new ArrayList<Object>();
+      outerMap.put(id, list);
+    }
+    return list;
+  }
+
+  private static Map<String, Object> ensureNestedMap(Map<String, Object> outerMap, String id) {
+    Map<String, Object> map = (Map<String, Object>) outerMap.get(id);
+    if (map == null) {
+      map = new HashMap<String, Object>();
+      outerMap.put(id, map);
+    }
+    return map;
+  }
+
+  /**
+   * A specification class for creating a new SAML Auth provider.
+   *
+   * <p>Set the initial attributes of the new provider by calling various setter methods available
+   * in this class.
+   */
+  public static final class CreateRequest extends AbstractCreateRequest<CreateRequest> {
+
+    /**
+     * Creates a new {@link CreateRequest}, which can be used to create a new SAML Auth provider.
+     *
+     * <p>The returned object should be passed to
+     * {@link AbstractFirebaseAuth#createSamlProviderConfig(CreateRequest)} to register the provider
+     * information persistently.
+     */
+    public CreateRequest() { }
+
+    /**
+     * Sets the ID for the new provider.
+     *
+     * @param providerId A non-null, non-empty provider ID string.
+     * @throws IllegalArgumentException If the provider ID is null or empty, or is not prefixed with
+     *     'saml.'.
+     */
+    @Override
+    public CreateRequest setProviderId(String providerId) {
+      checkSamlProviderId(providerId);
+      return super.setProviderId(providerId);
+    }
+
+    /**
+     * Sets the IDP entity ID for the new provider.
+     *
+     * @param idpEntityId A non-null, non-empty IDP entity ID string.
+     * @throws IllegalArgumentException If the IDP entity ID is null or empty.
+     */
+    public CreateRequest setIdpEntityId(String idpEntityId) {
+      checkArgument(!Strings.isNullOrEmpty(idpEntityId),
+          "IDP entity ID must not be null or empty.");
+      ensureNestedMap(properties, "idpConfig").put("idpEntityId", idpEntityId);
+      return this;
+    }
+
+    /**
+     * Sets the SSO URL for the new provider.
+     *
+     * @param ssoUrl A non-null, non-empty SSO URL string.
+     * @throws IllegalArgumentException If the SSO URL is null or empty, or if the format is
+     *     invalid.
+     */
+    public CreateRequest setSsoUrl(String ssoUrl) {
+      checkArgument(!Strings.isNullOrEmpty(ssoUrl), "SSO URL must not be null or empty.");
+      assertValidUrl(ssoUrl);
+      ensureNestedMap(properties, "idpConfig").put("ssoUrl", ssoUrl);
+      return this;
+    }
+
+    /**
+     * Adds a x509 certificate to the new provider.
+     *
+     * @param x509Certificate A non-null, non-empty x509 certificate string.
+     * @throws IllegalArgumentException If the x509 certificate is null or empty.
+     */
+    public CreateRequest addX509Certificate(String x509Certificate) {
+      checkArgument(!Strings.isNullOrEmpty(x509Certificate),
+          "The x509 certificate must not be null or empty.");
+      Map<String, Object> idpConfigProperties = ensureNestedMap(properties, "idpConfig");
+      List<Object> x509Certificates = ensureNestedList(idpConfigProperties, "idpCertificates");
+      x509Certificates.add(ImmutableMap.<String, Object>of("x509Certificate", x509Certificate));
+      return this;
+    }
+
+    /**
+     * Adds a collection of x509 certificates to the new provider.
+     *
+     * @param x509Certificates A non-null, non-empty collection of x509 certificate strings.
+     * @throws IllegalArgumentException If the collection is null or empty, or if any x509
+     *     certificates are null or empty.
+     */
+    public CreateRequest addAllX509Certificates(Collection<String> x509Certificates) {
+      checkArgument(x509Certificates != null,
+          "The collection of x509 certificates must not be null.");
+      checkArgument(!x509Certificates.isEmpty(),
+          "The collection of x509 certificates must not be empty.");
+      for (String certificate : x509Certificates) {
+        addX509Certificate(certificate);
+      }
+      return this;
+    }
+
+    /**
+     * Sets the RP entity ID for the new provider.
+     *
+     * @param rpEntityId A non-null, non-empty RP entity ID string.
+     * @throws IllegalArgumentException If the RP entity ID is null or empty.
+     */
+    public CreateRequest setRpEntityId(String rpEntityId) {
+      checkArgument(!Strings.isNullOrEmpty(rpEntityId), "RP entity ID must not be null or empty.");
+      ensureNestedMap(properties, "spConfig").put("spEntityId", rpEntityId);
+      return this;
+    }
+
+    /**
+     * Sets the callback URL for the new provider.
+     *
+     * @param callbackUrl A non-null, non-empty callback URL string.
+     * @throws IllegalArgumentException If the callback URL is null or empty, or if the format is
+     *     invalid.
+     */
+    public CreateRequest setCallbackUrl(String callbackUrl) {
+      checkArgument(!Strings.isNullOrEmpty(callbackUrl), "Callback URL must not be null or empty.");
+      assertValidUrl(callbackUrl);
+      ensureNestedMap(properties, "spConfig").put("callbackUri", callbackUrl);
+      return this;
+    }
+
+    CreateRequest getThis() {
+      return this;
+    }
+  }
+
+  /**
+   * A specification class for updating an existing SAML Auth provider.
+   *
+   * <p>An instance of this class can be obtained via a {@link SamlProviderConfig} object, or from
+   * a provider ID string. Specify the changes to be made to the provider config by calling the
+   * various setter methods available in this class.
+   */
+  public static final class UpdateRequest extends AbstractUpdateRequest<UpdateRequest> {
+    /**
+     * Creates a new {@link UpdateRequest}, which can be used to updates an existing SAML Auth
+     * provider.
+     *
+     * <p>The returned object should be passed to
+     * {@link AbstractFirebaseAuth#updateSamlProviderConfig(UpdateRequest)} to update the provider
+     * information persistently.
+     *
+     * @param providerId a non-null, non-empty provider ID string.
+     * @throws IllegalArgumentException If the provider ID is null or empty, or is not prefixed with
+     *     'saml.'.
+     */
+    public UpdateRequest(String providerId) {
+      super(providerId);
+      checkSamlProviderId(providerId);
+    }
+
+    /**
+     * Sets the IDP entity ID for the existing provider.
+     *
+     * @param idpEntityId A non-null, non-empty IDP entity ID string.
+     * @throws IllegalArgumentException If the IDP entity ID is null or empty.
+     */
+    public UpdateRequest setIdpEntityId(String idpEntityId) {
+      checkArgument(!Strings.isNullOrEmpty(idpEntityId),
+          "IDP entity ID must not be null or empty.");
+      ensureNestedMap(properties, "idpConfig").put("idpEntityId", idpEntityId);
+      return this;
+    }
+
+    /**
+     * Sets the SSO URL for the existing provider.
+     *
+     * @param ssoUrl A non-null, non-empty SSO URL string.
+     * @throws IllegalArgumentException If the SSO URL is null or empty, or if the format is
+     *     invalid.
+     */
+    public UpdateRequest setSsoUrl(String ssoUrl) {
+      checkArgument(!Strings.isNullOrEmpty(ssoUrl), "SSO URL must not be null or empty.");
+      assertValidUrl(ssoUrl);
+      ensureNestedMap(properties, "idpConfig").put("ssoUrl", ssoUrl);
+      return this;
+    }
+
+    /**
+     * Adds a x509 certificate to the existing provider.
+     *
+     * @param x509Certificate A non-null, non-empty x509 certificate string.
+     * @throws IllegalArgumentException If the x509 certificate is null or empty.
+     */
+    public UpdateRequest addX509Certificate(String x509Certificate) {
+      checkArgument(!Strings.isNullOrEmpty(x509Certificate),
+          "The x509 certificate must not be null or empty.");
+      Map<String, Object> idpConfigProperties = ensureNestedMap(properties, "idpConfig");
+      List<Object> x509Certificates = ensureNestedList(idpConfigProperties, "idpCertificates");
+      x509Certificates.add(ImmutableMap.<String, Object>of("x509Certificate", x509Certificate));
+      return this;
+    }
+
+    /**
+     * Adds a collection of x509 certificates to the existing provider.
+     *
+     * @param x509Certificates A non-null, non-empty collection of x509 certificate strings.
+     * @throws IllegalArgumentException If the collection is null or empty, or if any x509
+     *     certificates are null or empty.
+     */
+    public UpdateRequest addAllX509Certificates(Collection<String> x509Certificates) {
+      checkArgument(x509Certificates != null,
+          "The collection of x509 certificates must not be null.");
+      checkArgument(!x509Certificates.isEmpty(),
+          "The collection of x509 certificates must not be empty.");
+      for (String certificate : x509Certificates) {
+        addX509Certificate(certificate);
+      }
+      return this;
+    }
+
+    /**
+     * Sets the RP entity ID for the existing provider.
+     *
+     * @param rpEntityId A non-null, non-empty RP entity ID string.
+     * @throws IllegalArgumentException If the RP entity ID is null or empty.
+     */
+    public UpdateRequest setRpEntityId(String rpEntityId) {
+      checkArgument(!Strings.isNullOrEmpty(rpEntityId), "RP entity ID must not be null or empty.");
+      ensureNestedMap(properties, "spConfig").put("spEntityId", rpEntityId);
+      return this;
+    }
+
+    /**
+     * Sets the callback URL for the exising provider.
+     *
+     * @param callbackUrl A non-null, non-empty callback URL string.
+     * @throws IllegalArgumentException If the callback URL is null or empty, or if the format is
+     *     invalid.
+     */
+    public UpdateRequest setCallbackUrl(String callbackUrl) {
+      checkArgument(!Strings.isNullOrEmpty(callbackUrl), "Callback URL must not be null or empty.");
+      assertValidUrl(callbackUrl);
+      ensureNestedMap(properties, "spConfig").put("callbackUri", callbackUrl);
+      return this;
+    }
+
+    UpdateRequest getThis() {
+      return this;
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/auth/Tenant.java b/src/main/java/com/google/firebase/auth/Tenant.java
index 993aa1b..61f5257 100644
--- a/src/main/java/com/google/firebase/auth/Tenant.java
+++ b/src/main/java/com/google/firebase/auth/Tenant.java
@@ -31,9 +31,6 @@ import java.util.Map;
  */
 public final class Tenant {
 
-  // Lazily initialized from 'resourceName'.
-  private String tenantId;
-
   @Key("name")
   private String resourceName;
 
@@ -47,10 +44,7 @@ public final class Tenant {
   private boolean emailLinkSignInEnabled;
 
   public String getTenantId() {
-    if (tenantId == null) {
-      tenantId = resourceName.substring(resourceName.lastIndexOf("/") + 1);
-    }
-    return tenantId;
+    return resourceName.substring(resourceName.lastIndexOf("/") + 1);
   }
 
   public String getDisplayName() {
@@ -66,10 +60,9 @@ public final class Tenant {
   }
 
   /**
-   * Returns a new {@link UpdateRequest}, which can be used to update the attributes
-   * of this tenant.
+   * Returns a new {@link UpdateRequest}, which can be used to update the attributes of this tenant.
    *
-   * @return a non-null Tenant.UpdateRequest instance.
+   * @return a non-null {@link UpdateRequest} instance.
    */
   public UpdateRequest updateRequest() {
     return new UpdateRequest(getTenantId());
diff --git a/src/main/java/com/google/firebase/auth/TenantManager.java b/src/main/java/com/google/firebase/auth/TenantManager.java
index 0b61755..4b3bbb9 100644
--- a/src/main/java/com/google/firebase/auth/TenantManager.java
+++ b/src/main/java/com/google/firebase/auth/TenantManager.java
@@ -68,7 +68,7 @@ public final class TenantManager {
   }
 
   public synchronized TenantAwareFirebaseAuth getAuthForTenant(@NonNull String tenantId) {
-    checkArgument(!Strings.isNullOrEmpty(tenantId), "tenantId must not be null or empty");
+    checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty.");
     if (!tenantAwareAuths.containsKey(tenantId)) {
       tenantAwareAuths.put(tenantId, new TenantAwareFirebaseAuth(firebaseApp, tenantId));
     }
@@ -90,7 +90,7 @@ public final class TenantManager {
 
   private CallableOperation<Tenant, FirebaseAuthException> getTenantOp(final String tenantId) {
     checkNotDestroyed();
-    checkArgument(!Strings.isNullOrEmpty(tenantId), "tenantId must not be null or empty");
+    checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty.");
     return new CallableOperation<Tenant, FirebaseAuthException>() {
       @Override
       protected Tenant execute() throws FirebaseAuthException {
@@ -196,7 +196,7 @@ public final class TenantManager {
   private CallableOperation<Tenant, FirebaseAuthException> createTenantOp(
       final CreateRequest request) {
     checkNotDestroyed();
-    checkNotNull(request, "create request must not be null");
+    checkNotNull(request, "Create request must not be null.");
     return new CallableOperation<Tenant, FirebaseAuthException>() {
       @Override
       protected Tenant execute() throws FirebaseAuthException {
@@ -234,7 +234,9 @@ public final class TenantManager {
   private CallableOperation<Tenant, FirebaseAuthException> updateTenantOp(
       final UpdateRequest request) {
     checkNotDestroyed();
-    checkNotNull(request, "update request must not be null");
+    checkNotNull(request, "Update request must not be null.");
+    checkArgument(!request.getProperties().isEmpty(),
+        "Tenant update must have at least one property set.");
     return new CallableOperation<Tenant, FirebaseAuthException>() {
       @Override
       protected Tenant execute() throws FirebaseAuthException {
@@ -269,7 +271,7 @@ public final class TenantManager {
 
   private CallableOperation<Void, FirebaseAuthException> deleteTenantOp(final String tenantId) {
     checkNotDestroyed();
-    checkArgument(!Strings.isNullOrEmpty(tenantId), "tenantId must not be null or empty");
+    checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty.");
     return new CallableOperation<Void, FirebaseAuthException>() {
       @Override
       protected Void execute() throws FirebaseAuthException {
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..e8b5e20
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponse.java
@@ -0,0 +1,52 @@
+/*
+ * 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.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.firebase.auth.OidcProviderConfig;
+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;
+
+  @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 Strings.nullToEmpty(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/ListSamlProviderConfigsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponse.java
new file mode 100644
index 0000000..4386e21
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponse.java
@@ -0,0 +1,52 @@
+/*
+ * 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.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.firebase.auth.SamlProviderConfig;
+import java.util.List;
+
+/**
+ * JSON data binding for ListInboundSamlConfigsResponse messages sent by Google identity toolkit
+ * service.
+ */
+public final class ListSamlProviderConfigsResponse
+    implements ListProviderConfigsResponse<SamlProviderConfig> {
+
+  @Key("inboundSamlConfigs")
+  private List<SamlProviderConfig> providerConfigs;
+
+  @Key("nextPageToken")
+  private String pageToken;
+
+  @Override
+  public List<SamlProviderConfig> getProviderConfigs() {
+    return providerConfigs == null ? ImmutableList.<SamlProviderConfig>of() : providerConfigs;
+  }
+
+  @Override
+  public boolean hasProviderConfigs() {
+    return providerConfigs != null && !providerConfigs.isEmpty();
+  }
+
+  @Override
+  public String getPageToken() {
+    return Strings.nullToEmpty(pageToken);
+  }
+}
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 c417722..f651e3d 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
@@ -46,6 +46,9 @@ import com.google.common.util.concurrent.MoreExecutors;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.ImplFirebaseTrampolines;
+import com.google.firebase.auth.ProviderConfigTestUtils.TemporaryProviderConfig;
+import com.google.firebase.auth.UserTestUtils.RandomUser;
+import com.google.firebase.auth.UserTestUtils.TemporaryUser;
 import com.google.firebase.auth.hash.Scrypt;
 import com.google.firebase.internal.Nullable;
 import com.google.firebase.testing.IntegrationTestUtils;
@@ -55,16 +58,15 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Random;
-import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
-import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExternalResource;
 
 public class FirebaseAuthIT {
 
@@ -80,13 +82,12 @@ public class FirebaseAuthIT {
   private static final HttpTransport transport = Utils.getDefaultTransport();
   private static final String ACTION_LINK_CONTINUE_URL = "http://localhost/?a=1&b=2#c=3";
 
-  private static FirebaseAuth auth;
+  private static final FirebaseAuth auth = FirebaseAuth.getInstance(
+      IntegrationTestUtils.ensureDefaultApp());
 
-  @BeforeClass
-  public static void setUpClass() {
-    FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp();
-    auth = FirebaseAuth.getInstance(masterApp);
-  }
+  @Rule public final TemporaryUser temporaryUser = new TemporaryUser(auth);
+  @Rule public final TemporaryProviderConfig temporaryProviderConfig =
+      new TemporaryProviderConfig(auth);
 
   @Test
   public void testGetNonExistingUser() throws Exception {
@@ -210,39 +211,34 @@ public class FirebaseAuthIT {
 
   @Test
   public void testCreateUserWithParams() throws Exception {
-    RandomUser randomUser = RandomUser.create();
-    String phone = randomPhoneNumber();
+    RandomUser randomUser = UserTestUtils.generateRandomUserInfo();
     UserRecord.CreateRequest user = new UserRecord.CreateRequest()
-        .setUid(randomUser.uid)
-        .setEmail(randomUser.email)
-        .setPhoneNumber(phone)
+        .setUid(randomUser.getUid())
+        .setEmail(randomUser.getEmail())
+        .setPhoneNumber(randomUser.getPhoneNumber())
         .setDisplayName("Random User")
         .setPhotoUrl("https://example.com/photo.png")
         .setEmailVerified(true)
         .setPassword("password");
 
-    UserRecord userRecord = auth.createUserAsync(user).get();
-    try {
-      assertEquals(randomUser.uid, userRecord.getUid());
-      assertEquals("Random User", userRecord.getDisplayName());
-      assertEquals(randomUser.email, userRecord.getEmail());
-      assertEquals(phone, userRecord.getPhoneNumber());
-      assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl());
-      assertTrue(userRecord.isEmailVerified());
-      assertFalse(userRecord.isDisabled());
-
-      assertEquals(2, userRecord.getProviderData().length);
-      List<String> providers = new ArrayList<>();
-      for (UserInfo provider : userRecord.getProviderData()) {
-        providers.add(provider.getProviderId());
-      }
-      assertTrue(providers.contains("password"));
-      assertTrue(providers.contains("phone"));
+    UserRecord userRecord = temporaryUser.create(user);
+    assertEquals(randomUser.getUid(), userRecord.getUid());
+    assertEquals("Random User", 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());
 
-      checkRecreateUser(randomUser.uid);
-    } finally {
-      auth.deleteUserAsync(userRecord.getUid()).get();
+    assertEquals(2, userRecord.getProviderData().length);
+    List<String> providers = new ArrayList<>();
+    for (UserInfo provider : userRecord.getProviderData()) {
+      providers.add(provider.getProviderId());
     }
+    assertTrue(providers.contains("password"));
+    assertTrue(providers.contains("phone"));
+
+    checkRecreateUser(randomUser.getUid());
   }
 
   @Test
@@ -267,12 +263,11 @@ public class FirebaseAuthIT {
     assertTrue(userRecord.getCustomClaims().isEmpty());
 
     // Update user
-    RandomUser randomUser = RandomUser.create();
-    String phone = randomPhoneNumber();
+    RandomUser randomUser = UserTestUtils.generateRandomUserInfo();
     UserRecord.UpdateRequest request = userRecord.updateRequest()
         .setDisplayName("Updated Name")
-        .setEmail(randomUser.email)
-        .setPhoneNumber(phone)
+        .setEmail(randomUser.getEmail())
+        .setPhoneNumber(randomUser.getPhoneNumber())
         .setPhotoUrl("https://example.com/photo.png")
         .setEmailVerified(true)
         .setPassword("secret");
@@ -280,8 +275,8 @@ public class FirebaseAuthIT {
     assertEquals(uid, userRecord.getUid());
     assertNull(userRecord.getTenantId());
     assertEquals("Updated Name", userRecord.getDisplayName());
-    assertEquals(randomUser.email, userRecord.getEmail());
-    assertEquals(phone, userRecord.getPhoneNumber());
+    assertEquals(randomUser.getEmail(), userRecord.getEmail());
+    assertEquals(randomUser.getPhoneNumber(), userRecord.getPhoneNumber());
     assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl());
     assertTrue(userRecord.isEmailVerified());
     assertFalse(userRecord.isDisabled());
@@ -302,7 +297,7 @@ public class FirebaseAuthIT {
     assertEquals(uid, userRecord.getUid());
     assertNull(userRecord.getTenantId());
     assertNull(userRecord.getDisplayName());
-    assertEquals(randomUser.email, userRecord.getEmail());
+    assertEquals(randomUser.getEmail(), userRecord.getEmail());
     assertNull(userRecord.getPhoneNumber());
     assertNull(userRecord.getPhotoUrl());
     assertTrue(userRecord.isEmailVerified());
@@ -312,15 +307,15 @@ public class FirebaseAuthIT {
 
     // Delete user
     auth.deleteUserAsync(userRecord.getUid()).get();
-    assertUserDoesNotExist(auth, userRecord.getUid());
+    UserTestUtils.assertUserDoesNotExist(auth, userRecord.getUid());
   }
 
   @Test
   public void testLastRefreshTime() throws Exception {
-    RandomUser user = RandomUser.create();
+    RandomUser user = UserTestUtils.generateRandomUserInfo();
     UserRecord newUserRecord = auth.createUser(new UserRecord.CreateRequest()
-                                                   .setUid(user.uid)
-                                                   .setEmail(user.email)
+                                                   .setUid(user.getUid())
+                                                   .setEmail(user.getEmail())
                                                    .setEmailVerified(false)
                                                    .setPassword("password"));
 
@@ -348,430 +343,105 @@ public class FirebaseAuthIT {
   public void testListUsers() throws Exception {
     final List<String> uids = new ArrayList<>();
 
-    try {
-      for (int i = 0; i < 3; i++) {
-        UserRecord.CreateRequest createRequest =
-            new UserRecord.CreateRequest().setPassword("password");
-        uids.add(auth.createUserAsync(createRequest).get().getUid());
-      }
-
-      // Test list by batches
-      final AtomicInteger collected = new AtomicInteger(0);
-      ListUsersPage page = auth.listUsersAsync(null).get();
-      while (page != null) {
-        for (ExportedUserRecord user : page.getValues()) {
-          if (uids.contains(user.getUid())) {
-            collected.incrementAndGet();
-            assertNotNull("Missing passwordHash field. A common cause would be "
-                + "forgetting to add the \"Firebase Authentication Admin\" permission. See "
-                + "instructions in CONTRIBUTING.md", user.getPasswordHash());
-            assertNotNull(user.getPasswordSalt());
-            assertNull(user.getTenantId());
-          }
-        }
-        page = page.getNextPage();
-      }
-      assertEquals(uids.size(), collected.get());
+    for (int i = 0; i < 3; i++) {
+      UserRecord.CreateRequest createRequest =
+          new UserRecord.CreateRequest().setPassword("password");
+      uids.add(temporaryUser.create(createRequest).getUid());
+    }
 
-      // Test iterate all
-      collected.set(0);
-      page = auth.listUsersAsync(null).get();
-      for (ExportedUserRecord user : page.iterateAll()) {
+    // Test list by batches
+    final AtomicInteger collected = new AtomicInteger(0);
+    ListUsersPage page = auth.listUsersAsync(null).get();
+    while (page != null) {
+      for (ExportedUserRecord user : page.getValues()) {
         if (uids.contains(user.getUid())) {
           collected.incrementAndGet();
-          assertNotNull(user.getPasswordHash());
+          assertNotNull("Missing passwordHash field. A common cause would be "
+              + "forgetting to add the \"Firebase Authentication Admin\" permission. See "
+              + "instructions in CONTRIBUTING.md", user.getPasswordHash());
           assertNotNull(user.getPasswordSalt());
           assertNull(user.getTenantId());
         }
       }
-      assertEquals(uids.size(), collected.get());
-
-      // Test iterate async
-      collected.set(0);
-      final Semaphore semaphore = new Semaphore(0);
-      final AtomicReference<Throwable> error = new AtomicReference<>();
-      ApiFuture<ListUsersPage> pageFuture = auth.listUsersAsync(null);
-      ApiFutures.addCallback(pageFuture, new ApiFutureCallback<ListUsersPage>() {
-        @Override
-        public void onFailure(Throwable t) {
-          error.set(t);
-          semaphore.release();
-        }
-
-        @Override
-        public void onSuccess(ListUsersPage result) {
-          for (ExportedUserRecord user : result.iterateAll()) {
-            if (uids.contains(user.getUid())) {
-              collected.incrementAndGet();
-              assertNotNull(user.getPasswordHash());
-              assertNotNull(user.getPasswordSalt());
-              assertNull(user.getTenantId());
-            }
-          }
-          semaphore.release();
-        }
-      }, MoreExecutors.directExecutor());
-      semaphore.acquire();
-      assertEquals(uids.size(), collected.get());
-      assertNull(error.get());
-    } finally {
-      for (String uid : uids) {
-        auth.deleteUserAsync(uid).get();
+      page = page.getNextPage();
+    }
+    assertEquals(uids.size(), collected.get());
+
+    // Test iterate all
+    collected.set(0);
+    page = auth.listUsersAsync(null).get();
+    for (ExportedUserRecord user : page.iterateAll()) {
+      if (uids.contains(user.getUid())) {
+        collected.incrementAndGet();
+        assertNotNull(user.getPasswordHash());
+        assertNotNull(user.getPasswordSalt());
+        assertNull(user.getTenantId());
       }
     }
-  }
-
-  @Test
-  public void testTenantAwareUserLifecycle() throws Exception {
-    // Create tenant to use.
-    TenantManager tenantManager = auth.getTenantManager();
-    Tenant.CreateRequest tenantCreateRequest =
-        new Tenant.CreateRequest().setDisplayName("DisplayName");
-    final String tenantId = tenantManager.createTenant(tenantCreateRequest).getTenantId();
-
-    TenantAwareFirebaseAuth tenantAwareAuth = auth.getTenantManager().getAuthForTenant(tenantId);
-
-    // Create user
-    UserRecord userRecord = tenantAwareAuth.createUserAsync(new UserRecord.CreateRequest()).get();
-    String uid = userRecord.getUid();
-
-    // Get user
-    userRecord = tenantAwareAuth.getUserAsync(userRecord.getUid()).get();
-    assertEquals(uid, userRecord.getUid());
-    assertEquals(tenantId, userRecord.getTenantId());
-    assertNull(userRecord.getDisplayName());
-    assertNull(userRecord.getEmail());
-    assertNull(userRecord.getPhoneNumber());
-    assertNull(userRecord.getPhotoUrl());
-    assertFalse(userRecord.isEmailVerified());
-    assertFalse(userRecord.isDisabled());
-    assertTrue(userRecord.getUserMetadata().getCreationTimestamp() > 0);
-    assertEquals(0, userRecord.getUserMetadata().getLastSignInTimestamp());
-    assertEquals(0, userRecord.getProviderData().length);
-    assertTrue(userRecord.getCustomClaims().isEmpty());
-
-    // Update user
-    RandomUser randomUser = RandomUser.create();
-    String phone = randomPhoneNumber();
-    UserRecord.UpdateRequest request = userRecord.updateRequest()
-        .setDisplayName("Updated Name")
-        .setEmail(randomUser.email)
-        .setPhoneNumber(phone)
-        .setPhotoUrl("https://example.com/photo.png")
-        .setEmailVerified(true)
-        .setPassword("secret");
-    userRecord = tenantAwareAuth.updateUserAsync(request).get();
-    assertEquals(uid, userRecord.getUid());
-    assertEquals(tenantId, userRecord.getTenantId());
-    assertEquals("Updated Name", userRecord.getDisplayName());
-    assertEquals(randomUser.email, userRecord.getEmail());
-    assertEquals(phone, userRecord.getPhoneNumber());
-    assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl());
-    assertTrue(userRecord.isEmailVerified());
-    assertFalse(userRecord.isDisabled());
-    assertEquals(2, userRecord.getProviderData().length);
-    assertTrue(userRecord.getCustomClaims().isEmpty());
-
-    // Get user by email
-    userRecord = tenantAwareAuth.getUserByEmailAsync(userRecord.getEmail()).get();
-    assertEquals(uid, userRecord.getUid());
-
-    // Disable user and remove properties
-    request = userRecord.updateRequest()
-        .setPhotoUrl(null)
-        .setDisplayName(null)
-        .setPhoneNumber(null)
-        .setDisabled(true);
-    userRecord = tenantAwareAuth.updateUserAsync(request).get();
-    assertEquals(uid, userRecord.getUid());
-    assertEquals(tenantId, userRecord.getTenantId());
-    assertNull(userRecord.getDisplayName());
-    assertEquals(randomUser.email, userRecord.getEmail());
-    assertNull(userRecord.getPhoneNumber());
-    assertNull(userRecord.getPhotoUrl());
-    assertTrue(userRecord.isEmailVerified());
-    assertTrue(userRecord.isDisabled());
-    assertEquals(1, userRecord.getProviderData().length);
-    assertTrue(userRecord.getCustomClaims().isEmpty());
-
-    // Delete user and tenant
-    tenantAwareAuth.deleteUserAsync(userRecord.getUid()).get();
-    assertUserDoesNotExist(tenantAwareAuth, userRecord.getUid());
-    tenantManager.deleteTenant(tenantId);
-  }
-
-  @Test
-  public void testTenantAwareListUsers() throws Exception {
-    // Create tenant to use.
-    TenantManager tenantManager = auth.getTenantManager();
-    Tenant.CreateRequest tenantCreateRequest =
-        new Tenant.CreateRequest().setDisplayName("DisplayName");
-    final String tenantId = tenantManager.createTenant(tenantCreateRequest).getTenantId();
-
-    TenantAwareFirebaseAuth tenantAwareAuth = tenantManager.getAuthForTenant(tenantId);
-    final List<String> uids = new ArrayList<>();
-
-    try {
-      for (int i = 0; i < 3; i++) {
-        UserRecord.CreateRequest createRequest =
-            new UserRecord.CreateRequest().setPassword("password");
-        uids.add(tenantAwareAuth.createUserAsync(createRequest).get().getUid());
+    assertEquals(uids.size(), collected.get());
+
+    // Test iterate async
+    collected.set(0);
+    final Semaphore semaphore = new Semaphore(0);
+    final AtomicReference<Throwable> error = new AtomicReference<>();
+    ApiFuture<ListUsersPage> pageFuture = auth.listUsersAsync(null);
+    ApiFutures.addCallback(pageFuture, new ApiFutureCallback<ListUsersPage>() {
+      @Override
+      public void onFailure(Throwable t) {
+        error.set(t);
+        semaphore.release();
       }
 
-      // Test list by batches
-      final AtomicInteger collected = new AtomicInteger(0);
-      ListUsersPage page = tenantAwareAuth.listUsersAsync(null).get();
-      while (page != null) {
-        for (ExportedUserRecord user : page.getValues()) {
+      @Override
+      public void onSuccess(ListUsersPage result) {
+        for (ExportedUserRecord user : result.iterateAll()) {
           if (uids.contains(user.getUid())) {
             collected.incrementAndGet();
-            assertNotNull("Missing passwordHash field. A common cause would be "
-                + "forgetting to add the \"Firebase Authentication Admin\" permission. See "
-                + "instructions in CONTRIBUTING.md", user.getPasswordHash());
+            assertNotNull(user.getPasswordHash());
             assertNotNull(user.getPasswordSalt());
-            assertEquals(tenantId, user.getTenantId());
-          }
-        }
-        page = page.getNextPage();
-      }
-      assertEquals(uids.size(), collected.get());
-
-      // Test iterate all
-      collected.set(0);
-      page = tenantAwareAuth.listUsersAsync(null).get();
-      for (ExportedUserRecord user : page.iterateAll()) {
-        if (uids.contains(user.getUid())) {
-          collected.incrementAndGet();
-          assertNotNull(user.getPasswordHash());
-          assertNotNull(user.getPasswordSalt());
-          assertEquals(tenantId, user.getTenantId());
-        }
-      }
-      assertEquals(uids.size(), collected.get());
-
-      // Test iterate async
-      collected.set(0);
-      final Semaphore semaphore = new Semaphore(0);
-      final AtomicReference<Throwable> error = new AtomicReference<>();
-      ApiFuture<ListUsersPage> pageFuture = tenantAwareAuth.listUsersAsync(null);
-      ApiFutures.addCallback(pageFuture, new ApiFutureCallback<ListUsersPage>() {
-        @Override
-        public void onFailure(Throwable t) {
-          error.set(t);
-          semaphore.release();
-        }
-
-        @Override
-        public void onSuccess(ListUsersPage result) {
-          for (ExportedUserRecord user : result.iterateAll()) {
-            if (uids.contains(user.getUid())) {
-              collected.incrementAndGet();
-              assertNotNull(user.getPasswordHash());
-              assertNotNull(user.getPasswordSalt());
-              assertEquals(tenantId, user.getTenantId());
-            }
-          }
-          semaphore.release();
-        }
-      }, MoreExecutors.directExecutor());
-      semaphore.acquire();
-      assertEquals(uids.size(), collected.get());
-      assertNull(error.get());
-    } finally {
-      for (String uid : uids) {
-        tenantAwareAuth.deleteUserAsync(uid).get();
-      }
-      tenantManager.deleteTenant(tenantId);
-    }
-  }
-
-  @Test
-  public void testTenantAwareGetUserWithMultipleTenantIds() throws Exception {
-    // Create tenants to use.
-    TenantManager tenantManager = auth.getTenantManager();
-    Tenant.CreateRequest tenantCreateRequest1 =
-        new Tenant.CreateRequest().setDisplayName("DisplayName1");
-    String tenantId1 = tenantManager.createTenant(tenantCreateRequest1).getTenantId();
-    Tenant.CreateRequest tenantCreateRequest2 =
-        new Tenant.CreateRequest().setDisplayName("DisplayName2");
-    String tenantId2 = tenantManager.createTenant(tenantCreateRequest2).getTenantId();
-
-    // Create three users (one without a tenant ID, and two with different tenant IDs).
-    UserRecord.CreateRequest createRequest = new UserRecord.CreateRequest();
-    UserRecord nonTenantUserRecord = auth.createUser(createRequest);
-    TenantAwareFirebaseAuth tenantAwareAuth1 = auth.getTenantManager().getAuthForTenant(tenantId1);
-    UserRecord tenantUserRecord1 = tenantAwareAuth1.createUser(createRequest);
-    TenantAwareFirebaseAuth tenantAwareAuth2 = auth.getTenantManager().getAuthForTenant(tenantId2);
-    UserRecord tenantUserRecord2 = tenantAwareAuth2.createUser(createRequest);
-
-    // Make sure only non-tenant users can be fetched using the standard client.
-    assertNotNull(auth.getUser(nonTenantUserRecord.getUid()));
-    assertUserDoesNotExist(auth, tenantUserRecord1.getUid());
-    assertUserDoesNotExist(auth, tenantUserRecord2.getUid());
-
-    // Make sure tenant-aware client cannot fetch users outside that tenant.
-    assertUserDoesNotExist(tenantAwareAuth1, nonTenantUserRecord.getUid());
-    assertUserDoesNotExist(tenantAwareAuth1, tenantUserRecord2.getUid());
-    assertUserDoesNotExist(tenantAwareAuth2, nonTenantUserRecord.getUid());
-    assertUserDoesNotExist(tenantAwareAuth2, tenantUserRecord1.getUid());
-
-    // Make sure tenant-aware client can fetch users under that tenant.
-    assertNotNull(tenantAwareAuth1.getUser(tenantUserRecord1.getUid()));
-    assertNotNull(tenantAwareAuth2.getUser(tenantUserRecord2.getUid()));
-
-    // Delete tenants.
-    tenantManager.deleteTenant(tenantId1);
-    tenantManager.deleteTenant(tenantId2);
-  }
-
-  @Test
-  public void testTenantLifecycle() throws Exception {
-    TenantManager tenantManager = auth.getTenantManager();
-
-    // Create tenant
-    Tenant.CreateRequest createRequest = new Tenant.CreateRequest().setDisplayName("DisplayName");
-    Tenant tenant = tenantManager.createTenantAsync(createRequest).get();
-    assertEquals("DisplayName", tenant.getDisplayName());
-    assertFalse(tenant.isPasswordSignInAllowed());
-    assertFalse(tenant.isEmailLinkSignInEnabled());
-    String tenantId = tenant.getTenantId();
-
-    // Get tenant
-    tenant = tenantManager.getTenantAsync(tenantId).get();
-    assertEquals(tenantId, tenant.getTenantId());
-    assertEquals("DisplayName", tenant.getDisplayName());
-    assertFalse(tenant.isPasswordSignInAllowed());
-    assertFalse(tenant.isEmailLinkSignInEnabled());
-
-    // Update tenant
-    Tenant.UpdateRequest updateRequest = tenant.updateRequest()
-        .setDisplayName("UpdatedName")
-        .setPasswordSignInAllowed(true)
-        .setEmailLinkSignInEnabled(true);
-    tenant = tenantManager.updateTenantAsync(updateRequest).get();
-    assertEquals(tenantId, tenant.getTenantId());
-    assertEquals("UpdatedName", tenant.getDisplayName());
-    assertTrue(tenant.isPasswordSignInAllowed());
-    assertTrue(tenant.isEmailLinkSignInEnabled());
-
-    // Delete tenant
-    tenantManager.deleteTenantAsync(tenant.getTenantId()).get();
-    try {
-      tenantManager.getTenantAsync(tenant.getTenantId()).get();
-      fail("No error thrown for getting a deleted tenant");
-    } catch (ExecutionException e) {
-      assertTrue(e.getCause() instanceof FirebaseAuthException);
-      assertEquals(FirebaseUserManager.TENANT_NOT_FOUND_ERROR,
-          ((FirebaseAuthException) e.getCause()).getErrorCode());
-    }
-  }
-
-  @Test
-  public void testListTenants() throws Exception {
-    TenantManager tenantManager = auth.getTenantManager();
-    final List<String> tenantIds = new ArrayList<>();
-
-    try {
-      for (int i = 0; i < 3; i++) {
-        Tenant.CreateRequest createRequest =
-            new Tenant.CreateRequest().setDisplayName("DisplayName" + i);
-        tenantIds.add(tenantManager.createTenantAsync(createRequest).get().getTenantId());
-      }
-
-      // Test list by batches
-      final AtomicInteger collected = new AtomicInteger(0);
-      ListTenantsPage page = tenantManager.listTenantsAsync(null).get();
-      while (page != null) {
-        for (Tenant tenant : page.getValues()) {
-          if (tenantIds.contains(tenant.getTenantId())) {
-            collected.incrementAndGet();
-            assertNotNull(tenant.getDisplayName());
-          }
-        }
-        page = page.getNextPage();
-      }
-      assertEquals(tenantIds.size(), collected.get());
-
-      // Test iterate all
-      collected.set(0);
-      page = tenantManager.listTenantsAsync(null).get();
-      for (Tenant tenant : page.iterateAll()) {
-        if (tenantIds.contains(tenant.getTenantId())) {
-          collected.incrementAndGet();
-          assertNotNull(tenant.getDisplayName());
-        }
-      }
-      assertEquals(tenantIds.size(), collected.get());
-
-      // Test iterate async
-      collected.set(0);
-      final Semaphore semaphore = new Semaphore(0);
-      final AtomicReference<Throwable> error = new AtomicReference<>();
-      ApiFuture<ListTenantsPage> pageFuture = tenantManager.listTenantsAsync(null);
-      ApiFutures.addCallback(pageFuture, new ApiFutureCallback<ListTenantsPage>() {
-        @Override
-        public void onFailure(Throwable t) {
-          error.set(t);
-          semaphore.release();
-        }
-
-        @Override
-        public void onSuccess(ListTenantsPage result) {
-          for (Tenant tenant : result.iterateAll()) {
-            if (tenantIds.contains(tenant.getTenantId())) {
-              collected.incrementAndGet();
-              assertNotNull(tenant.getDisplayName());
-            }
+            assertNull(user.getTenantId());
           }
-          semaphore.release();
         }
-      }, MoreExecutors.directExecutor());
-      semaphore.acquire();
-      assertEquals(tenantIds.size(), collected.get());
-      assertNull(error.get());
-    } finally {
-      for (String tenantId : tenantIds) {
-        tenantManager.deleteTenantAsync(tenantId).get();
+        semaphore.release();
       }
-    }
+    }, MoreExecutors.directExecutor());
+    semaphore.acquire();
+    assertEquals(uids.size(), collected.get());
+    assertNull(error.get());
   }
 
   @Test
   public void testCustomClaims() throws Exception {
-    UserRecord userRecord = auth.createUserAsync(new UserRecord.CreateRequest()).get();
+    UserRecord userRecord = temporaryUser.create(new UserRecord.CreateRequest());
     String uid = userRecord.getUid();
 
-    try {
-      // New user should not have any claims
-      assertTrue(userRecord.getCustomClaims().isEmpty());
-
-      Map<String, Object> expected = ImmutableMap.<String, Object>of(
-          "admin", true, "package", "gold");
-      auth.setCustomUserClaimsAsync(uid, expected).get();
-
-      // Should have 2 claims
-      UserRecord updatedUser = auth.getUserAsync(uid).get();
-      assertEquals(2, updatedUser.getCustomClaims().size());
-      for (Map.Entry<String, Object> entry : expected.entrySet()) {
-        assertEquals(entry.getValue(), updatedUser.getCustomClaims().get(entry.getKey()));
-      }
+    // New user should not have any claims
+    assertTrue(userRecord.getCustomClaims().isEmpty());
 
-      // User's ID token should have the custom claims
-      String customToken = auth.createCustomTokenAsync(uid).get();
-      String idToken = signInWithCustomToken(customToken);
-      FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get();
-      Map<String, Object> result = decoded.getClaims();
-      for (Map.Entry<String, Object> entry : expected.entrySet()) {
-        assertEquals(entry.getValue(), result.get(entry.getKey()));
-      }
+    Map<String, Object> expected = ImmutableMap.<String, Object>of(
+        "admin", true, "package", "gold");
+    auth.setCustomUserClaimsAsync(uid, expected).get();
 
-      // Should be able to remove custom claims
-      auth.setCustomUserClaimsAsync(uid, null).get();
-      updatedUser = auth.getUserAsync(uid).get();
-      assertTrue(updatedUser.getCustomClaims().isEmpty());
-    } finally {
-      auth.deleteUserAsync(uid).get();
+    // Should have 2 claims
+    UserRecord updatedUser = auth.getUserAsync(uid).get();
+    assertEquals(2, updatedUser.getCustomClaims().size());
+    for (Map.Entry<String, Object> entry : expected.entrySet()) {
+      assertEquals(entry.getValue(), updatedUser.getCustomClaims().get(entry.getKey()));
+    }
+
+    // User's ID token should have the custom claims
+    String customToken = auth.createCustomTokenAsync(uid).get();
+    String idToken = signInWithCustomToken(customToken);
+    FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get();
+    Map<String, Object> result = decoded.getClaims();
+    for (Map.Entry<String, Object> entry : expected.entrySet()) {
+      assertEquals(entry.getValue(), result.get(entry.getKey()));
     }
+
+    // Should be able to remove custom claims
+    auth.setCustomUserClaimsAsync(uid, null).get();
+    updatedUser = auth.getUserAsync(uid).get();
+    assertTrue(updatedUser.getCustomClaims().isEmpty());
   }
 
   @Test
@@ -807,58 +477,6 @@ public class FirebaseAuthIT {
     }
   }
 
-  @Test
-  public void testTenantAwareCustomToken() throws Exception {
-    // Create tenant to use.
-    TenantManager tenantManager = auth.getTenantManager();
-    Tenant.CreateRequest tenantCreateRequest =
-        new Tenant.CreateRequest().setDisplayName("DisplayName");
-    String tenantId = tenantManager.createTenant(tenantCreateRequest).getTenantId();
-
-    try {
-      // Create and decode a token with a tenant-aware client.
-      TenantAwareFirebaseAuth tenantAwareAuth = auth.getTenantManager().getAuthForTenant(tenantId);
-      String customToken = tenantAwareAuth.createCustomTokenAsync("user1").get();
-      String idToken = signInWithCustomToken(customToken, tenantId);
-      FirebaseToken decoded = tenantAwareAuth.verifyIdTokenAsync(idToken).get();
-      assertEquals("user1", decoded.getUid());
-      assertEquals(tenantId, decoded.getTenantId());
-    } finally {
-      // Delete tenant.
-      tenantManager.deleteTenantAsync(tenantId).get();
-    }
-  }
-
-  @Test
-  public void testVerifyTokenWithWrongTenantAwareClient() throws Exception {
-    // Create tenant to use.
-    TenantManager tenantManager = auth.getTenantManager();
-    Tenant.CreateRequest tenantCreateRequest =
-        new Tenant.CreateRequest().setDisplayName("DisplayName");
-    String tenantId = tenantManager.createTenant(tenantCreateRequest).getTenantId();
-
-    // Create tenant-aware clients.
-    TenantAwareFirebaseAuth tenantAwareAuth1 = auth.getTenantManager().getAuthForTenant(tenantId);
-    TenantAwareFirebaseAuth tenantAwareAuth2 = auth.getTenantManager().getAuthForTenant("OTHER");
-
-    try {
-      // Create a token with one client and decode with the other.
-      String customToken = tenantAwareAuth1.createCustomTokenAsync("user").get();
-      String idToken = signInWithCustomToken(customToken, tenantId);
-      try {
-        tenantAwareAuth2.verifyIdTokenAsync(idToken).get();
-        fail("No error thrown for verifying a token with the wrong tenant-aware client");
-      } catch (ExecutionException e) {
-        assertTrue(e.getCause() instanceof FirebaseAuthException);
-        assertEquals(FirebaseUserManager.TENANT_ID_MISMATCH_ERROR,
-            ((FirebaseAuthException) e.getCause()).getErrorCode());
-      }
-    } finally {
-      // Delete tenant.
-      tenantManager.deleteTenantAsync(tenantId).get();
-    }
-  }
-
   @Test
   public void testVerifyIdToken() throws Exception {
     String customToken = auth.createCustomTokenAsync("user2").get();
@@ -935,32 +553,29 @@ public class FirebaseAuthIT {
 
   @Test
   public void testImportUsers() throws Exception {
-    RandomUser randomUser = RandomUser.create();
+    RandomUser randomUser = UserTestUtils.generateRandomUserInfo();
     ImportUserRecord user = ImportUserRecord.builder()
-        .setUid(randomUser.uid)
-        .setEmail(randomUser.email)
+        .setUid(randomUser.getUid())
+        .setEmail(randomUser.getEmail())
         .build();
 
     UserImportResult result = auth.importUsersAsync(ImmutableList.of(user)).get();
+    temporaryUser.registerUid(randomUser.getUid());
     assertEquals(1, result.getSuccessCount());
     assertEquals(0, result.getFailureCount());
 
-    try {
-      UserRecord savedUser = auth.getUserAsync(randomUser.uid).get();
-      assertEquals(randomUser.email, savedUser.getEmail());
-    } finally {
-      auth.deleteUserAsync(randomUser.uid).get();
-    }
+    UserRecord savedUser = auth.getUserAsync(randomUser.getUid()).get();
+    assertEquals(randomUser.getEmail(), savedUser.getEmail());
   }
 
   @Test
   public void testImportUsersWithPassword() throws Exception {
-    RandomUser randomUser = RandomUser.create();
+    RandomUser randomUser = UserTestUtils.generateRandomUserInfo();
     final byte[] passwordHash = BaseEncoding.base64().decode(
         "V358E8LdWJXAO7muq0CufVpEOXaj8aFiC7T/rcaGieN04q/ZPJ08WhJEHGjj9lz/2TT+/86N5VjVoc5DdBhBiw==");
     ImportUserRecord user = ImportUserRecord.builder()
-        .setUid(randomUser.uid)
-        .setEmail(randomUser.email)
+        .setUid(randomUser.getUid())
+        .setEmail(randomUser.getEmail())
         .setPasswordHash(passwordHash)
         .setPasswordSalt("NaCl".getBytes())
         .build();
@@ -976,88 +591,309 @@ public class FirebaseAuthIT {
             .setRounds(8)
             .setMemoryCost(14)
             .build())).get();
+    temporaryUser.registerUid(randomUser.getUid());
     assertEquals(1, result.getSuccessCount());
     assertEquals(0, result.getFailureCount());
 
-    try {
-      UserRecord savedUser = auth.getUserAsync(randomUser.uid).get();
-      assertEquals(randomUser.email, savedUser.getEmail());
-      String idToken = signInWithPassword(randomUser.email, "password");
-      assertFalse(Strings.isNullOrEmpty(idToken));
-    } finally {
-      auth.deleteUserAsync(randomUser.uid).get();
-    }
+    UserRecord savedUser = auth.getUserAsync(randomUser.getUid()).get();
+    assertEquals(randomUser.getEmail(), savedUser.getEmail());
+    String idToken = signInWithPassword(randomUser.getEmail(), "password");
+    assertFalse(Strings.isNullOrEmpty(idToken));
   }
 
   @Test
   public void testGeneratePasswordResetLink() throws Exception {
-    RandomUser user = RandomUser.create();
-    auth.createUser(new UserRecord.CreateRequest()
-        .setUid(user.uid)
-        .setEmail(user.email)
+    RandomUser user = UserTestUtils.generateRandomUserInfo();
+    temporaryUser.create(new UserRecord.CreateRequest()
+        .setUid(user.getUid())
+        .setEmail(user.getEmail())
         .setEmailVerified(false)
         .setPassword("password"));
-    try {
-      String link = auth.generatePasswordResetLink(user.email, ActionCodeSettings.builder()
-          .setUrl(ACTION_LINK_CONTINUE_URL)
-          .setHandleCodeInApp(false)
-          .build());
-      Map<String, String> linkParams = parseLinkParameters(link);
-      assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl"));
-      String email = resetPassword(user.email, "password", "newpassword",
-          linkParams.get("oobCode"));
-      assertEquals(user.email, email);
-      // Password reset also verifies the user's email
-      assertTrue(auth.getUser(user.uid).isEmailVerified());
-    } finally {
-      auth.deleteUser(user.uid);
-    }
+    String link = auth.generatePasswordResetLink(user.getEmail(), ActionCodeSettings.builder()
+        .setUrl(ACTION_LINK_CONTINUE_URL)
+        .setHandleCodeInApp(false)
+        .build());
+    Map<String, String> linkParams = parseLinkParameters(link);
+    assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl"));
+    String email = resetPassword(user.getEmail(), "password", "newpassword",
+        linkParams.get("oobCode"));
+    assertEquals(user.getEmail(), email);
+    // Password reset also verifies the user's email
+    assertTrue(auth.getUser(user.getUid()).isEmailVerified());
   }
 
   @Test
   public void testGenerateEmailVerificationResetLink() throws Exception {
-    RandomUser user = RandomUser.create();
-    auth.createUser(new UserRecord.CreateRequest()
-        .setUid(user.uid)
-        .setEmail(user.email)
+    RandomUser user = UserTestUtils.generateRandomUserInfo();
+    temporaryUser.create(new UserRecord.CreateRequest()
+        .setUid(user.getUid())
+        .setEmail(user.getEmail())
         .setEmailVerified(false)
         .setPassword("password"));
-    try {
-      String link = auth.generateEmailVerificationLink(user.email, ActionCodeSettings.builder()
-          .setUrl(ACTION_LINK_CONTINUE_URL)
-          .setHandleCodeInApp(false)
-          .build());
-      Map<String, String> linkParams = parseLinkParameters(link);
-      assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl"));
-      // There doesn't seem to be a public API for verifying an email, so we cannot do a more
-      // thorough test here.
-      assertEquals("verifyEmail", linkParams.get("mode"));
-    } finally {
-      auth.deleteUser(user.uid);
-    }
+    String link = auth.generateEmailVerificationLink(user.getEmail(), ActionCodeSettings.builder()
+        .setUrl(ACTION_LINK_CONTINUE_URL)
+        .setHandleCodeInApp(false)
+        .build());
+    Map<String, String> linkParams = parseLinkParameters(link);
+    assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl"));
+    // There doesn't seem to be a public API for verifying an email, so we cannot do a more
+    // thorough test here.
+    assertEquals("verifyEmail", linkParams.get("mode"));
   }
 
   @Test
   public void testGenerateSignInWithEmailLink() throws Exception {
-    RandomUser user = RandomUser.create();
-    auth.createUser(new UserRecord.CreateRequest()
-        .setUid(user.uid)
-        .setEmail(user.email)
+    RandomUser user = UserTestUtils.generateRandomUserInfo();
+    temporaryUser.create(new UserRecord.CreateRequest()
+        .setUid(user.getUid())
+        .setEmail(user.getEmail())
         .setEmailVerified(false)
         .setPassword("password"));
-    try {
-      String link = auth.generateSignInWithEmailLink(user.email, ActionCodeSettings.builder()
-          .setUrl(ACTION_LINK_CONTINUE_URL)
-          .setHandleCodeInApp(false)
-          .build());
-      Map<String, String> linkParams = parseLinkParameters(link);
-      assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl"));
-      String idToken = signInWithEmailLink(user.email, linkParams.get("oobCode"));
-      assertFalse(Strings.isNullOrEmpty(idToken));
-      assertTrue(auth.getUser(user.uid).isEmailVerified());
-    } finally {
-      auth.deleteUser(user.uid);
+    String link = auth.generateSignInWithEmailLink(user.getEmail(), ActionCodeSettings.builder()
+        .setUrl(ACTION_LINK_CONTINUE_URL)
+        .setHandleCodeInApp(false)
+        .build());
+    Map<String, String> linkParams = parseLinkParameters(link);
+    assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl"));
+    String idToken = signInWithEmailLink(user.getEmail(), linkParams.get("oobCode"));
+    assertFalse(Strings.isNullOrEmpty(idToken));
+    assertTrue(auth.getUser(user.getUid()).isEmailVerified());
+  }
+
+  @Test
+  public void testOidcProviderConfigLifecycle() throws Exception {
+    // Create provider config
+    String providerId = "oidc.provider-id";
+    OidcProviderConfig config = temporaryProviderConfig.createOidcProviderConfig(
+        new OidcProviderConfig.CreateRequest()
+            .setProviderId(providerId)
+            .setDisplayName("DisplayName")
+            .setEnabled(true)
+            .setClientId("ClientId")
+            .setIssuer("https://oidc.com/issuer"));
+    assertEquals(providerId, config.getProviderId());
+    assertEquals("DisplayName", config.getDisplayName());
+    assertTrue(config.isEnabled());
+    assertEquals("ClientId", config.getClientId());
+    assertEquals("https://oidc.com/issuer", config.getIssuer());
+
+    // Get provider config
+    config = auth.getOidcProviderConfigAsync(providerId).get();
+    assertEquals(providerId, config.getProviderId());
+    assertEquals("DisplayName", config.getDisplayName());
+    assertTrue(config.isEnabled());
+    assertEquals("ClientId", config.getClientId());
+    assertEquals("https://oidc.com/issuer", config.getIssuer());
+
+    // Update provider config
+    OidcProviderConfig.UpdateRequest updateRequest =
+        new OidcProviderConfig.UpdateRequest(providerId)
+            .setDisplayName("NewDisplayName")
+            .setEnabled(false)
+            .setClientId("NewClientId")
+            .setIssuer("https://oidc.com/new-issuer");
+    config = auth.updateOidcProviderConfigAsync(updateRequest).get();
+    assertEquals(providerId, config.getProviderId());
+    assertEquals("NewDisplayName", config.getDisplayName());
+    assertFalse(config.isEnabled());
+    assertEquals("NewClientId", config.getClientId());
+    assertEquals("https://oidc.com/new-issuer", config.getIssuer());
+
+    // Delete provider config
+    temporaryProviderConfig.deleteOidcProviderConfig(providerId);
+    ProviderConfigTestUtils.assertOidcProviderConfigDoesNotExist(auth, providerId);
+  }
+
+  @Test
+  public void testListOidcProviderConfigs() throws Exception {
+    final List<String> providerIds = new ArrayList<>();
+
+    // Create provider configs
+    for (int i = 0; i < 3; i++) {
+      String providerId = "oidc.provider-id" + i;
+      providerIds.add(providerId);
+      temporaryProviderConfig.createOidcProviderConfig(
+          new OidcProviderConfig.CreateRequest()
+            .setProviderId(providerId)
+            .setClientId("CLIENT_ID")
+            .setIssuer("https://oidc.com/issuer"));
+    }
+
+    // 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 (checkOidcProviderConfig(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 (checkOidcProviderConfig(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 (checkOidcProviderConfig(providerIds, providerConfig)) {
+                collected.incrementAndGet();
+              }
+            }
+            semaphore.release();
+          }
+        }, MoreExecutors.directExecutor());
+    semaphore.acquire();
+    assertEquals(providerIds.size(), collected.get());
+    assertNull(error.get());
+  }
+
+  @Test
+  public void testSamlProviderConfigLifecycle() throws Exception {
+    // Create provider config
+    String providerId = "saml.provider-id";
+    SamlProviderConfig config = temporaryProviderConfig.createSamlProviderConfig(
+        new SamlProviderConfig.CreateRequest()
+            .setProviderId(providerId)
+            .setDisplayName("DisplayName")
+            .setEnabled(true)
+            .setIdpEntityId("IDP_ENTITY_ID")
+            .setSsoUrl("https://example.com/login")
+            .addX509Certificate("certificate1")
+            .addX509Certificate("certificate2")
+            .setRpEntityId("RP_ENTITY_ID")
+            .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"));
+    assertEquals(providerId, config.getProviderId());
+    assertEquals("DisplayName", config.getDisplayName());
+    assertTrue(config.isEnabled());
+    assertEquals("IDP_ENTITY_ID", config.getIdpEntityId());
+    assertEquals("https://example.com/login", config.getSsoUrl());
+    assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates());
+    assertEquals("RP_ENTITY_ID", config.getRpEntityId());
+    assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl());
+
+    config = auth.getSamlProviderConfig(providerId);
+    assertEquals(providerId, config.getProviderId());
+    assertEquals("DisplayName", config.getDisplayName());
+    assertTrue(config.isEnabled());
+    assertEquals("IDP_ENTITY_ID", config.getIdpEntityId());
+    assertEquals("https://example.com/login", config.getSsoUrl());
+    assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates());
+    assertEquals("RP_ENTITY_ID", config.getRpEntityId());
+    assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl());
+
+    // Update provider config
+    SamlProviderConfig.UpdateRequest updateRequest =
+        new SamlProviderConfig.UpdateRequest(providerId)
+            .setDisplayName("NewDisplayName")
+            .setEnabled(false)
+            .addX509Certificate("certificate");
+    config = auth.updateSamlProviderConfigAsync(updateRequest).get();
+    assertEquals(providerId, config.getProviderId());
+    assertEquals("NewDisplayName", config.getDisplayName());
+    assertFalse(config.isEnabled());
+    assertEquals(ImmutableList.of("certificate"), config.getX509Certificates());
+
+    // Delete provider config
+    temporaryProviderConfig.deleteSamlProviderConfig(providerId);
+    ProviderConfigTestUtils.assertSamlProviderConfigDoesNotExist(auth, providerId);
+  }
+
+  @Test
+  public void testListSamlProviderConfigs() throws Exception {
+    final List<String> providerIds = new ArrayList<>();
+
+    // Create provider configs
+    for (int i = 0; i < 3; i++) {
+      String providerId = "saml.provider-id" + i;
+      providerIds.add(providerId);
+      temporaryProviderConfig.createSamlProviderConfig(
+          new SamlProviderConfig.CreateRequest()
+            .setProviderId(providerId)
+            .setIdpEntityId("IDP_ENTITY_ID")
+            .setSsoUrl("https://example.com/login")
+            .addX509Certificate("certificate")
+            .setRpEntityId("RP_ENTITY_ID")
+            .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"));
+    }
+
+    // Test list by batches
+    final AtomicInteger collected = new AtomicInteger(0);
+    ListProviderConfigsPage<SamlProviderConfig> page =
+        auth.listSamlProviderConfigsAsync(null).get();
+    while (page != null) {
+      for (SamlProviderConfig providerConfig : page.getValues()) {
+        if (checkSamlProviderConfig(providerIds, providerConfig)) {
+          collected.incrementAndGet();
+        }
+      }
+      page = page.getNextPage();
+    }
+    assertEquals(providerIds.size(), collected.get());
+
+    // Test iterate all
+    collected.set(0);
+    page = auth.listSamlProviderConfigsAsync(null).get();
+    for (SamlProviderConfig providerConfig : page.iterateAll()) {
+      if (checkSamlProviderConfig(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<SamlProviderConfig>> pageFuture =
+        auth.listSamlProviderConfigsAsync(null);
+    ApiFutures.addCallback(
+        pageFuture,
+        new ApiFutureCallback<ListProviderConfigsPage<SamlProviderConfig>>() {
+          @Override
+          public void onFailure(Throwable t) {
+            error.set(t);
+            semaphore.release();
+          }
+
+          @Override
+          public void onSuccess(ListProviderConfigsPage<SamlProviderConfig> result) {
+            for (SamlProviderConfig providerConfig : result.iterateAll()) {
+              if (checkSamlProviderConfig(providerIds, providerConfig)) {
+                collected.incrementAndGet();
+              }
+            }
+            semaphore.release();
+          }
+        }, MoreExecutors.directExecutor());
+    semaphore.acquire();
+    assertEquals(providerIds.size(), collected.get());
+    assertNull(error.get());
   }
 
   private Map<String, String> parseLinkParameters(String link) throws Exception {
@@ -1075,15 +911,6 @@ public class FirebaseAuthIT {
     return result;
   }
 
-  static String randomPhoneNumber() {
-    Random random = new Random();
-    StringBuilder builder = new StringBuilder("+1");
-    for (int i = 0; i < 10; i++) {
-      builder.append(random.nextInt(10));
-    }
-    return builder.toString();
-  }
-
   private String signInWithCustomToken(String customToken) throws IOException {
     return signInWithCustomToken(customToken, null);
   }
@@ -1173,28 +1000,32 @@ public class FirebaseAuthIT {
     }
   }
 
-  static class RandomUser {
-    final String uid;
-    final String email;
-
-    private RandomUser(String uid, String email) {
-      this.uid = uid;
-      this.email = email;
+  private boolean checkOidcProviderConfig(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;
+  }
 
-    static RandomUser create() {
-      final String uid = UUID.randomUUID().toString().replaceAll("-", "");
-      final String email = ("test" + uid.substring(0, 12) + "@example."
-          + uid.substring(12) + ".com").toLowerCase();
-      return new RandomUser(uid, email);
+  private boolean checkSamlProviderConfig(List<String> providerIds, SamlProviderConfig config) {
+    if (providerIds.contains(config.getProviderId())) {
+      assertEquals("IDP_ENTITY_ID", config.getIdpEntityId());
+      assertEquals("https://example.com/login", config.getSsoUrl());
+      assertEquals(ImmutableList.of("certificate"), config.getX509Certificates());
+      assertEquals("RP_ENTITY_ID", config.getRpEntityId());
+      assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl());
+      return true;
     }
+    return false;
   }
 
   private static void assertUserDoesNotExist(AbstractFirebaseAuth firebaseAuth, String uid)
       throws Exception {
     try {
       firebaseAuth.getUserAsync(uid).get();
-      fail("No error thrown for getting a user which was expected to be absent");
+      fail("No error thrown for getting a user which was expected to be absent.");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
       assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
@@ -1209,13 +1040,14 @@ public class FirebaseAuthIT {
   static UserRecord newUserWithParams(FirebaseAuth auth) throws Exception {
     // TODO(rsgowman): This function could be used throughout this file (similar to the other
     // ports).
-    RandomUser randomUser = RandomUser.create();
+    RandomUser randomUser = UserTestUtils.generateRandomUserInfo();
     return auth.createUser(new UserRecord.CreateRequest()
-                               .setUid(randomUser.uid)
-                               .setEmail(randomUser.email)
-                               .setPhoneNumber(randomPhoneNumber())
+                               .setUid(randomUser.getUid())
+                               .setEmail(randomUser.getEmail())
+                               .setPhoneNumber(randomUser.getPhoneNumber())
                                .setDisplayName("Random User")
                                .setPhotoUrl("https://example.com/photo.png")
                                .setPassword("password"));
   }
 }
+
diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
index ac8860c..baff2e6 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
@@ -68,6 +68,7 @@ public class FirebaseUserManagerTest {
   private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory();
   private static final String TEST_TOKEN = "token";
   private static final GoogleCredentials credentials = new MockGoogleCredentials(TEST_TOKEN);
+
   private static final ActionCodeSettings ACTION_CODE_SETTINGS = ActionCodeSettings.builder()
           .setUrl("https://example.dynamic.link")
           .setHandleCodeInApp(true)
@@ -79,8 +80,10 @@ public class FirebaseUserManagerTest {
           .build();
   private static final Map<String, Object> ACTION_CODE_SETTINGS_MAP =
           ACTION_CODE_SETTINGS.getProperties();
-  private static final String TENANTS_BASE_URL =
-          "https://identitytoolkit.googleapis.com/v2/projects/test-project-id/tenants";
+
+  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";
 
   @After
   public void tearDown() {
@@ -1608,6 +1611,896 @@ public class FirebaseUserManagerTest {
     }
   }
 
+  @Test
+  public void testCreateOidcProvider() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("oidc.json"));
+    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 + "/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"));
+  }
+
+  @Test
+  public void testCreateOidcProviderMinimal() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("oidc.json"));
+    // 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")
+            .setClientId("CLIENT_ID")
+            .setIssuer("https://oidc.com/issuer");
+
+    FirebaseAuth.getInstance().createOidcProviderConfig(createRequest);
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs");
+    GenericJson parsed = parseRequestContent(interceptor);
+    assertNull(parsed.get("displayName"));
+    assertNull(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"));
+  }
+
+  @Test
+  public void testCreateOidcProviderError() throws Exception {
+    TestResponseInterceptor interceptor =
+        initializeAppForUserManagementWithStatusCode(404,
+            "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
+    OidcProviderConfig.CreateRequest createRequest =
+        new OidcProviderConfig.CreateRequest().setProviderId("oidc.provider-id");
+    try {
+      FirebaseAuth.getInstance().createOidcProviderConfig(createRequest);
+      fail("No error thrown for invalid response");
+    } catch (FirebaseAuthException e) {
+      assertEquals(FirebaseUserManager.INTERNAL_ERROR, e.getErrorCode());
+    }
+    checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs");
+  }
+
+  @Test
+  public void testCreateOidcProviderMissingId() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("oidc.json"));
+    OidcProviderConfig.CreateRequest createRequest =
+        new OidcProviderConfig.CreateRequest()
+            .setDisplayName("DISPLAY_NAME")
+            .setEnabled(true)
+            .setClientId("CLIENT_ID")
+            .setIssuer("https://oidc.com/issuer");
+    try {
+      FirebaseAuth.getInstance().createOidcProviderConfig(createRequest);
+      fail("No error thrown for invalid response");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void testTenantAwareCreateOidcProvider() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement(
+        "TENANT_ID",
+        TestUtils.loadResource("oidc.json"));
+    OidcProviderConfig.CreateRequest createRequest =
+        new OidcProviderConfig.CreateRequest()
+            .setProviderId("oidc.provider-id")
+            .setDisplayName("DISPLAY_NAME")
+            .setEnabled(true)
+            .setClientId("CLIENT_ID")
+            .setIssuer("https://oidc.com/issuer");
+    TenantAwareFirebaseAuth tenantAwareAuth =
+        FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID");
+
+    OidcProviderConfig config = tenantAwareAuth.createOidcProviderConfig(createRequest);
+
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "POST", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs");
+  }
+
+  @Test
+  public void testUpdateOidcProvider() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("oidc.json"));
+    OidcProviderConfig.UpdateRequest request =
+        new OidcProviderConfig.UpdateRequest("oidc.provider-id")
+            .setDisplayName("DISPLAY_NAME")
+            .setEnabled(true)
+            .setClientId("CLIENT_ID")
+            .setIssuer("https://oidc.com/issuer");
+
+    OidcProviderConfig config = FirebaseAuth.getInstance().updateOidcProviderConfig(request);
+
+    checkOidcProviderConfig(config, "oidc.provider-id");
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id");
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals("clientId,displayName,enabled,issuer", 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("https://oidc.com/issuer", parsed.get("issuer"));
+  }
+
+  @Test
+  public void testUpdateOidcProviderMinimal() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("oidc.json"));
+    OidcProviderConfig.UpdateRequest request =
+        new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName("DISPLAY_NAME");
+
+    OidcProviderConfig config = FirebaseAuth.getInstance().updateOidcProviderConfig(request);
+
+    checkOidcProviderConfig(config, "oidc.provider-id");
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id");
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals("displayName", url.getFirst("updateMask"));
+    GenericJson parsed = parseRequestContent(interceptor);
+    assertEquals(1, parsed.size());
+    assertEquals("DISPLAY_NAME", parsed.get("displayName"));
+  }
+
+  @Test
+  public void testUpdateOidcProviderConfigNoValues() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("oidc.json"));
+    try {
+      FirebaseAuth.getInstance().updateOidcProviderConfig(
+          new OidcProviderConfig.UpdateRequest("oidc.provider-id"));
+      fail("No error thrown for empty provider config update");
+    } catch (IllegalArgumentException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testUpdateOidcProviderConfigError() throws Exception {
+    TestResponseInterceptor interceptor =
+        initializeAppForUserManagementWithStatusCode(404,
+            "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
+    OidcProviderConfig.UpdateRequest request =
+        new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName("DISPLAY_NAME");
+    try {
+      FirebaseAuth.getInstance().updateOidcProviderConfig(request);
+      fail("No error thrown for invalid response");
+    } catch (FirebaseAuthException e) {
+      assertEquals(FirebaseUserManager.INTERNAL_ERROR, e.getErrorCode());
+    }
+    checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id");
+  }
+
+  @Test
+  public void testTenantAwareUpdateOidcProvider() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement(
+        "TENANT_ID",
+        TestUtils.loadResource("oidc.json"));
+    TenantAwareFirebaseAuth tenantAwareAuth =
+        FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID");
+    OidcProviderConfig.UpdateRequest request =
+        new OidcProviderConfig.UpdateRequest("oidc.provider-id")
+            .setDisplayName("DISPLAY_NAME")
+            .setEnabled(true)
+            .setClientId("CLIENT_ID")
+            .setIssuer("https://oidc.com/issuer");
+
+    OidcProviderConfig config = tenantAwareAuth.updateOidcProviderConfig(request);
+
+    checkOidcProviderConfig(config, "oidc.provider-id");
+    checkRequestHeaders(interceptor);
+    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"));
+    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"));
+  }
+
+  @Test
+  public void testGetOidcProviderConfig() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("oidc.json"));
+
+    OidcProviderConfig config =
+        FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id");
+
+    checkOidcProviderConfig(config, "oidc.provider-id");
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id");
+  }
+
+  @Test
+  public void testGetOidcProviderConfigMissingId() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("oidc.json"));
+
+    try {
+      FirebaseAuth.getInstance().getOidcProviderConfig(null);
+      fail("No error thrown for missing provider ID.");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void testGetOidcProviderConfigInvalidId() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("oidc.json"));
+
+    try {
+      FirebaseAuth.getInstance().getOidcProviderConfig("saml.invalid-oidc-provider-id");
+      fail("No error thrown for invalid provider ID.");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void testGetOidcProviderConfigWithNotFoundError() throws Exception {
+    TestResponseInterceptor interceptor =
+        initializeAppForUserManagementWithStatusCode(404,
+            "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}");
+    try {
+      FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id");
+      fail("No error thrown for invalid response");
+    } catch (FirebaseAuthException e) {
+      assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode());
+    }
+    checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id");
+  }
+
+  @Test
+  public void testGetTenantAwareOidcProviderConfig() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement(
+        "TENANT_ID",
+        TestUtils.loadResource("oidc.json"));
+    TenantAwareFirebaseAuth tenantAwareAuth =
+        FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID");
+
+    OidcProviderConfig config = tenantAwareAuth.getOidcProviderConfig("oidc.provider-id");
+
+    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 testDeleteOidcProviderConfig() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
+
+    FirebaseAuth.getInstance().deleteOidcProviderConfig("oidc.provider-id");
+
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id");
+  }
+
+  @Test
+  public void testDeleteOidcProviderMissingId() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
+
+    try {
+      FirebaseAuth.getInstance().deleteOidcProviderConfig(null);
+      fail("No error thrown for missing provider ID.");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void testDeleteOidcProviderInvalidId() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
+
+    try {
+      FirebaseAuth.getInstance().deleteOidcProviderConfig("saml.invalid-oidc-provider-id");
+      fail("No error thrown for invalid provider ID.");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void testDeleteOidcProviderConfigWithNotFoundError() throws Exception {
+    TestResponseInterceptor interceptor =
+        initializeAppForUserManagementWithStatusCode(404,
+            "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}");
+    try {
+      FirebaseAuth.getInstance().deleteOidcProviderConfig("oidc.UNKNOWN");
+      fail("No error thrown for invalid response");
+    } catch (FirebaseAuthException e) {
+      assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode());
+    }
+    checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.UNKNOWN");
+  }
+
+  @Test
+  public void testTenantAwareDeleteOidcProviderConfig() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement(
+        "TENANT_ID",
+        "{}");
+    TenantAwareFirebaseAuth tenantAwareAuth =
+        FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID");
+
+    tenantAwareAuth.deleteOidcProviderConfig("oidc.provider-id");
+
+    checkRequestHeaders(interceptor);
+    String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/oidc.provider-id";
+    checkUrl(interceptor, "DELETE", expectedUrl);
+  }
+
+  @Test
+  public void testCreateSamlProvider() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("saml.json"));
+    SamlProviderConfig.CreateRequest createRequest =
+        new SamlProviderConfig.CreateRequest()
+          .setProviderId("saml.provider-id")
+          .setDisplayName("DISPLAY_NAME")
+          .setEnabled(true)
+          .setIdpEntityId("IDP_ENTITY_ID")
+          .setSsoUrl("https://example.com/login")
+          .addX509Certificate("certificate1")
+          .addX509Certificate("certificate2")
+          .setRpEntityId("RP_ENTITY_ID")
+          .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler");
+
+    SamlProviderConfig config = FirebaseAuth.getInstance().createSamlProviderConfig(createRequest);
+
+    checkSamlProviderConfig(config, "saml.provider-id");
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs");
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals("saml.provider-id", url.getFirst("inboundSamlConfigId"));
+
+    GenericJson parsed = parseRequestContent(interceptor);
+    assertEquals("DISPLAY_NAME", parsed.get("displayName"));
+    assertTrue((boolean) parsed.get("enabled"));
+
+    Map<String, Object> idpConfig = (Map<String, Object>) parsed.get("idpConfig");
+    assertNotNull(idpConfig);
+    assertEquals(3, idpConfig.size());
+    assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId"));
+    assertEquals("https://example.com/login", idpConfig.get("ssoUrl"));
+    List<Object> idpCertificates = (List<Object>) idpConfig.get("idpCertificates");
+    assertNotNull(idpCertificates);
+    assertEquals(2, idpCertificates.size());
+    assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0));
+    assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1));
+
+    Map<String, Object> spConfig = (Map<String, Object>) parsed.get("spConfig");
+    assertNotNull(spConfig);
+    assertEquals(2, spConfig.size());
+    assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId"));
+    assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri"));
+  }
+
+  @Test
+  public void testCreateSamlProviderMinimal() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("saml.json"));
+    // Only the 'enabled', 'displayName', and 'signRequest' fields can be omitted from a SAML
+    // provider config creation request.
+    SamlProviderConfig.CreateRequest createRequest =
+        new SamlProviderConfig.CreateRequest()
+          .setProviderId("saml.provider-id")
+          .setIdpEntityId("IDP_ENTITY_ID")
+          .setSsoUrl("https://example.com/login")
+          .addX509Certificate("certificate")
+          .setRpEntityId("RP_ENTITY_ID")
+          .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler");
+
+    FirebaseAuth.getInstance().createSamlProviderConfig(createRequest);
+
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs");
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals("saml.provider-id", url.getFirst("inboundSamlConfigId"));
+
+    GenericJson parsed = parseRequestContent(interceptor);
+    assertNull(parsed.get("displayName"));
+    assertNull(parsed.get("enabled"));
+    Map<String, Object> idpConfig = (Map<String, Object>) parsed.get("idpConfig");
+    assertNotNull(idpConfig);
+    assertEquals(3, idpConfig.size());
+    assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId"));
+    assertEquals("https://example.com/login", idpConfig.get("ssoUrl"));
+    List<Object> idpCertificates = (List<Object>) idpConfig.get("idpCertificates");
+    assertNotNull(idpCertificates);
+    assertEquals(1, idpCertificates.size());
+    assertEquals(ImmutableMap.of("x509Certificate", "certificate"), idpCertificates.get(0));
+    Map<String, Object> spConfig = (Map<String, Object>) parsed.get("spConfig");
+    assertNotNull(spConfig);
+    assertEquals(2, spConfig.size());
+    assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId"));
+    assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri"));
+  }
+
+  @Test
+  public void testCreateSamlProviderError() throws Exception {
+    TestResponseInterceptor interceptor =
+        initializeAppForUserManagementWithStatusCode(404,
+            "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
+    SamlProviderConfig.CreateRequest createRequest =
+        new SamlProviderConfig.CreateRequest().setProviderId("saml.provider-id");
+    try {
+      FirebaseAuth.getInstance().createSamlProviderConfig(createRequest);
+      fail("No error thrown for invalid response");
+    } catch (FirebaseAuthException e) {
+      assertEquals(FirebaseUserManager.INTERNAL_ERROR, e.getErrorCode());
+    }
+    checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs");
+  }
+
+  @Test
+  public void testCreateSamlProviderMissingId() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("saml.json"));
+    SamlProviderConfig.CreateRequest createRequest =
+        new SamlProviderConfig.CreateRequest()
+          .setDisplayName("DISPLAY_NAME")
+          .setEnabled(true)
+          .setIdpEntityId("IDP_ENTITY_ID")
+          .setSsoUrl("https://example.com/login")
+          .addX509Certificate("certificate1")
+          .addX509Certificate("certificate2")
+          .setRpEntityId("RP_ENTITY_ID")
+          .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler");
+    try {
+      FirebaseAuth.getInstance().createSamlProviderConfig(createRequest);
+      fail("No error thrown for invalid response");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void testTenantAwareCreateSamlProvider() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement(
+        "TENANT_ID",
+        TestUtils.loadResource("saml.json"));
+    SamlProviderConfig.CreateRequest createRequest =
+        new SamlProviderConfig.CreateRequest()
+          .setProviderId("saml.provider-id")
+          .setDisplayName("DISPLAY_NAME")
+          .setEnabled(true)
+          .setIdpEntityId("IDP_ENTITY_ID")
+          .setSsoUrl("https://example.com/login")
+          .addX509Certificate("certificate1")
+          .addX509Certificate("certificate2")
+          .setRpEntityId("RP_ENTITY_ID")
+          .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler");
+    TenantAwareFirebaseAuth tenantAwareAuth =
+        FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID");
+
+    tenantAwareAuth.createSamlProviderConfig(createRequest);
+
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "POST", TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs");
+  }
+
+  @Test
+  public void testUpdateSamlProvider() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("saml.json"));
+    SamlProviderConfig.UpdateRequest updateRequest =
+        new SamlProviderConfig.UpdateRequest("saml.provider-id")
+          .setDisplayName("DISPLAY_NAME")
+          .setEnabled(true)
+          .setIdpEntityId("IDP_ENTITY_ID")
+          .setSsoUrl("https://example.com/login")
+          .addX509Certificate("certificate1")
+          .addX509Certificate("certificate2")
+          .setRpEntityId("RP_ENTITY_ID")
+          .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler");
+
+    SamlProviderConfig config = FirebaseAuth.getInstance().updateSamlProviderConfig(updateRequest);
+
+    checkSamlProviderConfig(config, "saml.provider-id");
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id");
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals(
+        "displayName,enabled,idpConfig.idpCertificates,idpConfig.idpEntityId,idpConfig.ssoUrl,"
+          + "spConfig.callbackUri,spConfig.spEntityId",
+        url.getFirst("updateMask"));
+
+    GenericJson parsed = parseRequestContent(interceptor);
+    assertEquals("DISPLAY_NAME", parsed.get("displayName"));
+    assertTrue((boolean) parsed.get("enabled"));
+
+    Map<String, Object> idpConfig = (Map<String, Object>) parsed.get("idpConfig");
+    assertNotNull(idpConfig);
+    assertEquals(3, idpConfig.size());
+    assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId"));
+    assertEquals("https://example.com/login", idpConfig.get("ssoUrl"));
+    List<Object> idpCertificates = (List<Object>) idpConfig.get("idpCertificates");
+    assertNotNull(idpCertificates);
+    assertEquals(2, idpCertificates.size());
+    assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0));
+    assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1));
+
+    Map<String, Object> spConfig = (Map<String, Object>) parsed.get("spConfig");
+    assertNotNull(spConfig);
+    assertEquals(2, spConfig.size());
+    assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId"));
+    assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri"));
+  }
+
+  @Test
+  public void testUpdateSamlProviderMinimal() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("saml.json"));
+    SamlProviderConfig.UpdateRequest request =
+        new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName("DISPLAY_NAME");
+
+    SamlProviderConfig config = FirebaseAuth.getInstance().updateSamlProviderConfig(request);
+
+    checkSamlProviderConfig(config, "saml.provider-id");
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id");
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals("displayName", url.getFirst("updateMask"));
+    GenericJson parsed = parseRequestContent(interceptor);
+    assertEquals(1, parsed.size());
+    assertEquals("DISPLAY_NAME", parsed.get("displayName"));
+  }
+
+  @Test
+  public void testUpdateSamlProviderConfigNoValues() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("saml.json"));
+    try {
+      FirebaseAuth.getInstance().updateSamlProviderConfig(
+          new SamlProviderConfig.UpdateRequest("saml.provider-id"));
+      fail("No error thrown for empty provider config update");
+    } catch (IllegalArgumentException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testUpdateSamlProviderConfigError() throws Exception {
+    TestResponseInterceptor interceptor =
+        initializeAppForUserManagementWithStatusCode(404,
+            "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}");
+    SamlProviderConfig.UpdateRequest request =
+        new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName("DISPLAY_NAME");
+    try {
+      FirebaseAuth.getInstance().updateSamlProviderConfig(request);
+      fail("No error thrown for invalid response");
+    } catch (FirebaseAuthException e) {
+      assertEquals(FirebaseUserManager.INTERNAL_ERROR, e.getErrorCode());
+    }
+    checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id");
+  }
+
+  @Test
+  public void testTenantAwareUpdateSamlProvider() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement(
+        "TENANT_ID",
+        TestUtils.loadResource("saml.json"));
+    TenantAwareFirebaseAuth tenantAwareAuth =
+        FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID");
+    SamlProviderConfig.UpdateRequest updateRequest =
+        new SamlProviderConfig.UpdateRequest("saml.provider-id")
+          .setDisplayName("DISPLAY_NAME")
+          .setEnabled(true)
+          .setIdpEntityId("IDP_ENTITY_ID")
+          .setSsoUrl("https://example.com/login");
+
+    SamlProviderConfig config = tenantAwareAuth.updateSamlProviderConfig(updateRequest);
+
+    checkSamlProviderConfig(config, "saml.provider-id");
+    checkRequestHeaders(interceptor);
+    String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs/saml.provider-id";
+    checkUrl(interceptor, "PATCH", expectedUrl);
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals("displayName,enabled,idpConfig.idpEntityId,idpConfig.ssoUrl",
+        url.getFirst("updateMask"));
+
+    GenericJson parsed = parseRequestContent(interceptor);
+    assertEquals("DISPLAY_NAME", parsed.get("displayName"));
+    assertTrue((boolean) parsed.get("enabled"));
+    Map<String, Object> idpConfig = (Map<String, Object>) parsed.get("idpConfig");
+    assertNotNull(idpConfig);
+    assertEquals(2, idpConfig.size());
+    assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId"));
+    assertEquals("https://example.com/login", idpConfig.get("ssoUrl"));
+  }
+
+  @Test
+  public void testGetSamlProviderConfig() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("saml.json"));
+
+    SamlProviderConfig config =
+        FirebaseAuth.getInstance().getSamlProviderConfig("saml.provider-id");
+
+    checkSamlProviderConfig(config, "saml.provider-id");
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id");
+  }
+
+  @Test
+  public void testGetSamlProviderConfigMissingId() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("saml.json"));
+
+    try {
+      FirebaseAuth.getInstance().getSamlProviderConfig(null);
+      fail("No error thrown for missing provider ID.");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void testGetSamlProviderConfigInvalidId() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("saml.json"));
+
+    try {
+      FirebaseAuth.getInstance().getSamlProviderConfig("oidc.invalid-saml-provider-id");
+      fail("No error thrown for invalid provider ID.");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void testGetSamlProviderConfigWithNotFoundError() throws Exception {
+    TestResponseInterceptor interceptor =
+        initializeAppForUserManagementWithStatusCode(404,
+            "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}");
+    try {
+      FirebaseAuth.getInstance().getSamlProviderConfig("saml.provider-id");
+      fail("No error thrown for invalid response");
+    } catch (FirebaseAuthException e) {
+      assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode());
+    }
+    checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id");
+  }
+
+  @Test
+  public void testGetTenantAwareSamlProviderConfig() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement(
+        "TENANT_ID",
+        TestUtils.loadResource("saml.json"));
+    TenantAwareFirebaseAuth tenantAwareAuth =
+        FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID");
+
+    SamlProviderConfig config = tenantAwareAuth.getSamlProviderConfig("saml.provider-id");
+
+    checkSamlProviderConfig(config, "saml.provider-id");
+    checkRequestHeaders(interceptor);
+    String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs/saml.provider-id";
+    checkUrl(interceptor, "GET", expectedUrl);
+  }
+
+  @Test
+  public void testListSamlProviderConfigs() throws Exception {
+    final TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("listSaml.json"));
+    ListProviderConfigsPage<SamlProviderConfig> page =
+        FirebaseAuth.getInstance().listSamlProviderConfigsAsync(null, 99).get();
+
+    ImmutableList<SamlProviderConfig> providerConfigs = ImmutableList.copyOf(page.getValues());
+    assertEquals(2, providerConfigs.size());
+    checkSamlProviderConfig(providerConfigs.get(0), "saml.provider-id1");
+    checkSamlProviderConfig(providerConfigs.get(1), "saml.provider-id2");
+    assertEquals("", page.getNextPageToken());
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs");
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals(99, url.getFirst("pageSize"));
+    assertNull(url.getFirst("nextPageToken"));
+  }
+
+  @Test
+  public void testListSamlProviderConfigsWithPageToken() throws Exception {
+    final TestResponseInterceptor interceptor = initializeAppForUserManagement(
+        TestUtils.loadResource("listSaml.json"));
+    ListProviderConfigsPage<SamlProviderConfig> page =
+        FirebaseAuth.getInstance().listSamlProviderConfigsAsync("token", 99).get();
+
+    ImmutableList<SamlProviderConfig> providerConfigs = ImmutableList.copyOf(page.getValues());
+    assertEquals(2, providerConfigs.size());
+    checkSamlProviderConfig(providerConfigs.get(0), "saml.provider-id1");
+    checkSamlProviderConfig(providerConfigs.get(1), "saml.provider-id2");
+    assertEquals("", page.getNextPageToken());
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs");
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals(99, url.getFirst("pageSize"));
+    assertEquals("token", url.getFirst("nextPageToken"));
+  }
+
+  @Test
+  public void testListZeroSamlProviderConfigs() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
+    ListProviderConfigsPage<SamlProviderConfig> page =
+        FirebaseAuth.getInstance().listSamlProviderConfigsAsync(null).get();
+    assertTrue(Iterables.isEmpty(page.getValues()));
+    assertEquals("", page.getNextPageToken());
+    checkRequestHeaders(interceptor);
+  }
+
+  @Test
+  public void testTenantAwareListSamlProviderConfigs() throws Exception {
+    final TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement(
+        "TENANT_ID",
+        TestUtils.loadResource("listSaml.json"));
+    TenantAwareFirebaseAuth tenantAwareAuth =
+        FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID");
+    ListProviderConfigsPage<SamlProviderConfig> page =
+        tenantAwareAuth.listSamlProviderConfigsAsync(null, 99).get();
+
+    ImmutableList<SamlProviderConfig> providerConfigs = ImmutableList.copyOf(page.getValues());
+    assertEquals(2, providerConfigs.size());
+    checkSamlProviderConfig(providerConfigs.get(0), "saml.provider-id1");
+    checkSamlProviderConfig(providerConfigs.get(1), "saml.provider-id2");
+    assertEquals("", page.getNextPageToken());
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs");
+    GenericUrl url = interceptor.getResponse().getRequest().getUrl();
+    assertEquals(99, url.getFirst("pageSize"));
+    assertNull(url.getFirst("nextPageToken"));
+  }
+
+  @Test
+  public void testDeleteSamlProviderConfig() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
+
+    FirebaseAuth.getInstance().deleteSamlProviderConfig("saml.provider-id");
+
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id");
+  }
+
+  @Test
+  public void testDeleteSamlProviderMissingId() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
+
+    try {
+      FirebaseAuth.getInstance().deleteSamlProviderConfig(null);
+      fail("No error thrown for missing provider ID.");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void testDeleteSamlProviderInvalidId() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
+
+    try {
+      FirebaseAuth.getInstance().deleteSamlProviderConfig("oidc.invalid-saml-provider-id");
+      fail("No error thrown for invalid provider ID.");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void testDeleteSamlProviderConfigWithNotFoundError() throws Exception {
+    TestResponseInterceptor interceptor =
+        initializeAppForUserManagementWithStatusCode(404,
+            "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}");
+    try {
+      FirebaseAuth.getInstance().deleteSamlProviderConfig("saml.UNKNOWN");
+      fail("No error thrown for invalid response");
+    } catch (FirebaseAuthException e) {
+      assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode());
+    }
+    checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.UNKNOWN");
+  }
+
+  @Test
+  public void testTenantAwareDeleteSamlProviderConfig() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement(
+        "TENANT_ID",
+        "{}");
+    TenantAwareFirebaseAuth tenantAwareAuth =
+        FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID");
+
+    tenantAwareAuth.deleteSamlProviderConfig("saml.provider-id");
+
+    checkRequestHeaders(interceptor);
+    String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs/saml.provider-id";
+    checkUrl(interceptor, "DELETE", expectedUrl);
+  }
+
   private static TestResponseInterceptor initializeAppForUserManagementWithStatusCode(
       int statusCode, String response) {
     FirebaseApp.initializeApp(new FirebaseOptions.Builder()
@@ -1622,7 +2515,24 @@ public class FirebaseUserManagerTest {
     return interceptor;
   }
 
+  private static TestResponseInterceptor initializeAppForTenantAwareUserManagement(
+      String tenantId,
+      String... responses) {
+    initializeAppWithResponses(responses);
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    TenantManager tenantManager =  FirebaseAuth.getInstance().getTenantManager();
+    tenantManager.getAuthForTenant(tenantId).getUserManager().setInterceptor(interceptor);
+    return interceptor;
+  }
+
   private static TestResponseInterceptor initializeAppForUserManagement(String... responses) {
+    initializeAppWithResponses(responses);
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    FirebaseAuth.getInstance().getUserManager().setInterceptor(interceptor);
+    return interceptor;
+  }
+
+  private static void initializeAppWithResponses(String... responses) {
     List<MockLowLevelHttpResponse> mocks = new ArrayList<>();
     for (String response : responses) {
       mocks.add(new MockLowLevelHttpResponse().setContent(response));
@@ -1633,9 +2543,6 @@ public class FirebaseUserManagerTest {
         .setHttpTransport(transport)
         .setProjectId("test-project-id")
         .build());
-    TestResponseInterceptor interceptor = new TestResponseInterceptor();
-    FirebaseAuth.getInstance().getUserManager().setInterceptor(interceptor);
-    return interceptor;
   }
 
   private static GenericJson parseRequestContent(TestResponseInterceptor interceptor)
@@ -1710,6 +2617,25 @@ public class FirebaseUserManagerTest {
     assertFalse(tenant.isEmailLinkSignInEnabled());
   }
 
+  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());
+    assertEquals("https://oidc.com/issuer", config.getIssuer());
+  }
+
+  private static void checkSamlProviderConfig(SamlProviderConfig config, String providerId) {
+    assertEquals(providerId, config.getProviderId());
+    assertEquals("DISPLAY_NAME", config.getDisplayName());
+    assertTrue(config.isEnabled());
+    assertEquals("IDP_ENTITY_ID", config.getIdpEntityId());
+    assertEquals("https://example.com/login", config.getSsoUrl());
+    assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates());
+    assertEquals("RP_ENTITY_ID", config.getRpEntityId());
+    assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl());
+  }
+
   private static void checkRequestHeaders(TestResponseInterceptor interceptor) {
     HttpHeaders headers = interceptor.getResponse().getRequest().getHeaders();
     String auth = "Bearer " + TEST_TOKEN;
diff --git a/src/test/java/com/google/firebase/auth/GetUsersIT.java b/src/test/java/com/google/firebase/auth/GetUsersIT.java
index efe2f78..a0bdcb6 100644
--- a/src/test/java/com/google/firebase/auth/GetUsersIT.java
+++ b/src/test/java/com/google/firebase/auth/GetUsersIT.java
@@ -21,6 +21,7 @@ import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.ImmutableList;
 import com.google.firebase.FirebaseApp;
+import com.google.firebase.auth.UserTestUtils.RandomUser;
 import com.google.firebase.testing.IntegrationTestUtils;
 import java.util.Collection;
 
@@ -44,18 +45,17 @@ public class GetUsersIT {
     testUser2 = FirebaseAuthIT.newUserWithParams(auth);
     testUser3 = FirebaseAuthIT.newUserWithParams(auth);
 
-    FirebaseAuthIT.RandomUser randomUser = FirebaseAuthIT.RandomUser.create();
-    importUserUid = randomUser.uid;
-    String phone = FirebaseAuthIT.randomPhoneNumber();
+    RandomUser randomUser = UserTestUtils.generateRandomUserInfo();
+    importUserUid = randomUser.getUid();
     UserImportResult result = auth.importUsers(ImmutableList.of(
           ImportUserRecord.builder()
-          .setUid(randomUser.uid)
-          .setEmail(randomUser.email)
-          .setPhoneNumber(phone)
+          .setUid(randomUser.getUid())
+          .setEmail(randomUser.getEmail())
+          .setPhoneNumber(randomUser.getPhoneNumber())
           .addUserProvider(
               UserProvider.builder()
                   .setProviderId("google.com")
-                  .setUid("google_" + randomUser.uid)
+                  .setUid("google_" + randomUser.getUid())
                   .build())
           .build()
           ));
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/java/com/google/firebase/auth/OidcProviderConfigTest.java b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java
new file mode 100644
index 0000000..fa5c6d4
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.api.client.googleapis.util.Utils;
+import com.google.api.client.json.JsonFactory;
+import java.io.IOException;
+import java.util.Map;
+import org.junit.Test;
+
+public class OidcProviderConfigTest {
+
+  private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
+
+  private static final String OIDC_JSON_STRING =
+      ("{"
+        + "  'name':        'projects/projectId/oauthIdpConfigs/oidc.provider-id',"
+        + "  'displayName': 'DISPLAY_NAME',"
+        + "  'enabled':      true,"
+        + "  'clientId':    'CLIENT_ID',"
+        + "  'issuer':      'https://oidc.com/issuer'"
+        + "}").replace("'", "\"");
+
+  @Test
+  public void testJsonDeserialization() throws IOException {
+    OidcProviderConfig config = jsonFactory.fromString(OIDC_JSON_STRING, OidcProviderConfig.class);
+
+    assertEquals("oidc.provider-id", config.getProviderId());
+    assertEquals("DISPLAY_NAME", config.getDisplayName());
+    assertTrue(config.isEnabled());
+    assertEquals("CLIENT_ID", config.getClientId());
+    assertEquals("https://oidc.com/issuer", config.getIssuer());
+  }
+
+  @Test
+  public void testCreateRequest() throws IOException {
+    OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest();
+    createRequest
+      .setProviderId("oidc.provider-id")
+      .setDisplayName("DISPLAY_NAME")
+      .setEnabled(false)
+      .setClientId("CLIENT_ID")
+      .setIssuer("https://oidc.com/issuer");
+
+    assertEquals("oidc.provider-id", createRequest.getProviderId());
+    Map<String,Object> properties = createRequest.getProperties();
+    assertEquals(properties.size(), 4);
+    assertEquals("DISPLAY_NAME", (String) properties.get("displayName"));
+    assertFalse((boolean) properties.get("enabled"));
+    assertEquals("CLIENT_ID", (String) properties.get("clientId"));
+    assertEquals("https://oidc.com/issuer", (String) properties.get("issuer"));
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestMissingProviderId() {
+    new OidcProviderConfig.CreateRequest().setProviderId(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestInvalidProviderId() {
+    new OidcProviderConfig.CreateRequest().setProviderId("saml.provider-id");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestMissingDisplayName() {
+    new OidcProviderConfig.CreateRequest().setDisplayName(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestMissingClientId() {
+    new OidcProviderConfig.CreateRequest().setClientId(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestMissingIssuer() {
+    new OidcProviderConfig.CreateRequest().setIssuer(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestInvalidIssuerUrl() {
+    new OidcProviderConfig.CreateRequest().setIssuer("not a valid url");
+  }
+
+  @Test
+  public void testUpdateRequestFromOidcProviderConfig() throws IOException {
+    OidcProviderConfig config = jsonFactory.fromString(OIDC_JSON_STRING, OidcProviderConfig.class);
+
+    OidcProviderConfig.UpdateRequest updateRequest = config.updateRequest();
+
+    assertEquals("oidc.provider-id", updateRequest.getProviderId());
+    assertTrue(updateRequest.getProperties().isEmpty());
+  }
+
+  @Test
+  public void testUpdateRequest() throws IOException {
+    OidcProviderConfig.UpdateRequest updateRequest =
+        new OidcProviderConfig.UpdateRequest("oidc.provider-id");
+    updateRequest
+      .setDisplayName("DISPLAY_NAME")
+      .setEnabled(false)
+      .setClientId("CLIENT_ID")
+      .setIssuer("https://oidc.com/issuer");
+
+    assertEquals("oidc.provider-id", updateRequest.getProviderId());
+    Map<String,Object> properties = updateRequest.getProperties();
+    assertEquals(properties.size(), 4);
+    assertEquals("DISPLAY_NAME", (String) properties.get("displayName"));
+    assertFalse((boolean) properties.get("enabled"));
+    assertEquals("CLIENT_ID", (String) properties.get("clientId"));
+    assertEquals("https://oidc.com/issuer", (String) properties.get("issuer"));
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestMissingProviderId() {
+    new OidcProviderConfig.UpdateRequest(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestInvalidProviderId() {
+    new OidcProviderConfig.UpdateRequest("saml.provider-id");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestMissingDisplayName() {
+    new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestMissingClientId() {
+    new OidcProviderConfig.UpdateRequest("oidc.provider-id").setClientId(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestMissingIssuer() {
+    new OidcProviderConfig.UpdateRequest("oidc.provider-id").setIssuer(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestInvalidIssuerUrl() {
+    new OidcProviderConfig.UpdateRequest("oidc.provider-id").setIssuer("not a valid url");
+  }
+}
diff --git a/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java
new file mode 100644
index 0000000..262c48f
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java
@@ -0,0 +1,122 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import org.junit.rules.ExternalResource;
+
+class ProviderConfigTestUtils {
+
+  static void assertOidcProviderConfigDoesNotExist(
+      AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception {
+    try {
+      firebaseAuth.getOidcProviderConfigAsync(providerId).get();
+      fail("No error thrown for getting a deleted OIDC provider config.");
+    } catch (ExecutionException e) {
+      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR,
+          ((FirebaseAuthException) e.getCause()).getErrorCode());
+    }
+  }
+
+  static void assertSamlProviderConfigDoesNotExist(
+      AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception {
+    try {
+      firebaseAuth.getSamlProviderConfigAsync(providerId).get();
+      fail("No error thrown for getting a deleted SAML provider config.");
+    } catch (ExecutionException e) {
+      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR,
+          ((FirebaseAuthException) e.getCause()).getErrorCode());
+    }
+  }
+
+  /**
+   * Creates temporary provider configs for testing, and deletes them at the end of each test case.
+   */
+  static final class TemporaryProviderConfig extends ExternalResource {
+
+    private final AbstractFirebaseAuth auth;
+    private final List<String> oidcIds = new ArrayList<>();
+    private final List<String> samlIds = new ArrayList<>();
+
+    TemporaryProviderConfig(AbstractFirebaseAuth auth) {
+      this.auth = auth;
+    }
+
+    synchronized OidcProviderConfig createOidcProviderConfig(
+        OidcProviderConfig.CreateRequest request) throws FirebaseAuthException {
+      OidcProviderConfig config = auth.createOidcProviderConfig(request);
+      oidcIds.add(config.getProviderId());
+      return config;
+    }
+
+    synchronized void deleteOidcProviderConfig(String providerId) throws FirebaseAuthException {
+      checkArgument(oidcIds.contains(providerId),
+          "Provider ID is not currently associated with a temporary OIDC provider config: "
+          + providerId);
+      auth.deleteOidcProviderConfig(providerId);
+      oidcIds.remove(providerId);
+    }
+
+    synchronized SamlProviderConfig createSamlProviderConfig(
+        SamlProviderConfig.CreateRequest request) throws FirebaseAuthException {
+      SamlProviderConfig config = auth.createSamlProviderConfig(request);
+      samlIds.add(config.getProviderId());
+      return config;
+    }
+
+    synchronized void deleteSamlProviderConfig(String providerId) throws FirebaseAuthException {
+      checkArgument(samlIds.contains(providerId),
+          "Provider ID is not currently associated with a temporary SAML provider config: "
+          + providerId);
+      auth.deleteSamlProviderConfig(providerId);
+      samlIds.remove(providerId);
+    }
+
+    @Override
+    protected synchronized void after() {
+      // Delete OIDC provider configs.
+      for (String id : oidcIds) {
+        try {
+          auth.deleteOidcProviderConfig(id);
+        } catch (Exception ignore) {
+          // Ignore
+        }
+      }
+      oidcIds.clear();
+
+      // Delete SAML provider configs.
+      for (String id : samlIds) {
+        try {
+          auth.deleteSamlProviderConfig(id);
+        } catch (Exception ignore) {
+          // Ignore
+        }
+      }
+      samlIds.clear();
+    }
+  }
+}
+
diff --git a/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java b/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java
new file mode 100644
index 0000000..1351752
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java
@@ -0,0 +1,323 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+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.collect.ImmutableMap;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+
+public class SamlProviderConfigTest {
+
+  private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
+
+  private static final String SAML_JSON_STRING =
+      ("{"
+        + "  'name':        'projects/projectId/inboundSamlConfigs/saml.provider-id',"
+        + "  'displayName': 'DISPLAY_NAME',"
+        + "  'enabled':      true,"
+        + "  'idpConfig': {"
+        + "    'idpEntityId': 'IDP_ENTITY_ID',"
+        + "    'ssoUrl':      'https://example.com/login',"
+        + "    'idpCertificates': ["
+        + "      { 'x509Certificate': 'certificate1' },"
+        + "      { 'x509Certificate': 'certificate2' }"
+        + "    ]"
+        + "  },"
+        + "  'spConfig': {"
+        + "    'spEntityId':  'RP_ENTITY_ID',"
+        + "    'callbackUri': 'https://projectId.firebaseapp.com/__/auth/handler'"
+        + "   }"
+        + "}").replace("'", "\"");
+
+  @Test
+  public void testJsonDeserialization() throws IOException {
+    SamlProviderConfig config = jsonFactory.fromString(SAML_JSON_STRING, SamlProviderConfig.class);
+
+    assertEquals("saml.provider-id", config.getProviderId());
+    assertEquals("DISPLAY_NAME", config.getDisplayName());
+    assertTrue(config.isEnabled());
+    assertEquals("IDP_ENTITY_ID", config.getIdpEntityId());
+    assertEquals("https://example.com/login", config.getSsoUrl());
+    assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates());
+    assertEquals("RP_ENTITY_ID", config.getRpEntityId());
+    assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl());
+  }
+
+  @Test
+  public void testCreateRequest() throws IOException {
+    SamlProviderConfig.CreateRequest createRequest =
+        new SamlProviderConfig.CreateRequest()
+          .setProviderId("saml.provider-id")
+          .setDisplayName("DISPLAY_NAME")
+          .setEnabled(false)
+          .setIdpEntityId("IDP_ENTITY_ID")
+          .setSsoUrl("https://example.com/login")
+          .addX509Certificate("certificate1")
+          .addX509Certificate("certificate2")
+          .setRpEntityId("RP_ENTITY_ID")
+          .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler");
+
+    assertEquals("saml.provider-id", createRequest.getProviderId());
+    Map<String,Object> properties = createRequest.getProperties();
+    assertEquals(4, properties.size());
+    assertEquals("DISPLAY_NAME", (String) properties.get("displayName"));
+    assertFalse((boolean) properties.get("enabled"));
+
+    Map<String, Object> idpConfig = (Map<String, Object>) properties.get("idpConfig");
+    assertNotNull(idpConfig);
+    assertEquals(3, idpConfig.size());
+    assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId"));
+    assertEquals("https://example.com/login", idpConfig.get("ssoUrl"));
+    List<Object> idpCertificates = (List<Object>) idpConfig.get("idpCertificates");
+    assertNotNull(idpCertificates);
+    assertEquals(2, idpCertificates.size());
+    assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0));
+    assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1));
+
+    Map<String, Object> spConfig = (Map<String, Object>) properties.get("spConfig");
+    assertNotNull(spConfig);
+    assertEquals(2, spConfig.size());
+    assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId"));
+    assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri"));
+  }
+
+  @Test
+  public void testCreateRequestX509Certificates() throws IOException {
+    SamlProviderConfig.CreateRequest createRequest =
+        new SamlProviderConfig.CreateRequest()
+          .addX509Certificate("certificate1")
+          .addAllX509Certificates(ImmutableList.of("certificate2", "certificate3"))
+          .addX509Certificate("certificate4");
+
+    Map<String,Object> properties = createRequest.getProperties();
+    assertEquals(1, properties.size());
+    Map<String, Object> idpConfig = (Map<String, Object>) properties.get("idpConfig");
+    assertNotNull(idpConfig);
+    assertEquals(1, idpConfig.size());
+
+    List<Object> idpCertificates = (List<Object>) idpConfig.get("idpCertificates");
+    assertNotNull(idpCertificates);
+    assertEquals(4, idpCertificates.size());
+    assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0));
+    assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1));
+    assertEquals(ImmutableMap.of("x509Certificate", "certificate3"), idpCertificates.get(2));
+    assertEquals(ImmutableMap.of("x509Certificate", "certificate4"), idpCertificates.get(3));
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestMissingProviderId() {
+    new SamlProviderConfig.CreateRequest().setProviderId(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestInvalidProviderId() {
+    new SamlProviderConfig.CreateRequest().setProviderId("oidc.provider-id");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestMissingDisplayName() {
+    new SamlProviderConfig.CreateRequest().setDisplayName(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestMissingIdpEntityId() {
+    new SamlProviderConfig.CreateRequest().setIdpEntityId(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestMissingSsoUrl() {
+    new SamlProviderConfig.CreateRequest().setSsoUrl(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestInvalidSsoUrl() {
+    new SamlProviderConfig.CreateRequest().setSsoUrl("not a valid url");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestMissingX509Certificate() {
+    new SamlProviderConfig.CreateRequest().addX509Certificate(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestNullX509CertificatesCollection() {
+    new SamlProviderConfig.CreateRequest().addAllX509Certificates(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestEmptyX509CertificatesCollection() {
+    new SamlProviderConfig.CreateRequest().addAllX509Certificates(ImmutableList.<String>of());
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestMissingRpEntityId() {
+    new SamlProviderConfig.CreateRequest().setRpEntityId(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestMissingCallbackUrl() {
+    new SamlProviderConfig.CreateRequest().setCallbackUrl(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testCreateRequestInvalidCallbackUrl() {
+    new SamlProviderConfig.CreateRequest().setCallbackUrl("not a valid url");
+  }
+
+  @Test
+  public void testUpdateRequestFromSamlProviderConfig() throws IOException {
+    SamlProviderConfig config = jsonFactory.fromString(SAML_JSON_STRING, SamlProviderConfig.class);
+
+    SamlProviderConfig.UpdateRequest updateRequest = config.updateRequest();
+
+    assertEquals("saml.provider-id", updateRequest.getProviderId());
+    assertTrue(updateRequest.getProperties().isEmpty());
+  }
+
+  @Test
+  public void testUpdateRequest() throws IOException {
+    SamlProviderConfig.UpdateRequest updateRequest =
+        new SamlProviderConfig.UpdateRequest("saml.provider-id");
+    updateRequest
+      .setDisplayName("DISPLAY_NAME")
+      .setEnabled(false)
+      .setIdpEntityId("IDP_ENTITY_ID")
+      .setSsoUrl("https://example.com/login")
+      .addX509Certificate("certificate1")
+      .addX509Certificate("certificate2")
+      .setRpEntityId("RP_ENTITY_ID")
+      .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler");
+
+    Map<String,Object> properties = updateRequest.getProperties();
+    assertEquals(4, properties.size());
+    assertEquals("DISPLAY_NAME", (String) properties.get("displayName"));
+    assertFalse((boolean) properties.get("enabled"));
+
+    Map<String, Object> idpConfig = (Map<String, Object>) properties.get("idpConfig");
+    assertNotNull(idpConfig);
+    assertEquals(3, idpConfig.size());
+    assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId"));
+    assertEquals("https://example.com/login", idpConfig.get("ssoUrl"));
+    List<Object> idpCertificates = (List<Object>) idpConfig.get("idpCertificates");
+    assertNotNull(idpCertificates);
+    assertEquals(2, idpCertificates.size());
+    assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0));
+    assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1));
+
+    Map<String, Object> spConfig = (Map<String, Object>) properties.get("spConfig");
+    assertNotNull(spConfig);
+    assertEquals(2, spConfig.size());
+    assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId"));
+    assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri"));
+  }
+
+  @Test
+  public void testUpdateRequestX509Certificates() throws IOException {
+    SamlProviderConfig.UpdateRequest updateRequest =
+        new SamlProviderConfig.UpdateRequest("saml.provider-id");
+    updateRequest
+      .addX509Certificate("certificate1")
+      .addAllX509Certificates(ImmutableList.of("certificate2", "certificate3"))
+      .addX509Certificate("certificate4");
+
+    Map<String,Object> properties = updateRequest.getProperties();
+    assertEquals(1, properties.size());
+    Map<String, Object> idpConfig = (Map<String, Object>) properties.get("idpConfig");
+    assertNotNull(idpConfig);
+    assertEquals(1, idpConfig.size());
+
+    List<Object> idpCertificates = (List<Object>) idpConfig.get("idpCertificates");
+    assertNotNull(idpCertificates);
+    assertEquals(4, idpCertificates.size());
+    assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0));
+    assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1));
+    assertEquals(ImmutableMap.of("x509Certificate", "certificate3"), idpCertificates.get(2));
+    assertEquals(ImmutableMap.of("x509Certificate", "certificate4"), idpCertificates.get(3));
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestMissingProviderId() {
+    new SamlProviderConfig.UpdateRequest(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestInvalidProviderId() {
+    new SamlProviderConfig.UpdateRequest("oidc.invalid-saml-provider-id");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestMissingDisplayName() {
+    new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestMissingIdpEntityId() {
+    new SamlProviderConfig.UpdateRequest("saml.provider-id").setIdpEntityId(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestMissingSsoUrl() {
+    new SamlProviderConfig.UpdateRequest("saml.provider-id").setSsoUrl(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestInvalidSsoUrl() {
+    new SamlProviderConfig.UpdateRequest("saml.provider-id").setSsoUrl("not a valid url");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestMissingX509Certificate() {
+    new SamlProviderConfig.UpdateRequest("saml.provider-id").addX509Certificate(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestNullX509CertificatesCollection() {
+    new SamlProviderConfig.UpdateRequest("saml.provider-id").addAllX509Certificates(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestEmptyX509CertificatesCollection() {
+    new SamlProviderConfig.UpdateRequest("saml.provider-id")
+        .addAllX509Certificates(ImmutableList.<String>of());
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestMissingRpEntityId() {
+    new SamlProviderConfig.UpdateRequest("saml.provider-id").setRpEntityId(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestMissingCallbackUrl() {
+    new SamlProviderConfig.UpdateRequest("saml.provider-id").setCallbackUrl(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestInvalidCallbackUrl() {
+    new SamlProviderConfig.UpdateRequest("saml.provider-id").setCallbackUrl("not a valid url");
+  }
+}
diff --git a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java
new file mode 100644
index 0000000..a6d83dc
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java
@@ -0,0 +1,440 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.api.client.googleapis.util.Utils;
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.http.json.JsonHttpContent;
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.JsonObjectParser;
+import com.google.api.core.ApiFuture;
+import com.google.api.core.ApiFutureCallback;
+import com.google.api.core.ApiFutures;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.auth.ProviderConfigTestUtils.TemporaryProviderConfig;
+import com.google.firebase.auth.UserTestUtils.RandomUser;
+import com.google.firebase.auth.UserTestUtils.TemporaryUser;
+import com.google.firebase.internal.Nullable;
+import com.google.firebase.testing.IntegrationTestUtils;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExternalResource;
+
+public class TenantAwareFirebaseAuthIT {
+
+  private static final String VERIFY_CUSTOM_TOKEN_URL =
+      "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken";
+  private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
+  private static final HttpTransport transport = Utils.getDefaultTransport();
+
+  private static TenantManager tenantManager;
+  private static TenantAwareFirebaseAuth tenantAwareAuth;
+  private static String tenantId;
+
+  @Rule public final TemporaryUser temporaryUser = new TemporaryUser(tenantAwareAuth);
+  @Rule public final TemporaryProviderConfig temporaryProviderConfig =
+      new TemporaryProviderConfig(tenantAwareAuth);
+
+  @BeforeClass
+  public static void setUpClass() throws Exception {
+    FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp();
+    tenantManager = FirebaseAuth.getInstance(masterApp).getTenantManager();
+    Tenant.CreateRequest tenantCreateRequest =
+        new Tenant.CreateRequest().setDisplayName("DisplayName");
+    tenantId = tenantManager.createTenant(tenantCreateRequest).getTenantId();
+    tenantAwareAuth = tenantManager.getAuthForTenant(tenantId);
+  }
+
+  @AfterClass
+  public static void tearDownClass() throws Exception {
+    tenantManager.deleteTenant(tenantId);
+  }
+
+  @Test
+  public void testUserLifecycle() throws Exception {
+    // Create user
+    UserRecord userRecord = temporaryUser.create(new UserRecord.CreateRequest());
+    String uid = userRecord.getUid();
+
+    // Get user
+    userRecord = tenantAwareAuth.getUserAsync(userRecord.getUid()).get();
+    assertEquals(uid, userRecord.getUid());
+    assertEquals(tenantId, userRecord.getTenantId());
+    assertNull(userRecord.getDisplayName());
+    assertNull(userRecord.getEmail());
+    assertNull(userRecord.getPhoneNumber());
+    assertNull(userRecord.getPhotoUrl());
+    assertFalse(userRecord.isEmailVerified());
+    assertFalse(userRecord.isDisabled());
+    assertTrue(userRecord.getUserMetadata().getCreationTimestamp() > 0);
+    assertEquals(0, userRecord.getUserMetadata().getLastSignInTimestamp());
+    assertEquals(0, userRecord.getProviderData().length);
+    assertTrue(userRecord.getCustomClaims().isEmpty());
+
+    // Update user
+    RandomUser randomUser = UserTestUtils.generateRandomUserInfo();
+    UserRecord.UpdateRequest request = userRecord.updateRequest()
+        .setDisplayName("Updated Name")
+        .setEmail(randomUser.getEmail())
+        .setPhoneNumber(randomUser.getPhoneNumber())
+        .setPhotoUrl("https://example.com/photo.png")
+        .setEmailVerified(true)
+        .setPassword("secret");
+    userRecord = tenantAwareAuth.updateUserAsync(request).get();
+    assertEquals(uid, userRecord.getUid());
+    assertEquals(tenantId, userRecord.getTenantId());
+    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(2, userRecord.getProviderData().length);
+    assertTrue(userRecord.getCustomClaims().isEmpty());
+
+    // Get user by email
+    userRecord = tenantAwareAuth.getUserByEmailAsync(userRecord.getEmail()).get();
+    assertEquals(uid, userRecord.getUid());
+
+    // Disable user and remove properties
+    request = userRecord.updateRequest()
+        .setPhotoUrl(null)
+        .setDisplayName(null)
+        .setPhoneNumber(null)
+        .setDisabled(true);
+    userRecord = tenantAwareAuth.updateUserAsync(request).get();
+    assertEquals(uid, userRecord.getUid());
+    assertEquals(tenantId, userRecord.getTenantId());
+    assertNull(userRecord.getDisplayName());
+    assertEquals(randomUser.getEmail(), userRecord.getEmail());
+    assertNull(userRecord.getPhoneNumber());
+    assertNull(userRecord.getPhotoUrl());
+    assertTrue(userRecord.isEmailVerified());
+    assertTrue(userRecord.isDisabled());
+    assertEquals(1, userRecord.getProviderData().length);
+    assertTrue(userRecord.getCustomClaims().isEmpty());
+
+    // Delete user
+    tenantAwareAuth.deleteUserAsync(userRecord.getUid()).get();
+    UserTestUtils.assertUserDoesNotExist(tenantAwareAuth, userRecord.getUid());
+  }
+
+  @Test
+  public void testListUsers() throws Exception {
+    final List<String> uids = new ArrayList<>();
+
+    for (int i = 0; i < 3; i++) {
+      UserRecord.CreateRequest createRequest =
+          new UserRecord.CreateRequest().setPassword("password");
+      uids.add(temporaryUser.create(createRequest).getUid());
+    }
+
+    // Test list by batches
+    final AtomicInteger collected = new AtomicInteger(0);
+    ListUsersPage page = tenantAwareAuth.listUsersAsync(null).get();
+    while (page != null) {
+      for (ExportedUserRecord user : page.getValues()) {
+        if (uids.contains(user.getUid())) {
+          collected.incrementAndGet();
+          assertNotNull("Missing passwordHash field. A common cause would be "
+              + "forgetting to add the \"Firebase Authentication Admin\" permission. See "
+              + "instructions in CONTRIBUTING.md", user.getPasswordHash());
+          assertNotNull(user.getPasswordSalt());
+          assertEquals(tenantId, user.getTenantId());
+        }
+      }
+      page = page.getNextPage();
+    }
+    assertEquals(uids.size(), collected.get());
+
+    // Test iterate all
+    collected.set(0);
+    page = tenantAwareAuth.listUsersAsync(null).get();
+    for (ExportedUserRecord user : page.iterateAll()) {
+      if (uids.contains(user.getUid())) {
+        collected.incrementAndGet();
+        assertNotNull(user.getPasswordHash());
+        assertNotNull(user.getPasswordSalt());
+        assertEquals(tenantId, user.getTenantId());
+      }
+    }
+    assertEquals(uids.size(), collected.get());
+
+    // Test iterate async
+    collected.set(0);
+    final Semaphore semaphore = new Semaphore(0);
+    final AtomicReference<Throwable> error = new AtomicReference<>();
+    ApiFuture<ListUsersPage> pageFuture = tenantAwareAuth.listUsersAsync(null);
+    ApiFutures.addCallback(pageFuture, new ApiFutureCallback<ListUsersPage>() {
+      @Override
+      public void onFailure(Throwable t) {
+        error.set(t);
+        semaphore.release();
+      }
+
+      @Override
+      public void onSuccess(ListUsersPage result) {
+        for (ExportedUserRecord user : result.iterateAll()) {
+          if (uids.contains(user.getUid())) {
+            collected.incrementAndGet();
+            assertNotNull(user.getPasswordHash());
+            assertNotNull(user.getPasswordSalt());
+            assertEquals(tenantId, user.getTenantId());
+          }
+        }
+        semaphore.release();
+      }
+    }, MoreExecutors.directExecutor());
+    semaphore.acquire();
+    assertEquals(uids.size(), collected.get());
+    assertNull(error.get());
+  }
+
+  @Test
+  public void testCustomToken() throws Exception {
+    String customToken = tenantAwareAuth.createCustomTokenAsync("user1").get();
+    String idToken = signInWithCustomToken(customToken, tenantId);
+    FirebaseToken decoded = tenantAwareAuth.verifyIdTokenAsync(idToken).get();
+    assertEquals("user1", decoded.getUid());
+    assertEquals(tenantId, decoded.getTenantId());
+  }
+
+  @Test
+  public void testVerifyTokenWithWrongTenantAwareClient() throws Exception {
+    String customToken = tenantAwareAuth.createCustomTokenAsync("user").get();
+    String idToken = signInWithCustomToken(customToken, tenantId);
+
+    try {
+      tenantManager.getAuthForTenant("OTHER").verifyIdTokenAsync(idToken).get();
+      fail("No error thrown for verifying a token with the wrong tenant-aware client");
+    } catch (ExecutionException e) {
+      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      assertEquals(FirebaseUserManager.TENANT_ID_MISMATCH_ERROR,
+          ((FirebaseAuthException) e.getCause()).getErrorCode());
+    }
+  }
+
+  @Test
+  public void testOidcProviderConfigLifecycle() throws Exception {
+    // Create provider config
+    String providerId = "oidc.provider-id";
+    OidcProviderConfig config =
+        temporaryProviderConfig.createOidcProviderConfig(
+          new OidcProviderConfig.CreateRequest()
+              .setProviderId(providerId)
+              .setDisplayName("DisplayName")
+              .setEnabled(true)
+              .setClientId("ClientId")
+              .setIssuer("https://oidc.com/issuer"));
+    assertEquals(providerId, config.getProviderId());
+    assertEquals("DisplayName", config.getDisplayName());
+    assertEquals("ClientId", config.getClientId());
+    assertEquals("https://oidc.com/issuer", config.getIssuer());
+
+    // Get provider config
+    config = tenantAwareAuth.getOidcProviderConfigAsync(providerId).get();
+    assertEquals(providerId, config.getProviderId());
+    assertEquals("DisplayName", config.getDisplayName());
+    assertEquals("ClientId", config.getClientId());
+    assertEquals("https://oidc.com/issuer", config.getIssuer());
+
+    // Update provider config
+    OidcProviderConfig.UpdateRequest updateRequest =
+        new OidcProviderConfig.UpdateRequest(providerId)
+            .setDisplayName("NewDisplayName")
+            .setEnabled(false)
+            .setClientId("NewClientId")
+            .setIssuer("https://oidc.com/new-issuer");
+    config = tenantAwareAuth.updateOidcProviderConfigAsync(updateRequest).get();
+    assertEquals(providerId, config.getProviderId());
+    assertEquals("NewDisplayName", config.getDisplayName());
+    assertFalse(config.isEnabled());
+    assertEquals("NewClientId", config.getClientId());
+    assertEquals("https://oidc.com/new-issuer", config.getIssuer());
+
+    // Delete provider config
+    temporaryProviderConfig.deleteOidcProviderConfig(providerId);
+    ProviderConfigTestUtils.assertOidcProviderConfigDoesNotExist(tenantAwareAuth, providerId);
+  }
+
+  @Test
+  public void testListOidcProviderConfigs() throws Exception {
+    final List<String> providerIds = new ArrayList<>();
+
+    // Create provider configs
+    for (int i = 0; i < 3; i++) {
+      String providerId = "oidc.provider-id" + i;
+      providerIds.add(providerId);
+      temporaryProviderConfig.createOidcProviderConfig(
+          new OidcProviderConfig.CreateRequest()
+            .setProviderId(providerId)
+            .setClientId("CLIENT_ID")
+            .setIssuer("https://oidc.com/issuer"));
+    }
+
+    // List provider configs
+    // NOTE: We do not need to test all of the different ways we can iterate over the provider
+    // configs, since this testing is already performed in FirebaseAuthIT with the tenant-agnostic
+    // tests.
+    final AtomicInteger collected = new AtomicInteger(0);
+    ListProviderConfigsPage<OidcProviderConfig> page =
+        tenantAwareAuth.listOidcProviderConfigsAsync(null).get();
+    for (OidcProviderConfig providerConfig : page.iterateAll()) {
+      if (providerIds.contains(providerConfig.getProviderId())) {
+        collected.incrementAndGet();
+        assertEquals("CLIENT_ID", providerConfig.getClientId());
+        assertEquals("https://oidc.com/issuer", providerConfig.getIssuer());
+      }
+    }
+    assertEquals(providerIds.size(), collected.get());
+  }
+
+  @Test
+  public void testSamlProviderConfigLifecycle() throws Exception {
+    // Create provider config
+    String providerId = "saml.provider-id";
+    SamlProviderConfig config = temporaryProviderConfig.createSamlProviderConfig(
+        new SamlProviderConfig.CreateRequest()
+            .setProviderId(providerId)
+            .setDisplayName("DisplayName")
+            .setEnabled(true)
+            .setIdpEntityId("IDP_ENTITY_ID")
+            .setSsoUrl("https://example.com/login")
+            .addX509Certificate("certificate1")
+            .addX509Certificate("certificate2")
+            .setRpEntityId("RP_ENTITY_ID")
+            .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"));
+    assertEquals(providerId, config.getProviderId());
+    assertEquals("DisplayName", config.getDisplayName());
+    assertTrue(config.isEnabled());
+    assertEquals("IDP_ENTITY_ID", config.getIdpEntityId());
+    assertEquals("https://example.com/login", config.getSsoUrl());
+    assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates());
+    assertEquals("RP_ENTITY_ID", config.getRpEntityId());
+    assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl());
+
+    config = tenantAwareAuth.getSamlProviderConfig(providerId);
+    assertEquals(providerId, config.getProviderId());
+    assertEquals("DisplayName", config.getDisplayName());
+    assertTrue(config.isEnabled());
+    assertEquals("IDP_ENTITY_ID", config.getIdpEntityId());
+    assertEquals("https://example.com/login", config.getSsoUrl());
+    assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates());
+    assertEquals("RP_ENTITY_ID", config.getRpEntityId());
+    assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl());
+
+    // Update provider config
+    SamlProviderConfig.UpdateRequest updateRequest =
+        new SamlProviderConfig.UpdateRequest(providerId)
+            .setDisplayName("NewDisplayName")
+            .setEnabled(false)
+            .addX509Certificate("certificate");
+    config = tenantAwareAuth.updateSamlProviderConfigAsync(updateRequest).get();
+    assertEquals(providerId, config.getProviderId());
+    assertEquals("NewDisplayName", config.getDisplayName());
+    assertFalse(config.isEnabled());
+    assertEquals(ImmutableList.of("certificate"), config.getX509Certificates());
+
+    // Delete provider config
+    temporaryProviderConfig.deleteSamlProviderConfig(providerId);
+    ProviderConfigTestUtils.assertSamlProviderConfigDoesNotExist(tenantAwareAuth, providerId);
+  }
+
+  @Test
+  public void testListSamlProviderConfigs() throws Exception {
+    final List<String> providerIds = new ArrayList<>();
+
+    // Create provider configs
+    for (int i = 0; i < 3; i++) {
+      String providerId = "saml.provider-id" + i;
+      providerIds.add(providerId);
+      temporaryProviderConfig.createSamlProviderConfig(
+          new SamlProviderConfig.CreateRequest()
+            .setProviderId(providerId)
+            .setIdpEntityId("IDP_ENTITY_ID")
+            .setSsoUrl("https://example.com/login")
+            .addX509Certificate("certificate")
+            .setRpEntityId("RP_ENTITY_ID")
+            .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"));
+    }
+
+    // List provider configs
+    // NOTE: We do not need to test all of the different ways we can iterate over the provider
+    // configs, since this testing is already performed in FirebaseAuthIT with the tenant-agnostic
+    // tests.
+    final AtomicInteger collected = new AtomicInteger(0);
+    ListProviderConfigsPage<SamlProviderConfig> page =
+        tenantAwareAuth.listSamlProviderConfigsAsync(null).get();
+    for (SamlProviderConfig config : page.iterateAll()) {
+      if (providerIds.contains(config.getProviderId())) {
+        collected.incrementAndGet();
+        assertEquals("IDP_ENTITY_ID", config.getIdpEntityId());
+        assertEquals("https://example.com/login", config.getSsoUrl());
+        assertEquals(ImmutableList.of("certificate"), config.getX509Certificates());
+        assertEquals("RP_ENTITY_ID", config.getRpEntityId());
+        assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl());
+      }
+    }
+    assertEquals(providerIds.size(), collected.get());
+  }
+
+  private String signInWithCustomToken(
+      String customToken, @Nullable String tenantId) throws IOException {
+    final GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key="
+        + IntegrationTestUtils.getApiKey());
+    ImmutableMap.Builder<String, Object> content = ImmutableMap.<String, Object>builder();
+    content.put("token", customToken);
+    content.put("returnSecureToken", true);
+    if (tenantId != null) {
+      content.put("tenantId", tenantId);
+    }
+    HttpRequest request = transport.createRequestFactory().buildPostRequest(url,
+        new JsonHttpContent(jsonFactory, content.build()));
+    request.setParser(new JsonObjectParser(jsonFactory));
+    HttpResponse response = request.execute();
+    try {
+      GenericJson json = response.parseAs(GenericJson.class);
+      return json.get("idToken").toString();
+    } finally {
+      response.disconnect();
+    }
+  }
+}
diff --git a/src/test/java/com/google/firebase/auth/TenantManagerIT.java b/src/test/java/com/google/firebase/auth/TenantManagerIT.java
new file mode 100644
index 0000000..6d96a11
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/TenantManagerIT.java
@@ -0,0 +1,155 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.api.core.ApiFuture;
+import com.google.api.core.ApiFutureCallback;
+import com.google.api.core.ApiFutures;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.firebase.testing.IntegrationTestUtils;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Test;
+
+public class TenantManagerIT {
+
+  private static final FirebaseAuth auth = FirebaseAuth.getInstance(
+      IntegrationTestUtils.ensureDefaultApp());
+
+  @Test
+  public void testTenantLifecycle() throws Exception {
+    TenantManager tenantManager = auth.getTenantManager();
+
+    // Create tenant
+    Tenant.CreateRequest createRequest = new Tenant.CreateRequest().setDisplayName("DisplayName");
+    Tenant tenant = tenantManager.createTenantAsync(createRequest).get();
+    assertEquals("DisplayName", tenant.getDisplayName());
+    assertFalse(tenant.isPasswordSignInAllowed());
+    assertFalse(tenant.isEmailLinkSignInEnabled());
+    String tenantId = tenant.getTenantId();
+
+    // Get tenant
+    tenant = tenantManager.getTenantAsync(tenantId).get();
+    assertEquals(tenantId, tenant.getTenantId());
+    assertEquals("DisplayName", tenant.getDisplayName());
+    assertFalse(tenant.isPasswordSignInAllowed());
+    assertFalse(tenant.isEmailLinkSignInEnabled());
+
+    // Update tenant
+    Tenant.UpdateRequest updateRequest = tenant.updateRequest()
+        .setDisplayName("UpdatedName")
+        .setPasswordSignInAllowed(true)
+        .setEmailLinkSignInEnabled(true);
+    tenant = tenantManager.updateTenantAsync(updateRequest).get();
+    assertEquals(tenantId, tenant.getTenantId());
+    assertEquals("UpdatedName", tenant.getDisplayName());
+    assertTrue(tenant.isPasswordSignInAllowed());
+    assertTrue(tenant.isEmailLinkSignInEnabled());
+
+    // Delete tenant
+    tenantManager.deleteTenantAsync(tenant.getTenantId()).get();
+    try {
+      tenantManager.getTenantAsync(tenant.getTenantId()).get();
+      fail("No error thrown for getting a deleted tenant");
+    } catch (ExecutionException e) {
+      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      assertEquals(FirebaseUserManager.TENANT_NOT_FOUND_ERROR,
+          ((FirebaseAuthException) e.getCause()).getErrorCode());
+    }
+  }
+
+  @Test
+  public void testListTenants() throws Exception {
+    TenantManager tenantManager = auth.getTenantManager();
+    final List<String> tenantIds = new ArrayList<>();
+
+    try {
+      for (int i = 0; i < 3; i++) {
+        Tenant.CreateRequest createRequest =
+            new Tenant.CreateRequest().setDisplayName("DisplayName" + i);
+        tenantIds.add(tenantManager.createTenantAsync(createRequest).get().getTenantId());
+      }
+
+      // Test list by batches
+      final AtomicInteger collected = new AtomicInteger(0);
+      ListTenantsPage page = tenantManager.listTenantsAsync(null).get();
+      while (page != null) {
+        for (Tenant tenant : page.getValues()) {
+          if (tenantIds.contains(tenant.getTenantId())) {
+            collected.incrementAndGet();
+            assertNotNull(tenant.getDisplayName());
+          }
+        }
+        page = page.getNextPage();
+      }
+      assertEquals(tenantIds.size(), collected.get());
+
+      // Test iterate all
+      collected.set(0);
+      page = tenantManager.listTenantsAsync(null).get();
+      for (Tenant tenant : page.iterateAll()) {
+        if (tenantIds.contains(tenant.getTenantId())) {
+          collected.incrementAndGet();
+          assertNotNull(tenant.getDisplayName());
+        }
+      }
+      assertEquals(tenantIds.size(), collected.get());
+
+      // Test iterate async
+      collected.set(0);
+      final Semaphore semaphore = new Semaphore(0);
+      final AtomicReference<Throwable> error = new AtomicReference<>();
+      ApiFuture<ListTenantsPage> pageFuture = tenantManager.listTenantsAsync(null);
+      ApiFutures.addCallback(pageFuture, new ApiFutureCallback<ListTenantsPage>() {
+        @Override
+        public void onFailure(Throwable t) {
+          error.set(t);
+          semaphore.release();
+        }
+
+        @Override
+        public void onSuccess(ListTenantsPage result) {
+          for (Tenant tenant : result.iterateAll()) {
+            if (tenantIds.contains(tenant.getTenantId())) {
+              collected.incrementAndGet();
+              assertNotNull(tenant.getDisplayName());
+            }
+          }
+          semaphore.release();
+        }
+      }, MoreExecutors.directExecutor());
+      semaphore.acquire();
+      assertEquals(tenantIds.size(), collected.get());
+      assertNull(error.get());
+    } finally {
+      for (String tenantId : tenantIds) {
+        tenantManager.deleteTenantAsync(tenantId).get();
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/google/firebase/auth/TenantTest.java b/src/test/java/com/google/firebase/auth/TenantTest.java
index a0d2719..f47ccf8 100644
--- a/src/test/java/com/google/firebase/auth/TenantTest.java
+++ b/src/test/java/com/google/firebase/auth/TenantTest.java
@@ -32,7 +32,7 @@ public class TenantTest {
 
   private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
 
-  private static final String TENANT_JSON_STRING = 
+  private static final String TENANT_JSON_STRING =
       "{"
         + "\"name\":\"projects/project-id/resource/TENANT_ID\","
         + "\"displayName\":\"DISPLAY_NAME\","
@@ -41,7 +41,7 @@ public class TenantTest {
         + "}";
 
   @Test
-  public void testJsonSerialization() throws IOException {
+  public void testJsonDeserialization() throws IOException {
     Tenant tenant = jsonFactory.fromString(TENANT_JSON_STRING, Tenant.class);
 
     assertEquals(tenant.getTenantId(), "TENANT_ID");
diff --git a/src/test/java/com/google/firebase/auth/UserTestUtils.java b/src/test/java/com/google/firebase/auth/UserTestUtils.java
new file mode 100644
index 0000000..8f16114
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/UserTestUtils.java
@@ -0,0 +1,124 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import org.junit.rules.ExternalResource;
+
+final class UserTestUtils {
+
+  static void assertUserDoesNotExist(AbstractFirebaseAuth firebaseAuth, String uid)
+      throws Exception {
+    try {
+      firebaseAuth.getUserAsync(uid).get();
+      fail("No error thrown for getting a user which was expected to be absent.");
+    } catch (ExecutionException e) {
+      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
+          ((FirebaseAuthException) e.getCause()).getErrorCode());
+    }
+  }
+
+  static RandomUser generateRandomUserInfo() {
+    String uid = UUID.randomUUID().toString().replaceAll("-", "");
+    String email = String.format(
+        "test%s@example.%s.com",
+        uid.substring(0, 12),
+        uid.substring(12)).toLowerCase();
+    return new RandomUser(uid, email, generateRandomPhoneNumber());
+  }
+
+  private static String generateRandomPhoneNumber() {
+    Random random = new Random();
+    StringBuilder builder = new StringBuilder("+1");
+    for (int i = 0; i < 10; i++) {
+      builder.append(random.nextInt(10));
+    }
+    return builder.toString();
+  }
+
+  static class RandomUser {
+    private final String uid;
+    private final String email;
+    private final String phoneNumber;
+
+    private RandomUser(String uid, String email, String phoneNumber) {
+      this.uid = uid;
+      this.email = email;
+      this.phoneNumber = phoneNumber;
+    }
+
+    String getUid() {
+      return uid;
+    }
+
+    String getEmail() {
+      return email;
+    }
+
+    String getPhoneNumber() {
+      return phoneNumber;
+    }
+  }
+
+  /**
+   * Creates temporary Firebase user accounts for testing, and deletes them at the end of each
+   * test case.
+   */
+  static final class TemporaryUser extends ExternalResource {
+
+    private final AbstractFirebaseAuth auth;
+    private final List<String> users = new ArrayList<>();
+
+    TemporaryUser(AbstractFirebaseAuth auth) {
+      this.auth = auth;
+    }
+
+    public UserRecord create(UserRecord.CreateRequest request) throws FirebaseAuthException {
+      UserRecord user = auth.createUser(request);
+      registerUid(user.getUid());
+      return user;
+    }
+
+    public synchronized void registerUid(String uid) {
+      users.add(uid);
+    }
+
+    @Override
+    protected synchronized void after() {
+      for (String uid : users) {
+        try {
+          auth.deleteUser(uid);
+        } catch (Exception ignore) {
+          // Ignore
+        }
+      }
+
+      users.clear();
+    }
+  }
+}
+
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"
+  } ]
+}
diff --git a/src/test/resources/listSaml.json b/src/test/resources/listSaml.json
new file mode 100644
index 0000000..64b1e1e
--- /dev/null
+++ b/src/test/resources/listSaml.json
@@ -0,0 +1,35 @@
+{
+  "inboundSamlConfigs" : [ {
+    "name": "projects/projectId/inboundSamlConfigs/saml.provider-id1",
+    "displayName" : "DISPLAY_NAME",
+    "enabled" : true,
+    "idpConfig": {
+      "idpEntityId": "IDP_ENTITY_ID",
+      "ssoUrl": "https://example.com/login",
+      "idpCertificates": [
+        { "x509Certificate": "certificate1" },
+        { "x509Certificate": "certificate2" }
+      ]
+    },
+    "spConfig": {
+      "spEntityId": "RP_ENTITY_ID",
+      "callbackUri": "https://projectId.firebaseapp.com/__/auth/handler"
+    }
+  }, {
+    "name": "projects/projectId/inboundSamlConfigs/saml.provider-id2",
+    "displayName" : "DISPLAY_NAME",
+    "enabled" : true,
+    "idpConfig": {
+      "idpEntityId": "IDP_ENTITY_ID",
+      "ssoUrl": "https://example.com/login",
+      "idpCertificates": [
+        { "x509Certificate": "certificate1" },
+        { "x509Certificate": "certificate2" }
+      ]
+    },
+    "spConfig": {
+      "spEntityId": "RP_ENTITY_ID",
+      "callbackUri": "https://projectId.firebaseapp.com/__/auth/handler"
+    }
+  } ]
+}
diff --git a/src/test/resources/oidc.json b/src/test/resources/oidc.json
new file mode 100644
index 0000000..e2f1845
--- /dev/null
+++ b/src/test/resources/oidc.json
@@ -0,0 +1,7 @@
+{
+  "name": "projects/projectId/oauthIdpConfigs/oidc.provider-id",
+  "displayName" : "DISPLAY_NAME",
+  "enabled" : true,
+  "clientId" : "CLIENT_ID",
+  "issuer" : "https://oidc.com/issuer"
+}
diff --git a/src/test/resources/saml.json b/src/test/resources/saml.json
new file mode 100644
index 0000000..ef425b0
--- /dev/null
+++ b/src/test/resources/saml.json
@@ -0,0 +1,17 @@
+{
+  "name": "projects/projectId/inboundSamlConfigs/saml.provider-id",
+  "displayName": "DISPLAY_NAME",
+  "enabled": true,
+  "idpConfig": {
+    "idpEntityId": "IDP_ENTITY_ID",
+    "ssoUrl": "https://example.com/login",
+    "idpCertificates": [
+      { "x509Certificate": "certificate1" },
+      { "x509Certificate": "certificate2" }
+    ]
+  },
+  "spConfig": {
+    "spEntityId": "RP_ENTITY_ID",
+    "callbackUri": "https://projectId.firebaseapp.com/__/auth/handler"
+  }
+}