diff --git a/checkstyle.xml b/checkstyle.xml index 663918173..77e2dba54 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -42,15 +42,15 @@ - + - - + + @@ -229,6 +229,9 @@ + + + diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index f779ae9df..a864c4f6c 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.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 @@ private CallableOperation revokeRefreshTokensOp(fin @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 @@ protected UserRecord execute() throws FirebaseAuthException { } /** - * 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 @@ private CallableOperation listUsersOp( @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() { @Override protected ListUsersPage execute() throws FirebaseAuthException { @@ -515,32 +518,33 @@ protected ListUsersPage execute() throws FirebaseAuthException { /** * 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 createUserAsync(@NonNull CreateRequest request) { + public ApiFuture createUserAsync(@NonNull UserRecord.CreateRequest request) { return createUserOp(request).callAsync(firebaseApp); } private CallableOperation 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 @@ protected UserRecord execute() throws FirebaseAuthException { /** * 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 updateUserAsync(@NonNull UpdateRequest request) { + public ApiFuture updateUserAsync(@NonNull UserRecord.UpdateRequest request) { return updateUserOp(request).callAsync(firebaseApp); } private CallableOperation 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 @@ private CallableOperation setCustomUserClaimsOp( return new CallableOperation() { @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 ApiFuture generateSignInWithEmailLinkAsync( .callAsync(firebaseApp); } - FirebaseApp getFirebaseApp() { - return this.firebaseApp; - } - - FirebaseTokenVerifier getCookieVerifier() { - return this.cookieVerifier.get(); - } - - FirebaseUserManager getUserManager() { - return this.userManager.get(); - } - private CallableOperation generateEmailActionLinkOp( final EmailLinkType type, final String email, final ActionCodeSettings settings) { checkNotDestroyed(); @@ -1082,6 +1076,544 @@ protected String execute() throws FirebaseAuthException { }; } + /** + * 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 createOidcProviderConfigAsync( + @NonNull OidcProviderConfig.CreateRequest request) { + return createOidcProviderConfigOp(request).callAsync(firebaseApp); + } + + private CallableOperation + 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() { + @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 updateOidcProviderConfigAsync( + @NonNull OidcProviderConfig.UpdateRequest request) { + return updateOidcProviderConfigOp(request).callAsync(firebaseApp); + } + + private CallableOperation 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() { + @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 getOidcProviderConfigAsync(@NonNull String providerId) { + return getOidcProviderConfigOp(providerId).callAsync(firebaseApp); + } + + private CallableOperation + getOidcProviderConfigOp(final String providerId) { + checkNotDestroyed(); + OidcProviderConfig.checkOidcProviderId(providerId); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @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 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 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> 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> listOidcProviderConfigsAsync( + @Nullable String pageToken, + int maxResults) { + return listOidcProviderConfigsOp(pageToken, maxResults).callAsync(firebaseApp); + } + + private CallableOperation, FirebaseAuthException> + listOidcProviderConfigsOp(@Nullable final String pageToken, final int maxResults) { + checkNotDestroyed(); + final FirebaseUserManager userManager = getUserManager(); + final DefaultOidcProviderConfigSource source = new DefaultOidcProviderConfigSource(userManager); + final ListProviderConfigsPage.Factory factory = + new ListProviderConfigsPage.Factory(source, maxResults, pageToken); + return + new CallableOperation, FirebaseAuthException>() { + @Override + protected ListProviderConfigsPage 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 deleteOidcProviderConfigAsync(String providerId) { + return deleteOidcProviderConfigOp(providerId).callAsync(firebaseApp); + } + + private CallableOperation deleteOidcProviderConfigOp( + final String providerId) { + checkNotDestroyed(); + OidcProviderConfig.checkOidcProviderId(providerId); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @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 createSamlProviderConfigAsync( + @NonNull SamlProviderConfig.CreateRequest request) { + return createSamlProviderConfigOp(request).callAsync(firebaseApp); + } + + private CallableOperation + 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() { + @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 updateSamlProviderConfigAsync( + @NonNull SamlProviderConfig.UpdateRequest request) { + return updateSamlProviderConfigOp(request).callAsync(firebaseApp); + } + + private CallableOperation 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() { + @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 getSamlProviderConfigAsync(@NonNull String providerId) { + return getSamlProviderConfigOp(providerId).callAsync(firebaseApp); + } + + private CallableOperation + getSamlProviderConfigOp(final String providerId) { + checkNotDestroyed(); + SamlProviderConfig.checkSamlProviderId(providerId); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @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 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 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> 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> listSamlProviderConfigsAsync( + @Nullable String pageToken, + int maxResults) { + return listSamlProviderConfigsOp(pageToken, maxResults).callAsync(firebaseApp); + } + + private CallableOperation, FirebaseAuthException> + listSamlProviderConfigsOp(@Nullable final String pageToken, final int maxResults) { + checkNotDestroyed(); + final FirebaseUserManager userManager = getUserManager(); + final DefaultSamlProviderConfigSource source = new DefaultSamlProviderConfigSource(userManager); + final ListProviderConfigsPage.Factory factory = + new ListProviderConfigsPage.Factory(source, maxResults, pageToken); + return + new CallableOperation, FirebaseAuthException>() { + @Override + protected ListProviderConfigsPage 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 deleteSamlProviderConfigAsync(String providerId) { + return deleteSamlProviderConfigOp(providerId).callAsync(firebaseApp); + } + + private CallableOperation deleteSamlProviderConfigOp( + final String providerId) { + checkNotDestroyed(); + SamlProviderConfig.checkSamlProviderId(providerId); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @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 Supplier threadSafeMemoize(final Supplier supplier) { return Suppliers.memoize( new Supplier() { diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index e5d39b87f..c65e44deb 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.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 @@ */ 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 ERROR_CODES = ImmutableMap.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 @@ Tenant createTenant(Tenant.CreateRequest request) throws FirebaseAuthException { Tenant updateTenant(Tenant.UpdateRequest request) throws FirebaseAuthException { Map 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 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 @@ String getEmailActionLink(EmailLinkType type, String email, 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 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 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 builder = + ImmutableMap.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 builder = + ImmutableMap.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 generateMask(Map properties) { + ImmutableSortedSet.Builder maskBuilder = ImmutableSortedSet.naturalOrder(); + for (Map.Entry entry : properties.entrySet()) { + if (entry.getValue() instanceof Map) { + Set childMask = generateMask((Map) 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 post(String path, Object content, Class 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 000000000..4530c0fae --- /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. + * + *

Provides methods for iterating over the provider configs in the current page, and calling up + * subsequent pages of provider configs. + * + *

Instances of this class are thread-safe and immutable. + */ +public class ListProviderConfigsPage implements Page { + + static final String END_OF_LIST = ""; + + private final ListProviderConfigsResponse currentBatch; + private final ProviderConfigSource source; + private final int maxResults; + + private ListProviderConfigsPage( + @NonNull ListProviderConfigsResponse currentBatch, + @NonNull ProviderConfigSource 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. + * + *

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 getNextPage() { + if (hasNextPage()) { + Factory factory = new Factory(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. + * + *

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 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 getValues() { + return currentBatch.getProviderConfigs(); + } + + private static class ProviderConfigIterable implements Iterable { + + private final ListProviderConfigsPage startingPage; + + ProviderConfigIterable(@NonNull ListProviderConfigsPage startingPage) { + this.startingPage = checkNotNull(startingPage, "starting page must not be null"); + } + + @Override + @NonNull + public Iterator iterator() { + return new ProviderConfigIterator(startingPage); + } + + /** + * An {@link Iterator} that cycles through provider configs, one at a time. + * + *

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 implements Iterator { + + private ListProviderConfigsPage currentPage; + private List batch; + private int index = 0; + + private ProviderConfigIterator(ListProviderConfigsPage 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 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 { + @NonNull + ListProviderConfigsResponse fetch(int maxResults, String pageToken) + throws FirebaseAuthException; + } + + static class DefaultOidcProviderConfigSource implements ProviderConfigSource { + + 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 { + + 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. + * + *

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 { + + private final ProviderConfigSource source; + private final int maxResults; + private final String pageToken; + + Factory(@NonNull ProviderConfigSource 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 create() throws FirebaseAuthException { + ListProviderConfigsResponse batch = source.fetch(maxResults, pageToken); + return new ListProviderConfigsPage(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 8a8c8b4fe..d637174c7 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 Iterable iterateAll() { } /** - * 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 f406366ba..ba727af5a 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 String getNextPageToken() { @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 @@ String getNextPageToken() { * 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 000000000..90a49e624 --- /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. + * + *

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. + * + *

Set the initial attributes of the new provider by calling various setter methods available + * in this class. + */ + public static final class CreateRequest extends AbstractCreateRequest { + + /** + * Creates a new {@link CreateRequest}, which can be used to create a new OIDC Auth provider. + * + *

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. + * + *

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 { + + /** + * Creates a new {@link UpdateRequest}, which can be used to updates an existing OIDC Auth + * provider. + * + *

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 000000000..921a07b5b --- /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. + * + *

Set the initial attributes of the new provider by calling various setter methods available + * in this class. + */ + public abstract static class AbstractCreateRequest> { + + final Map 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 getProperties() { + return ImmutableMap.copyOf(properties); + } + + abstract T getThis(); + } + + /** + * A base class for updating the attributes of an existing provider. + */ + public abstract static class AbstractUpdateRequest> { + + final String providerId; + final Map 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 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 000000000..e74478bea --- /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. + * + *

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 getX509Certificates() { + List> idpCertificates = + (List>) idpConfig.get("idpCertificates"); + checkNotNull(idpCertificates); + ImmutableList.Builder certificates = ImmutableList.builder(); + for (Map 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 ensureNestedList(Map outerMap, String id) { + List list = (List) outerMap.get(id); + if (list == null) { + list = new ArrayList(); + outerMap.put(id, list); + } + return list; + } + + private static Map ensureNestedMap(Map outerMap, String id) { + Map map = (Map) outerMap.get(id); + if (map == null) { + map = new HashMap(); + outerMap.put(id, map); + } + return map; + } + + /** + * A specification class for creating a new SAML Auth provider. + * + *

Set the initial attributes of the new provider by calling various setter methods available + * in this class. + */ + public static final class CreateRequest extends AbstractCreateRequest { + + /** + * Creates a new {@link CreateRequest}, which can be used to create a new SAML Auth provider. + * + *

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 idpConfigProperties = ensureNestedMap(properties, "idpConfig"); + List x509Certificates = ensureNestedList(idpConfigProperties, "idpCertificates"); + x509Certificates.add(ImmutableMap.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 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. + * + *

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 { + /** + * Creates a new {@link UpdateRequest}, which can be used to updates an existing SAML Auth + * provider. + * + *

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 idpConfigProperties = ensureNestedMap(properties, "idpConfig"); + List x509Certificates = ensureNestedList(idpConfigProperties, "idpCertificates"); + x509Certificates.add(ImmutableMap.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 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 993aa1b19..61f52572d 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 @@ */ 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 boolean isEmailLinkSignInEnabled() { } /** - * 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 0b6175534..4b3bbb948 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 Tenant getTenant(@NonNull String tenantId) throws FirebaseAuthException { } 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 ApiFuture getTenantAsync(@NonNull String tenantId) { private CallableOperation 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() { @Override protected Tenant execute() throws FirebaseAuthException { @@ -196,7 +196,7 @@ public ApiFuture createTenantAsync(@NonNull CreateRequest request) { private CallableOperation createTenantOp( final CreateRequest request) { checkNotDestroyed(); - checkNotNull(request, "create request must not be null"); + checkNotNull(request, "Create request must not be null."); return new CallableOperation() { @Override protected Tenant execute() throws FirebaseAuthException { @@ -234,7 +234,9 @@ public ApiFuture updateTenantAsync(@NonNull UpdateRequest request) { private CallableOperation 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() { @Override protected Tenant execute() throws FirebaseAuthException { @@ -269,7 +271,7 @@ public ApiFuture deleteTenantAsync(String tenantId) { private CallableOperation 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() { @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 000000000..e8b5e20e6 --- /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 { + + @Key("oauthIdpConfigs") + private List providerConfigs; + + @Key("nextPageToken") + private String pageToken; + + @Override + public List getProviderConfigs() { + return providerConfigs == null ? ImmutableList.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 000000000..c81205a56 --- /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 { + + public List 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 000000000..4386e2145 --- /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 { + + @Key("inboundSamlConfigs") + private List providerConfigs; + + @Key("nextPageToken") + private String pageToken; + + @Override + public List getProviderConfigs() { + return providerConfigs == null ? ImmutableList.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 4977061e0..b612793d2 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 @@ 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; diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index c41772293..f651e3d3f 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.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.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 @@ private ApiFuture slowDeleteUsersAsync(List uids) thr @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 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 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 void testUserLifecycle() throws Exception { 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 void testUserLifecycle() throws Exception { 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 void testUserLifecycle() throws Exception { 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 void testUserLifecycle() throws Exception { // 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 void testLastRefreshTime() throws Exception { public void testListUsers() throws Exception { final List 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 error = new AtomicReference<>(); - ApiFuture pageFuture = auth.listUsersAsync(null); - ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { - @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 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 error = new AtomicReference<>(); + ApiFuture pageFuture = auth.listUsersAsync(null); + ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { + @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 error = new AtomicReference<>(); - ApiFuture pageFuture = tenantAwareAuth.listUsersAsync(null); - ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { - @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 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 error = new AtomicReference<>(); - ApiFuture pageFuture = tenantManager.listTenantsAsync(null); - ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { - @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 expected = ImmutableMap.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 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 result = decoded.getClaims(); - for (Map.Entry entry : expected.entrySet()) { - assertEquals(entry.getValue(), result.get(entry.getKey())); - } + Map expected = ImmutableMap.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 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 result = decoded.getClaims(); + for (Map.Entry 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 void testCustomTokenWithIAM() throws Exception { } } - @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 void testCustomTokenWithClaims() throws Exception { @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 void testImportUsersWithPassword() throws Exception { .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 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 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 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 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 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 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 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 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 error = new AtomicReference<>(); + ApiFuture> pageFuture = + auth.listOidcProviderConfigsAsync(null); + ApiFutures.addCallback( + pageFuture, + new ApiFutureCallback>() { + @Override + public void onFailure(Throwable t) { + error.set(t); + semaphore.release(); + } + + @Override + public void onSuccess(ListProviderConfigsPage 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 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 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 error = new AtomicReference<>(); + ApiFuture> pageFuture = + auth.listSamlProviderConfigsAsync(null); + ApiFutures.addCallback( + pageFuture, + new ApiFutureCallback>() { + @Override + public void onFailure(Throwable t) { + error.set(t); + semaphore.release(); + } + + @Override + public void onSuccess(ListProviderConfigsPage 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 parseLinkParameters(String link) throws Exception { @@ -1075,15 +911,6 @@ private Map parseLinkParameters(String link) throws Exception { 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 @@ private void checkRecreateUser(String uid) throws Exception { } } - 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 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 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 @@ static UserRecord newUserWithParams() throws Exception { 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 ac8860c72..baff2e6af 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 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 void testUnexpectedHttpError() { } } + @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 page = + FirebaseAuth.getInstance().listOidcProviderConfigsAsync(null, 99).get(); + + ImmutableList 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 page = + FirebaseAuth.getInstance().listOidcProviderConfigsAsync("token", 99).get(); + + ImmutableList 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 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 page = + tenantAwareAuth.listOidcProviderConfigsAsync(null, 99).get(); + + ImmutableList 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 idpConfig = (Map) 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 idpCertificates = (List) 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 spConfig = (Map) 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 idpConfig = (Map) 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 idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(1, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate"), idpCertificates.get(0)); + Map spConfig = (Map) 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 idpConfig = (Map) 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 idpCertificates = (List) 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 spConfig = (Map) 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 idpConfig = (Map) 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 page = + FirebaseAuth.getInstance().listSamlProviderConfigsAsync(null, 99).get(); + + ImmutableList 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 page = + FirebaseAuth.getInstance().listSamlProviderConfigsAsync("token", 99).get(); + + ImmutableList 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 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 page = + tenantAwareAuth.listSamlProviderConfigsAsync(null, 99).get(); + + ImmutableList 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 @@ private static TestResponseInterceptor initializeAppForUserManagementWithStatusC 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 mocks = new ArrayList<>(); for (String response : responses) { mocks.add(new MockLowLevelHttpResponse().setContent(response)); @@ -1633,9 +2543,6 @@ private static TestResponseInterceptor initializeAppForUserManagement(String... .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 @@ private static void checkTenant(Tenant tenant, String tenantId) { 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 efe2f783f..a0bdcb6c6 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 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 static void setUpClass() throws Exception { 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 fb4a7d275..5e848069d 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.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 void testRedactedPasswords() throws FirebaseAuthException, IOException { 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 void testMultiplePages() throws FirebaseAuthException, IOException { 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 users = ImmutableList.copyOf(page1.getValues()); @@ -136,7 +137,7 @@ public void testMultiplePages() throws FirebaseAuthException, IOException { @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 users = page.iterateAll(); int iterations = 0; @@ -162,7 +163,7 @@ public void testListUsersIterable() throws FirebaseAuthException, IOException { @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 users = page.iterateAll(); Iterator iterator = users.iterator(); int iterations = 0; @@ -192,7 +193,7 @@ public void testListUsersPagedIterable() throws FirebaseAuthException, IOExcepti 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 void testListUsersPagedIterator() throws FirebaseAuthException, IOExcepti 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 users = page.iterateAll().iterator(); int iterations = 0; while (users.hasNext()) { @@ -251,7 +252,7 @@ public void testPageWithNoUsers() throws FirebaseAuthException { ImmutableList.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 void testIterableWithNoUsers() throws FirebaseAuthException { ImmutableList.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 void testIteratorWithNoUsers() throws FirebaseAuthException { ListUsersPage.END_OF_LIST); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); Iterator iterator = page.iterateAll().iterator(); while (iterator.hasNext()) { fail("Should not be able to iterate"); @@ -294,7 +295,7 @@ public void testRemove() throws FirebaseAuthException, IOException { ListUsersPage.END_OF_LIST); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); Iterator iterator = page.iterateAll().iterator(); while (iterator.hasNext()) { assertNotNull(iterator.next()); @@ -308,14 +309,14 @@ public void testRemove() throws FirebaseAuthException, IOException { @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 void testInvalidPageToken() throws IOException { 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 000000000..fa5c6d4b0 --- /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 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 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 000000000..262c48f37 --- /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 oidcIds = new ArrayList<>(); + private final List 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 000000000..135175227 --- /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 properties = createRequest.getProperties(); + assertEquals(4, properties.size()); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("enabled")); + + Map idpConfig = (Map) 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 idpCertificates = (List) 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 spConfig = (Map) 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 properties = createRequest.getProperties(); + assertEquals(1, properties.size()); + Map idpConfig = (Map) properties.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(1, idpConfig.size()); + + List idpCertificates = (List) 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.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 properties = updateRequest.getProperties(); + assertEquals(4, properties.size()); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("enabled")); + + Map idpConfig = (Map) 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 idpCertificates = (List) 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 spConfig = (Map) 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 properties = updateRequest.getProperties(); + assertEquals(1, properties.size()); + Map idpConfig = (Map) properties.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(1, idpConfig.size()); + + List idpCertificates = (List) 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.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 000000000..a6d83dccf --- /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 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 error = new AtomicReference<>(); + ApiFuture pageFuture = tenantAwareAuth.listUsersAsync(null); + ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { + @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 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 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 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 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 content = ImmutableMap.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 000000000..6d96a114d --- /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 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 error = new AtomicReference<>(); + ApiFuture pageFuture = tenantManager.listTenantsAsync(null); + ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { + @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 a0d271991..f47ccf8fa 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 000000000..8f16114c4 --- /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 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 000000000..0c13ea48b --- /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 000000000..64b1e1e36 --- /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 000000000..e2f1845de --- /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 000000000..ef425b0a8 --- /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" + } +}