From 392bf67e74c77bcf6d882a50a4e044bbb8fc8f2a Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Mon, 27 Apr 2020 16:34:05 -0400 Subject: [PATCH 01/21] Add OIDC Auth provider config class (#397) Adds OIDC provider config class and base class. This is part of adding multi-tenancy support (see issue #332). --- .../firebase/auth/AuthProviderConfig.java | 100 ++++++++++++++++++ .../firebase/auth/OidcProviderConfig.java | 94 ++++++++++++++++ .../firebase/auth/OidcProviderConfigTest.java | 26 +++++ 3 files changed, 220 insertions(+) create mode 100644 src/main/java/com/google/firebase/auth/AuthProviderConfig.java create mode 100644 src/main/java/com/google/firebase/auth/OidcProviderConfig.java create mode 100644 src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java diff --git a/src/main/java/com/google/firebase/auth/AuthProviderConfig.java b/src/main/java/com/google/firebase/auth/AuthProviderConfig.java new file mode 100644 index 000000000..9f301fe2f --- /dev/null +++ b/src/main/java/com/google/firebase/auth/AuthProviderConfig.java @@ -0,0 +1,100 @@ +/* + * 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.util.HashMap; +import java.util.Map; + +/** + * The base class for Auth providers. + */ +public abstract class AuthProviderConfig { + + @Key("name") + private String providerId; + + @Key("displayName") + private String displayName; + + @Key("enabled") + private boolean enabled; + + public String getProviderId() { + return providerId; + } + + public String getDisplayName() { + return displayName; + } + + public boolean isEnabled() { + return enabled; + } + + /** + * 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 CreateRequest { + + final Map properties = new HashMap<>(); + + /** + * Sets the ID for the new provider. + * + * @param providerId a non-null, non-empty provider ID string. + */ + public CreateRequest setProviderId(String providerId) { + checkArgument( + !Strings.isNullOrEmpty(providerId), "provider ID name must not be null or empty"); + properties.put("name", providerId); + return this; + } + + /** + * Sets the display name for the new provider. + * + * @param displayName a non-null, non-empty display name string. + */ + public CreateRequest setDisplayName(String displayName) { + checkArgument(!Strings.isNullOrEmpty(displayName), "display name must not be null or empty"); + properties.put("displayName", displayName); + return this; + } + + /** + * 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 CreateRequest setEnabled(boolean enabled) { + properties.put("enabled", enabled); + return this; + } + + Map getProperties() { + return ImmutableMap.copyOf(properties); + } + } +} 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..0ad573a73 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java @@ -0,0 +1,94 @@ +/* + * 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; + +/** + * Contains metadata associated with an OIDC Auth provider. + * + *

Instances of this class are immutable and thread safe. + */ +public final class OidcProviderConfig extends AuthProviderConfig { + + @Key("clientId") + private String clientId; + + @Key("issuer") + private String issuer; + + public String getClientId() { + return clientId; + } + + public String getIssuer() { + return issuer; + } + + /** + * 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 AuthProviderConfig.CreateRequest { + + /** + * Creates a new {@link CreateRequest}, which can be used to create a new OIDC Auth provider. + * + *

The returned object should be passed to + * {@link TenantAwareFirebaseAuth#createProviderConfig(CreateRequest)} to register the provider + * information persistently. + */ + public CreateRequest() { } + + /** + * Sets the client ID for the new provider. + * + * @param clientId a non-null, non-empty client ID string. + */ + 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 string. + */ + public CreateRequest setIssuer(String issuer) { + checkArgument(!Strings.isNullOrEmpty(issuer), "issuer must not be null or empty"); + try { + new URL(issuer); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(issuer + " is a malformed URL", e); + } + properties.put("issuer", issuer); + return this; + } + } +} 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..15326656c --- /dev/null +++ b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java @@ -0,0 +1,26 @@ +/* + * 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 org.junit.Test; + +public class OidcProviderConfigTest { + @Test(expected = IllegalArgumentException.class) + public void testInvalidIssuerUrl() { + new OidcProviderConfig.CreateRequest().setIssuer("not a valid url"); + } +} From a2fa7824d1c8ac7cf0ff518890ea1157afdcedef Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Wed, 29 Apr 2020 10:30:38 -0400 Subject: [PATCH 02/21] Fix CreateRequest chaining and provider ID extraction. (#399) This makes the base `CreateRequest` setters return the proper instance type, so that methods can be chained. This also makes it so that the provider ID can be parsed from the resource name. A package private `getProviderId()` method was also added, which will be needed by the `FirebaseUserManager` class when the provider config operations are added there. Also renamed `AuthProviderConfig` to `ProviderConfig` since "Auth" is redundant with the package name. This work is part of adding multi-tenancy support (see issue #332). --- .../firebase/auth/OidcProviderConfig.java | 11 ++-- ...roviderConfig.java => ProviderConfig.java} | 29 +++++++---- .../java/com/google/firebase/auth/Tenant.java | 8 +-- .../firebase/auth/OidcProviderConfigTest.java | 51 +++++++++++++++++++ 4 files changed, 78 insertions(+), 21 deletions(-) rename src/main/java/com/google/firebase/auth/{AuthProviderConfig.java => ProviderConfig.java} (79%) diff --git a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java index 0ad573a73..7fc148e19 100644 --- a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java +++ b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java @@ -21,6 +21,7 @@ import com.google.api.client.util.Key; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; +import com.google.firebase.auth.ProviderConfig.AbstractCreateRequest; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; @@ -31,7 +32,7 @@ * *

Instances of this class are immutable and thread safe. */ -public final class OidcProviderConfig extends AuthProviderConfig { +public final class OidcProviderConfig extends ProviderConfig { @Key("clientId") private String clientId; @@ -53,13 +54,13 @@ public String getIssuer() { *

Set the initial attributes of the new provider by calling various setter methods available * in this class. */ - public static final class CreateRequest extends AuthProviderConfig.CreateRequest { + 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 TenantAwareFirebaseAuth#createProviderConfig(CreateRequest)} to register the provider + * {@link AbstractFirebaseAuth#createOidcProviderConfig(CreateRequest)} to register the provider * information persistently. */ public CreateRequest() { } @@ -90,5 +91,9 @@ public CreateRequest setIssuer(String issuer) { properties.put("issuer", issuer); return this; } + + CreateRequest getThis() { + return this; + } } } diff --git a/src/main/java/com/google/firebase/auth/AuthProviderConfig.java b/src/main/java/com/google/firebase/auth/ProviderConfig.java similarity index 79% rename from src/main/java/com/google/firebase/auth/AuthProviderConfig.java rename to src/main/java/com/google/firebase/auth/ProviderConfig.java index 9f301fe2f..07820ff9c 100644 --- a/src/main/java/com/google/firebase/auth/AuthProviderConfig.java +++ b/src/main/java/com/google/firebase/auth/ProviderConfig.java @@ -27,10 +27,10 @@ /** * The base class for Auth providers. */ -public abstract class AuthProviderConfig { +public abstract class ProviderConfig { @Key("name") - private String providerId; + private String resourceName; @Key("displayName") private String displayName; @@ -39,7 +39,7 @@ public abstract class AuthProviderConfig { private boolean enabled; public String getProviderId() { - return providerId; + return resourceName.substring(resourceName.lastIndexOf("/") + 1); } public String getDisplayName() { @@ -56,20 +56,25 @@ public boolean isEnabled() { *

Set the initial attributes of the new provider by calling various setter methods available * in this class. */ - public abstract static class CreateRequest { + public abstract static class AbstractCreateRequest> { final Map properties = new HashMap<>(); + String providerId; /** * Sets the ID for the new provider. * * @param providerId a non-null, non-empty provider ID string. */ - public CreateRequest setProviderId(String providerId) { + public T setProviderId(String providerId) { checkArgument( !Strings.isNullOrEmpty(providerId), "provider ID name must not be null or empty"); - properties.put("name", providerId); - return this; + this.providerId = providerId; + return getThis(); + } + + String getProviderId() { + return providerId; } /** @@ -77,10 +82,10 @@ public CreateRequest setProviderId(String providerId) { * * @param displayName a non-null, non-empty display name string. */ - public CreateRequest setDisplayName(String displayName) { + public T setDisplayName(String displayName) { checkArgument(!Strings.isNullOrEmpty(displayName), "display name must not be null or empty"); properties.put("displayName", displayName); - return this; + return getThis(); } /** @@ -88,13 +93,15 @@ public CreateRequest setDisplayName(String displayName) { * * @param enabled a boolean indicating whether the user can sign in with the provider */ - public CreateRequest setEnabled(boolean enabled) { + public T setEnabled(boolean enabled) { properties.put("enabled", enabled); - return this; + return getThis(); } Map getProperties() { return ImmutableMap.copyOf(properties); } + + abstract T getThis(); } } diff --git a/src/main/java/com/google/firebase/auth/Tenant.java b/src/main/java/com/google/firebase/auth/Tenant.java index 993aa1b19..29bf3c508 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() { diff --git a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java index 15326656c..e5cd94ab3 100644 --- a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java +++ b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java @@ -16,9 +16,60 @@ 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\"" + + "}"; + + @Test + public void testJsonSerialization() throws IOException { + OidcProviderConfig config = jsonFactory.fromString(OIDC_JSON_STRING, OidcProviderConfig.class); + + assertEquals(config.getProviderId(), "oidc.provider-id"); + assertEquals(config.getDisplayName(), "DISPLAY_NAME"); + assertTrue(config.isEnabled()); + assertEquals(config.getClientId(), "CLIENT_ID"); + assertEquals(config.getIssuer(), "https://oidc.com/issuer"); + } + + @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 testInvalidIssuerUrl() { new OidcProviderConfig.CreateRequest().setIssuer("not a valid url"); From db81c068167bb10f4577b7ae2b70a68c02426fec Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Wed, 29 Apr 2020 19:09:57 -0400 Subject: [PATCH 03/21] Add operations to create and delete OIDC provider configs. (#400) This adds an operation to create OIDC provider configs, as well as an operation to delete provider configs. These operations can be performed using either the tenant-aware or standard Firebase client. This work is part of adding multi-tenancy support (see issue #332). --- .../firebase/auth/AbstractFirebaseAuth.java | 143 +++++++++++--- .../firebase/auth/FirebaseUserManager.java | 35 +++- .../google/firebase/auth/FirebaseAuthIT.java | 63 +++++++ .../auth/FirebaseUserManagerTest.java | 178 +++++++++++++++++- src/test/resources/oidc.json | 7 + 5 files changed, 383 insertions(+), 43 deletions(-) create mode 100644 src/test/resources/oidc.json diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index f779ae9df..8b1f235be 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -31,8 +31,7 @@ import com.google.firebase.auth.FirebaseUserManager.UserImportRequest; 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 +322,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; } @@ -515,32 +515,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 +556,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 +641,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 +1057,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 +1073,98 @@ protected String execute() throws FirebaseAuthException { }; } + /** + * Creates a new provider OIDC Auth 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 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. + */ + 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"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected OidcProviderConfig execute() throws FirebaseAuthException { + return userManager.createOidcProviderConfig(request); + } + }; + } + + /** + * Deletes the 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. + * @throws FirebaseAuthException If an error occurs while deleting the provider config. + */ + public void deleteProviderConfig(@NonNull String providerId) throws FirebaseAuthException { + deleteProviderConfigOp(providerId).call(); + } + + /** + * Similar to {@link #deleteProviderConfig} 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. + */ + public ApiFuture deleteProviderConfigAsync(String providerId) { + return deleteProviderConfigOp(providerId).callAsync(firebaseApp); + } + + private CallableOperation deleteProviderConfigOp( + final String providerId) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(providerId), "provider ID must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseAuthException { + userManager.deleteProviderConfig(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..ed1c0c4fd 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -69,6 +69,7 @@ */ class FirebaseUserManager { + static final String CONFIGURATION_NOT_FOUND = "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 +79,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) .put("INSUFFICIENT_PERMISSION", "insufficient-permission") .put("DUPLICATE_EMAIL", "email-already-exists") .put("DUPLICATE_LOCAL_ID", "uid-already-exists") @@ -112,6 +113,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 +128,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; @@ -366,6 +371,20 @@ 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"); + String providerId = request.getProviderId(); + checkArgument(!Strings.isNullOrEmpty(providerId), "provider ID must not be null or empty"); + url.set("oauthIdpConfigId", providerId); + return sendRequest("POST", url, request.getProperties(), OidcProviderConfig.class); + } + + void deleteProviderConfig(String providerId) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs/" + providerId); + sendRequest("DELETE", url, null, GenericJson.class); + } + private static String getTenantUrlSuffix(String tenantId) { checkArgument(!Strings.isNullOrEmpty(tenantId)); return "/tenants/" + tenantId; diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index c41772293..dd6174169 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -1060,6 +1060,69 @@ public void testGenerateSignInWithEmailLink() throws Exception { } } + @Test + public void testOidcProviderConfigLifecycle() throws Exception { + // Create config provider + String providerId = "oidc.provider-id"; + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setProviderId(providerId) + .setDisplayName("DisplayName") + .setEnabled(true) + .setClientId("ClientId") + .setIssuer("https://oidc.com/issuer"); + OidcProviderConfig config = auth.createOidcProviderConfigAsync(createRequest).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertEquals("ClientId", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + + // TODO(micahstairs): Test getOidcProviderConfig and updateProviderConfig operations. + + // Delete config provider + auth.deleteProviderConfigAsync(providerId).get(); + // TODO(micahstairs): Once getOidcProviderConfig operation is implemented, add a check here to + // double-check that the config provider was deleted. + } + + @Test + public void testTenantAwareOidcProviderConfigLifecycle() 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 config provider + TenantAwareFirebaseAuth tenantAwareAuth = auth.getTenantManager().getAuthForTenant(tenantId); + String providerId = "oidc.provider-id"; + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setProviderId(providerId) + .setDisplayName("DisplayName") + .setEnabled(true) + .setClientId("ClientId") + .setIssuer("https://oidc.com/issuer"); + OidcProviderConfig config = + tenantAwareAuth.createOidcProviderConfigAsync(createRequest).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertEquals("ClientId", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + + // TODO(micahstairs): Test getOidcProviderConfig and updateProviderConfig operations. + + // Delete config provider + tenantAwareAuth.deleteProviderConfigAsync(providerId).get(); + // TODO(micahstairs): Once getOidcProviderConfig operation is implemented, add a check here to + // double-check that the config provider was deleted. + } finally { + // Delete tenant. + tenantManager.deleteTenantAsync(tenantId).get(); + } + } + private Map parseLinkParameters(String link) throws Exception { Map result = new HashMap<>(); int queryBegin = link.indexOf('?'); diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index ac8860c72..a4bf38d6c 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,149 @@ 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); + 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' field can be omitted from an OIDC provider config creation request. + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setProviderId("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + FirebaseAuth.getInstance().createOidcProviderConfig(createRequest); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", 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 testDeleteProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + + FirebaseAuth.getInstance().deleteProviderConfig("PROVIDER_ID"); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/PROVIDER_ID"); + } + + @Test + public void testDeleteProviderConfigWithNotFoundError() throws Exception { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().deleteProviderConfig("UNKNOWN"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND, e.getErrorCode()); + } + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/UNKNOWN"); + } + + @Test + public void testTenantAwareDeleteProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + "{}"); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + tenantAwareAuth.deleteProviderConfig("PROVIDER_ID"); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/PROVIDER_ID"); + } + private static TestResponseInterceptor initializeAppForUserManagementWithStatusCode( int statusCode, String response) { FirebaseApp.initializeApp(new FirebaseOptions.Builder() @@ -1622,7 +1768,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 +1796,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 +1870,14 @@ private static void checkTenant(Tenant tenant, String tenantId) { assertFalse(tenant.isEmailLinkSignInEnabled()); } + private static void checkOidcProviderConfig(OidcProviderConfig config) { + 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()); + } + private static void checkRequestHeaders(TestResponseInterceptor interceptor) { HttpHeaders headers = interceptor.getResponse().getRequest().getHeaders(); String auth = "Bearer " + TEST_TOKEN; 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" +} From 424d9ce4b9349506ca25b95779fb1d7d5b6c8436 Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Thu, 30 Apr 2020 17:17:54 -0400 Subject: [PATCH 04/21] Add getOidcProviderConfig operation. (#401) This adds an operation to get OIDC provider configs (can be done using either the tenant-aware or standard Firebase client). This work is part of adding multi-tenancy support (see issue #332). --- .../firebase/auth/AbstractFirebaseAuth.java | 40 ++++++++++++++ .../firebase/auth/FirebaseUserManager.java | 9 ++- .../google/firebase/auth/FirebaseAuthIT.java | 55 +++++++++++++++---- .../auth/FirebaseUserManagerTest.java | 45 ++++++++++++++- 4 files changed, 135 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index 8b1f235be..dae7af361 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -1115,6 +1115,46 @@ protected OidcProviderConfig execute() throws FirebaseAuthException { }; } + /** + * Gets the provider OIDC Auth config 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. + * @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. + * + * @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. + */ + public ApiFuture getOidcProviderConfigAsync(@NonNull String providerId) { + return getOidcProviderConfigOp(providerId).callAsync(firebaseApp); + } + + private CallableOperation + getOidcProviderConfigOp(final String providerId) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(providerId), "provider ID must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected OidcProviderConfig execute() throws FirebaseAuthException { + return userManager.getOidcProviderConfig(providerId); + } + }; + } + /** * Deletes the provider config identified by the specified provider ID. * diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index ed1c0c4fd..5dab93ea7 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -69,7 +69,7 @@ */ class FirebaseUserManager { - static final String CONFIGURATION_NOT_FOUND = "configuration-not-found"; + 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"; @@ -79,7 +79,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", CONFIGURATION_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") @@ -380,6 +380,11 @@ OidcProviderConfig createOidcProviderConfig( return sendRequest("POST", url, request.getProperties(), OidcProviderConfig.class); } + OidcProviderConfig getOidcProviderConfig(String providerId) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs/" + providerId); + return sendRequest("GET", url, null, OidcProviderConfig.class); + } + void deleteProviderConfig(String providerId) throws FirebaseAuthException { GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs/" + providerId); sendRequest("DELETE", url, null, GenericJson.class); diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index dd6174169..a778b8ce0 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -1077,17 +1077,27 @@ public void testOidcProviderConfigLifecycle() throws Exception { assertEquals("ClientId", config.getClientId()); assertEquals("https://oidc.com/issuer", config.getIssuer()); - // TODO(micahstairs): Test getOidcProviderConfig and updateProviderConfig operations. + try { + // Get config provider + config = auth.getOidcProviderConfigAsync(providerId).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertEquals("ClientId", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + + // TODO(micahstairs): Test updateProviderConfig operation - // Delete config provider - auth.deleteProviderConfigAsync(providerId).get(); - // TODO(micahstairs): Once getOidcProviderConfig operation is implemented, add a check here to - // double-check that the config provider was deleted. + } finally { + // Delete config provider + auth.deleteProviderConfigAsync(providerId).get(); + } + + assertOidcProviderConfigDoesNotExist(auth, providerId); } @Test public void testTenantAwareOidcProviderConfigLifecycle() throws Exception { - // Create tenant to use. + // Create tenant to use TenantManager tenantManager = auth.getTenantManager(); Tenant.CreateRequest tenantCreateRequest = new Tenant.CreateRequest().setDisplayName("DisplayName"); @@ -1111,12 +1121,22 @@ public void testTenantAwareOidcProviderConfigLifecycle() throws Exception { assertEquals("ClientId", config.getClientId()); assertEquals("https://oidc.com/issuer", config.getIssuer()); - // TODO(micahstairs): Test getOidcProviderConfig and updateProviderConfig operations. + try { + // Get config provider + config = tenantAwareAuth.getOidcProviderConfigAsync(providerId).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertEquals("ClientId", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + + // TODO(micahstairs): Test updateProviderConfig operation + + } finally { + // Delete config provider + tenantAwareAuth.deleteProviderConfigAsync(providerId).get(); + } - // Delete config provider - tenantAwareAuth.deleteProviderConfigAsync(providerId).get(); - // TODO(micahstairs): Once getOidcProviderConfig operation is implemented, add a check here to - // double-check that the config provider was deleted. + assertOidcProviderConfigDoesNotExist(tenantAwareAuth, providerId); } finally { // Delete tenant. tenantManager.deleteTenantAsync(tenantId).get(); @@ -1253,6 +1273,19 @@ static RandomUser create() { } } + + private static void assertOidcProviderConfigDoesNotExist( + AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception { + try { + firebaseAuth.getOidcProviderConfigAsync(providerId).get(); + fail("No error thrown for getting a deleted provider config"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } + private static void assertUserDoesNotExist(AbstractFirebaseAuth firebaseAuth, String uid) throws Exception { try { diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index a4bf38d6c..01ea7a5d1 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -1716,6 +1716,49 @@ public void testTenantAwareCreateOidcProvider() throws Exception { checkUrl(interceptor, "POST", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs"); } + @Test + public void testGetOidcProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + + OidcProviderConfig config = + FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id"); + + checkOidcProviderConfig(config); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @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); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/oidc.provider-id"); + } + + @Test public void testDeleteProviderConfig() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); @@ -1735,7 +1778,7 @@ public void testDeleteProviderConfigWithNotFoundError() throws Exception { FirebaseAuth.getInstance().deleteProviderConfig("UNKNOWN"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND, e.getErrorCode()); + assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); } checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/UNKNOWN"); } From f728f6343881a604c59760cf8783f99ef6c83b8c Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Fri, 1 May 2020 17:03:16 -0400 Subject: [PATCH 05/21] Add operation to update OIDC provider configs. (#402) This adds an operation to update OIDC provider configs (can be done using either the tenant-aware or standard Firebase client). This work is part of adding multi-tenancy support (see issue #332). --- .../firebase/auth/AbstractFirebaseAuth.java | 40 +++++++ .../firebase/auth/FirebaseUserManager.java | 38 +++++-- .../firebase/auth/OidcProviderConfig.java | 77 ++++++++++++- .../google/firebase/auth/ProviderConfig.java | 45 ++++++++ .../java/com/google/firebase/auth/Tenant.java | 5 +- .../google/firebase/auth/FirebaseAuthIT.java | 32 +++++- .../auth/FirebaseUserManagerTest.java | 102 ++++++++++++++++++ .../firebase/auth/OidcProviderConfigTest.java | 51 ++++++++- 8 files changed, 366 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index dae7af361..0d7ceabcc 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -1115,6 +1115,46 @@ protected OidcProviderConfig execute() throws FirebaseAuthException { }; } + /** + * Updates an existing OIDC 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 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}. + */ + 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"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected OidcProviderConfig execute() throws FirebaseAuthException { + return userManager.updateOidcProviderConfig(request); + } + }; + } + /** * Gets the provider OIDC Auth config corresponding to the specified provider ID. * diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 5dab93ea7..2d0a4cc0d 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -306,14 +306,6 @@ Tenant updateTenant(Tenant.UpdateRequest request) throws FirebaseAuthException { 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); @@ -380,21 +372,45 @@ OidcProviderConfig createOidcProviderConfig( return sendRequest("POST", url, request.getProperties(), OidcProviderConfig.class); } + OidcProviderConfig updateOidcProviderConfig(OidcProviderConfig.UpdateRequest request) + throws FirebaseAuthException { + Map properties = request.getProperties(); + checkArgument(!properties.isEmpty(), + "provider config update must have at least one property set"); + GenericUrl url = + new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(request.getProviderId())); + url.put("updateMask", generateMask(properties)); + return sendRequest("PATCH", url, properties, OidcProviderConfig.class); + } + OidcProviderConfig getOidcProviderConfig(String providerId) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs/" + providerId); + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId)); return sendRequest("GET", url, null, OidcProviderConfig.class); } void deleteProviderConfig(String providerId) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs/" + providerId); + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId)); sendRequest("DELETE", url, null, GenericJson.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())); + } + 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 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/OidcProviderConfig.java b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java index 7fc148e19..689298b74 100644 --- a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java +++ b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java @@ -22,6 +22,7 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.firebase.auth.ProviderConfig.AbstractCreateRequest; +import com.google.firebase.auth.ProviderConfig.AbstractUpdateRequest; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; @@ -48,6 +49,24 @@ 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()); + } + + private static void assertValidUrl(String url) throws IllegalArgumentException { + try { + new URL(url); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(url + " is a malformed URL", e); + } + } + /** * A specification class for creating a new OIDC Auth provider. * @@ -83,11 +102,7 @@ public CreateRequest setClientId(String clientId) { */ public CreateRequest setIssuer(String issuer) { checkArgument(!Strings.isNullOrEmpty(issuer), "issuer must not be null or empty"); - try { - new URL(issuer); - } catch (MalformedURLException e) { - throw new IllegalArgumentException(issuer + " is a malformed URL", e); - } + assertValidUrl(issuer); properties.put("issuer", issuer); return this; } @@ -96,4 +111,56 @@ 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 update the provider + * information persistently. + * + * @param tenantId a non-null, non-empty provider ID string. + * @throws IllegalArgumentException If the provider ID is null or empty. + */ + public UpdateRequest(String providerId) { + super(providerId); + } + + /** + * Sets the client ID for the exsting provider. + * + * @param clientId a non-null, non-empty client ID string. + */ + 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 string. + */ + 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 index 07820ff9c..434daab2f 100644 --- a/src/main/java/com/google/firebase/auth/ProviderConfig.java +++ b/src/main/java/com/google/firebase/auth/ProviderConfig.java @@ -104,4 +104,49 @@ Map getProperties() { 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. + */ + 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/Tenant.java b/src/main/java/com/google/firebase/auth/Tenant.java index 29bf3c508..61f52572d 100644 --- a/src/main/java/com/google/firebase/auth/Tenant.java +++ b/src/main/java/com/google/firebase/auth/Tenant.java @@ -60,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/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index a778b8ce0..71f9426c3 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -1074,6 +1074,7 @@ public void testOidcProviderConfigLifecycle() throws Exception { OidcProviderConfig config = auth.createOidcProviderConfigAsync(createRequest).get(); assertEquals(providerId, config.getProviderId()); assertEquals("DisplayName", config.getDisplayName()); + assertTrue(config.isEnabled()); assertEquals("ClientId", config.getClientId()); assertEquals("https://oidc.com/issuer", config.getIssuer()); @@ -1082,11 +1083,23 @@ public void testOidcProviderConfigLifecycle() throws Exception { 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()); - // TODO(micahstairs): Test updateProviderConfig operation - + // Update config provider + 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()); } finally { // Delete config provider auth.deleteProviderConfigAsync(providerId).get(); @@ -1129,8 +1142,19 @@ public void testTenantAwareOidcProviderConfigLifecycle() throws Exception { assertEquals("ClientId", config.getClientId()); assertEquals("https://oidc.com/issuer", config.getIssuer()); - // TODO(micahstairs): Test updateProviderConfig operation - + // Update config provider + 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()); } finally { // Delete config provider tenantAwareAuth.deleteProviderConfigAsync(providerId).get(); diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 01ea7a5d1..497f27f81 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -1716,6 +1716,108 @@ public void testTenantAwareCreateOidcProvider() throws Exception { 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); + 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); + 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); + 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( diff --git a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java index e5cd94ab3..2c62e5746 100644 --- a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java +++ b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java @@ -71,7 +71,56 @@ public void testCreateRequest() throws IOException { } @Test(expected = IllegalArgumentException.class) - public void testInvalidIssuerUrl() { + public void testCreateRequestMissingClientId() { + new OidcProviderConfig.CreateRequest().setClientId(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 testUpdateRequestMissingClientId() { + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setClientId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestInvalidIssuerUrl() { + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setIssuer("not a valid url"); + } } From 9052434b1e1b91fd61783d8162e5297e747e5972 Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Tue, 5 May 2020 16:57:42 -0400 Subject: [PATCH 06/21] Add operation to list OIDC provider configs. (#404) This adds an operation to list OIDC provider configs (can be done using either the tenant-aware or standard Firebase client). This work is part of adding multi-tenancy support (see issue #332). --- .../firebase/auth/AbstractFirebaseAuth.java | 81 +++++- .../firebase/auth/FirebaseUserManager.java | 22 ++ .../auth/ListProviderConfigsPage.java | 256 ++++++++++++++++++ .../google/firebase/auth/ListTenantsPage.java | 2 +- .../google/firebase/auth/ListUsersPage.java | 8 +- .../ListOidcProviderConfigsResponse.java | 63 +++++ .../internal/ListProviderConfigsResponse.java | 37 +++ .../auth/internal/ListTenantsResponse.java | 1 - .../google/firebase/auth/FirebaseAuthIT.java | 134 +++++++++ .../auth/FirebaseUserManagerTest.java | 91 ++++++- .../firebase/auth/ListUsersPageTest.java | 33 +-- src/test/resources/listOidc.json | 15 + 12 files changed, 707 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java create mode 100644 src/main/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponse.java create mode 100644 src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java create mode 100644 src/test/resources/listOidc.json diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index 0d7ceabcc..c5944f6e7 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -29,8 +29,10 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.auth.FirebaseUserManager.EmailLinkType; import com.google.firebase.auth.FirebaseUserManager.UserImportRequest; +import com.google.firebase.auth.ListProviderConfigsPage; +import com.google.firebase.auth.ListProviderConfigsPage.DefaultOidcProviderConfigSource; +import com.google.firebase.auth.ListUsersPage; import com.google.firebase.auth.ListUsersPage.DefaultUserSource; -import com.google.firebase.auth.ListUsersPage.PageFactory; import com.google.firebase.auth.UserRecord; import com.google.firebase.auth.internal.FirebaseTokenFactory; import com.google.firebase.internal.CallableOperation; @@ -503,8 +505,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 { @@ -1170,6 +1172,7 @@ public OidcProviderConfig getOidcProviderConfig(@NonNull String providerId) /** * Similar to {@link #getOidcProviderConfig(String)} but performs the operation asynchronously. + * Page size will be limited to 100 provider configs. * * @param providerId A provider ID string. * @return An {@code ApiFuture} which will complete successfully with an @@ -1195,6 +1198,78 @@ protected OidcProviderConfig execute() throws FirebaseAuthException { }; } + /** + * Gets a page of OIDC Auth provider configs starting from the specified {@code pageToken}. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @param maxResults Maximum number of provider configs to include in the returned page. This may + * not exceed 100. + * @return A {@link ListProviderConfigsPage} instance. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public ListProviderConfigsPage listOidcProviderConfigs( + @Nullable String pageToken, int maxResults) throws FirebaseAuthException { + return listOidcProviderConfigsOp(pageToken, maxResults).call(); + } + + /** + * Similar to {@link #listlistOidcProviderConfigs(String)} but performs the operation + * asynchronously. Page size will be limited to 100 provider configs. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @return An {@code ApiFuture} which will complete successfully with a + * {@link ListProviderConfigsPage} instance. If an error occurs while retrieving provider + * config data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty. + */ + public ApiFuture> listOidcProviderConfigsAsync( + @Nullable String pageToken) { + return listOidcProviderConfigsAsync( + pageToken, + FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS); + } + + /** + * Similar to {@link #listOidcProviderConfigs(String, int)} but performs the operation + * asynchronously. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @param maxResults Maximum number of provider configs to include in the returned page. This may + * not exceed 100. + * @return An {@code ApiFuture} which will complete successfully with a + * {@link ListProviderConfigsPage} instance. If an error occurs while retrieving provider + * config data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + */ + public ApiFuture> 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 provider config identified by the specified provider ID. * diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 2d0a4cc0d..2b31d59a2 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -44,6 +44,7 @@ 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.ListTenantsResponse; import com.google.firebase.auth.internal.UploadAccountResponse; import com.google.firebase.internal.ApiClientUtils; @@ -98,6 +99,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; @@ -388,6 +390,26 @@ OidcProviderConfig getOidcProviderConfig(String providerId) throws FirebaseAuthE return sendRequest("GET", url, null, OidcProviderConfig.class); } + ListOidcProviderConfigsResponse listOidcProviderConfigs(int maxResults, String pageToken) + throws FirebaseAuthException { + ImmutableMap.Builder builder = + ImmutableMap.builder().put("pageSize", maxResults); + if (pageToken != null) { + checkArgument(!pageToken.equals( + ListTenantsPage.END_OF_LIST), "invalid end of list page token"); + builder.put("nextPageToken", pageToken); + } + + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs"); + url.putAll(builder.build()); + ListOidcProviderConfigsResponse response = + sendRequest("GET", url, null, ListOidcProviderConfigsResponse.class); + if (response == null) { + throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve provider configs."); + } + return response; + } + void deleteProviderConfig(String providerId) throws FirebaseAuthException { GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId)); sendRequest("DELETE", url, null, GenericJson.class); diff --git a/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java b/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java new file mode 100644 index 000000000..2ab7f736f --- /dev/null +++ b/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java @@ -0,0 +1,256 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.json.JsonFactory; +import com.google.api.gax.paging.Page; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.internal.DownloadAccountResponse; +import com.google.firebase.auth.internal.ListOidcProviderConfigsResponse; +import com.google.firebase.auth.internal.ListProviderConfigsResponse; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * Represents a page of {@link ProviderConfig} instances. + * + *

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); + } + } + + // TODO(micahstairs): Add DefaultSamlProviderConfigSource class. + + /** + * 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/internal/ListOidcProviderConfigsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponse.java new file mode 100644 index 000000000..dc1df5c77 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponse.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.internal; + +import com.google.api.client.util.Key; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.OidcProviderConfig; +import com.google.firebase.auth.Tenant; +import java.util.List; + +/** + * JSON data binding for ListOAuthIdpConfigsResponse messages sent by Google identity toolkit + * service. + */ +public final class ListOidcProviderConfigsResponse + implements ListProviderConfigsResponse { + + @Key("oauthIdpConfigs") + private List providerConfigs; + + @Key("nextPageToken") + private String pageToken; + + @VisibleForTesting + public ListOidcProviderConfigsResponse( + List providerConfigs, + String pageToken) { + this.providerConfigs = providerConfigs; + this.pageToken = pageToken; + } + + public ListOidcProviderConfigsResponse() { } + + @Override + public List getProviderConfigs() { + return providerConfigs == null ? ImmutableList.of() : providerConfigs; + } + + @Override + public boolean hasProviderConfigs() { + return providerConfigs != null && !providerConfigs.isEmpty(); + } + + @Override + public String getPageToken() { + return pageToken == null ? "" : pageToken; + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java new file mode 100644 index 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/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 71f9426c3..4aec7caa7 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -66,6 +66,8 @@ import org.junit.BeforeClass; import org.junit.Test; +// TODO(micahstairs): Move tenant-aware tests into a seperate class, so that we only need to +// create and destroy the tenant once. public class FirebaseAuthIT { private static final String VERIFY_CUSTOM_TOKEN_URL = @@ -1167,6 +1169,129 @@ public void testTenantAwareOidcProviderConfigLifecycle() throws Exception { } } + @Test + public void testListOidcProviderConfigs() throws Exception { + final List providerIds = new ArrayList<>(); + + try { + // Create provider configs + for (int i = 0; i < 3; i++) { + String providerId = "oidc.provider-id" + i; + providerIds.add(providerId); + OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest() + .setProviderId(providerId) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + auth.createOidcProviderConfig(createRequest); + } + + // Test list by batches + final AtomicInteger collected = new AtomicInteger(0); + ListProviderConfigsPage page = + auth.listOidcProviderConfigsAsync(null).get(); + while (page != null) { + for (OidcProviderConfig providerConfig : page.getValues()) { + if (checkProviderConfig(providerIds, providerConfig)) { + collected.incrementAndGet(); + } + } + page = page.getNextPage(); + } + assertEquals(providerIds.size(), collected.get()); + + // Test iterate all + collected.set(0); + page = auth.listOidcProviderConfigsAsync(null).get(); + for (OidcProviderConfig providerConfig : page.iterateAll()) { + if (checkProviderConfig(providerIds, providerConfig)) { + collected.incrementAndGet(); + } + } + assertEquals(providerIds.size(), collected.get()); + + // Test iterate async + collected.set(0); + final Semaphore semaphore = new Semaphore(0); + final AtomicReference 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 (checkProviderConfig(providerIds, providerConfig)) { + collected.incrementAndGet(); + } + } + semaphore.release(); + } + }, MoreExecutors.directExecutor()); + semaphore.acquire(); + assertEquals(providerIds.size(), collected.get()); + assertNull(error.get()); + } finally { + // Delete provider configs + for (String providerId : providerIds) { + auth.deleteProviderConfigAsync(providerId).get(); + } + } + } + + @Test + public void testTenantAwareListOidcProviderConfigs() throws Exception { + // Create tenant to use + TenantManager tenantManager = auth.getTenantManager(); + Tenant.CreateRequest tenantCreateRequest = + new Tenant.CreateRequest().setDisplayName("DisplayName"); + String tenantId = tenantManager.createTenant(tenantCreateRequest).getTenantId(); + TenantAwareFirebaseAuth tenantAwareAuth = auth.getTenantManager().getAuthForTenant(tenantId); + + try { + final List providerIds = new ArrayList<>(); + try { + + // Create provider configs + for (int i = 0; i < 3; i++) { + String providerId = "oidc.provider-id" + i; + providerIds.add(providerId); + OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest() + .setProviderId(providerId) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + tenantAwareAuth.createOidcProviderConfig(createRequest); + } + + // List provider configs + final AtomicInteger collected = new AtomicInteger(0); + ListProviderConfigsPage page = + tenantAwareAuth.listOidcProviderConfigsAsync(null).get(); + for (OidcProviderConfig providerConfig : page.iterateAll()) { + if (checkProviderConfig(providerIds, providerConfig)) { + collected.incrementAndGet(); + } + } + assertEquals(providerIds.size(), collected.get()); + + } finally { + // Delete provider configs + for (String providerId : providerIds) { + tenantAwareAuth.deleteProviderConfigAsync(providerId).get(); + } + } + } finally { + // Delete tenant + tenantManager.deleteTenantAsync(tenantId).get(); + } + } + private Map parseLinkParameters(String link) throws Exception { Map result = new HashMap<>(); int queryBegin = link.indexOf('?'); @@ -1297,6 +1422,15 @@ static RandomUser create() { } } + private boolean checkProviderConfig(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; + } + private static void assertOidcProviderConfigDoesNotExist( AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception { diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 497f27f81..1f544352d 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -1625,7 +1625,7 @@ public void testCreateOidcProvider() throws Exception { OidcProviderConfig config = FirebaseAuth.getInstance().createOidcProviderConfig(createRequest); - checkOidcProviderConfig(config); + checkOidcProviderConfig(config, "oidc.provider-id"); checkRequestHeaders(interceptor); checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); GenericJson parsed = parseRequestContent(interceptor); @@ -1641,11 +1641,11 @@ public void testCreateOidcProvider() throws Exception { public void testCreateOidcProviderMinimal() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement( TestUtils.loadResource("oidc.json")); - // Only the 'enabled' field can be omitted from an OIDC provider config creation request. + // Only the 'enabled' and 'displayName' fields can be omitted from an OIDC provider config + // creation request. OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest() .setProviderId("oidc.provider-id") - .setDisplayName("DISPLAY_NAME") .setClientId("CLIENT_ID") .setIssuer("https://oidc.com/issuer"); @@ -1653,7 +1653,7 @@ public void testCreateOidcProviderMinimal() throws Exception { checkRequestHeaders(interceptor); checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); GenericJson parsed = parseRequestContent(interceptor); - assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertNull(parsed.get("displayName")); assertNull(parsed.get("enabled")); assertEquals("CLIENT_ID", parsed.get("clientId")); assertEquals("https://oidc.com/issuer", parsed.get("issuer")); @@ -1729,7 +1729,7 @@ public void testUpdateOidcProvider() throws Exception { OidcProviderConfig config = FirebaseAuth.getInstance().updateOidcProviderConfig(request); - checkOidcProviderConfig(config); + checkOidcProviderConfig(config, "oidc.provider-id"); checkRequestHeaders(interceptor); checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); GenericUrl url = interceptor.getResponse().getRequest().getUrl(); @@ -1750,7 +1750,7 @@ public void testUpdateOidcProviderMinimal() throws Exception { OidcProviderConfig config = FirebaseAuth.getInstance().updateOidcProviderConfig(request); - checkOidcProviderConfig(config); + checkOidcProviderConfig(config, "oidc.provider-id"); checkRequestHeaders(interceptor); checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); GenericUrl url = interceptor.getResponse().getRequest().getUrl(); @@ -1805,7 +1805,7 @@ public void testTenantAwareUpdateOidcProvider() throws Exception { OidcProviderConfig config = tenantAwareAuth.updateOidcProviderConfig(request); - checkOidcProviderConfig(config); + checkOidcProviderConfig(config, "oidc.provider-id"); checkRequestHeaders(interceptor); String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/oidc.provider-id"; checkUrl(interceptor, "PATCH", expectedUrl); @@ -1826,7 +1826,7 @@ public void testGetOidcProviderConfig() throws Exception { OidcProviderConfig config = FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id"); - checkOidcProviderConfig(config); + checkOidcProviderConfig(config, "oidc.provider-id"); checkRequestHeaders(interceptor); checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); } @@ -1855,11 +1855,80 @@ public void testGetTenantAwareOidcProviderConfig() throws Exception { OidcProviderConfig config = tenantAwareAuth.getOidcProviderConfig("oidc.provider-id"); - checkOidcProviderConfig(config); + checkOidcProviderConfig(config, "oidc.provider-id"); checkRequestHeaders(interceptor); checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/oidc.provider-id"); } + @Test + public void testListOidcProviderConfigs() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listOidc.json")); + ListProviderConfigsPage 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 testDeleteProviderConfig() throws Exception { @@ -2015,8 +2084,8 @@ private static void checkTenant(Tenant tenant, String tenantId) { assertFalse(tenant.isEmailLinkSignInEnabled()); } - private static void checkOidcProviderConfig(OidcProviderConfig config) { - assertEquals("oidc.provider-id", config.getProviderId()); + private static void checkOidcProviderConfig(OidcProviderConfig config, String providerId) { + assertEquals(providerId, config.getProviderId()); assertEquals("DISPLAY_NAME", config.getDisplayName()); assertTrue(config.isEnabled()); assertEquals("CLIENT_ID", config.getClientId()); diff --git a/src/test/java/com/google/firebase/auth/ListUsersPageTest.java b/src/test/java/com/google/firebase/auth/ListUsersPageTest.java index 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/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" + } ] +} From 580eeddac96db7ae4a4b0836ddf176b2c63e1e8d Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Tue, 5 May 2020 17:41:52 -0400 Subject: [PATCH 07/21] Move tenant-aware integration tests to separate class (#405) Move tenant-aware integration tests to separate class. This simplifies the setup and teardown required for these tests. --- .../google/firebase/auth/FirebaseAuthIT.java | 363 -------------- .../auth/TenantAwareFirebaseAuthIT.java | 442 ++++++++++++++++++ 2 files changed, 442 insertions(+), 363 deletions(-) create mode 100644 src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index 4aec7caa7..e16f0c59a 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -66,8 +66,6 @@ import org.junit.BeforeClass; import org.junit.Test; -// TODO(micahstairs): Move tenant-aware tests into a seperate class, so that we only need to -// create and destroy the tenant once. public class FirebaseAuthIT { private static final String VERIFY_CUSTOM_TOKEN_URL = @@ -423,209 +421,6 @@ public void onSuccess(ListUsersPage result) { } } - @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()); - } - - // 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()); - } 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(); @@ -809,58 +604,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(); @@ -1110,65 +853,6 @@ public void testOidcProviderConfigLifecycle() throws Exception { assertOidcProviderConfigDoesNotExist(auth, providerId); } - @Test - public void testTenantAwareOidcProviderConfigLifecycle() 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 config provider - TenantAwareFirebaseAuth tenantAwareAuth = auth.getTenantManager().getAuthForTenant(tenantId); - String providerId = "oidc.provider-id"; - OidcProviderConfig.CreateRequest createRequest = - new OidcProviderConfig.CreateRequest() - .setProviderId(providerId) - .setDisplayName("DisplayName") - .setEnabled(true) - .setClientId("ClientId") - .setIssuer("https://oidc.com/issuer"); - OidcProviderConfig config = - tenantAwareAuth.createOidcProviderConfigAsync(createRequest).get(); - assertEquals(providerId, config.getProviderId()); - assertEquals("DisplayName", config.getDisplayName()); - assertEquals("ClientId", config.getClientId()); - assertEquals("https://oidc.com/issuer", config.getIssuer()); - - try { - // Get config provider - 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 config provider - 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()); - } finally { - // Delete config provider - tenantAwareAuth.deleteProviderConfigAsync(providerId).get(); - } - - assertOidcProviderConfigDoesNotExist(tenantAwareAuth, providerId); - } finally { - // Delete tenant. - tenantManager.deleteTenantAsync(tenantId).get(); - } - } - @Test public void testListOidcProviderConfigs() throws Exception { final List providerIds = new ArrayList<>(); @@ -1245,53 +929,6 @@ public void onSuccess(ListProviderConfigsPage result) { } } - @Test - public void testTenantAwareListOidcProviderConfigs() throws Exception { - // Create tenant to use - TenantManager tenantManager = auth.getTenantManager(); - Tenant.CreateRequest tenantCreateRequest = - new Tenant.CreateRequest().setDisplayName("DisplayName"); - String tenantId = tenantManager.createTenant(tenantCreateRequest).getTenantId(); - TenantAwareFirebaseAuth tenantAwareAuth = auth.getTenantManager().getAuthForTenant(tenantId); - - try { - final List providerIds = new ArrayList<>(); - try { - - // Create provider configs - for (int i = 0; i < 3; i++) { - String providerId = "oidc.provider-id" + i; - providerIds.add(providerId); - OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest() - .setProviderId(providerId) - .setClientId("CLIENT_ID") - .setIssuer("https://oidc.com/issuer"); - tenantAwareAuth.createOidcProviderConfig(createRequest); - } - - // List provider configs - final AtomicInteger collected = new AtomicInteger(0); - ListProviderConfigsPage page = - tenantAwareAuth.listOidcProviderConfigsAsync(null).get(); - for (OidcProviderConfig providerConfig : page.iterateAll()) { - if (checkProviderConfig(providerIds, providerConfig)) { - collected.incrementAndGet(); - } - } - assertEquals(providerIds.size(), collected.get()); - - } finally { - // Delete provider configs - for (String providerId : providerIds) { - tenantAwareAuth.deleteProviderConfigAsync(providerId).get(); - } - } - } finally { - // Delete tenant - tenantManager.deleteTenantAsync(tenantId).get(); - } - } - private Map parseLinkParameters(String link) throws Exception { Map result = new HashMap<>(); int queryBegin = link.indexOf('?'); 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..32572c5a8 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java @@ -0,0 +1,442 @@ +/* + * 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.auth.oauth2.GoogleCredentials; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.firebase.FirebaseApp; +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.Random; +import java.util.UUID; +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.Test; + +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 FirebaseAuth auth; + private static TenantManager tenantManager; + private static TenantAwareFirebaseAuth tenantAwareAuth; + private static String tenantId; + + @BeforeClass + public static void setUpClass() throws Exception { + FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp(); + auth = FirebaseAuth.getInstance(masterApp); + tenantManager = auth.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 = 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 + tenantAwareAuth.deleteUserAsync(userRecord.getUid()).get(); + assertUserDoesNotExist(tenantAwareAuth, userRecord.getUid()); + } + + @Test + 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(tenantAwareAuth.createUserAsync(createRequest).get().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()); + } finally { + for (String uid : uids) { + tenantAwareAuth.deleteUserAsync(uid).get(); + } + } + } + + @Test + public void testGetUserWithMultipleTenantIds() throws Exception { + // Create second tenant. + Tenant.CreateRequest tenantCreateRequest = + new Tenant.CreateRequest().setDisplayName("DisplayName2"); + String tenantId2 = tenantManager.createTenant(tenantCreateRequest).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); + UserRecord tenantUserRecord1 = tenantAwareAuth.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(tenantAwareAuth, nonTenantUserRecord.getUid()); + assertUserDoesNotExist(tenantAwareAuth, tenantUserRecord2.getUid()); + assertUserDoesNotExist(tenantAwareAuth2, nonTenantUserRecord.getUid()); + assertUserDoesNotExist(tenantAwareAuth2, tenantUserRecord1.getUid()); + + // Make sure tenant-aware client can fetch users under that tenant. + assertNotNull(tenantAwareAuth.getUser(tenantUserRecord1.getUid())); + assertNotNull(tenantAwareAuth2.getUser(tenantUserRecord2.getUid())); + + // Delete second tenant. + tenantManager.deleteTenant(tenantId2); + } + + @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 { + auth.getTenantManager().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 config provider + String providerId = "oidc.provider-id"; + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setProviderId(providerId) + .setDisplayName("DisplayName") + .setEnabled(true) + .setClientId("ClientId") + .setIssuer("https://oidc.com/issuer"); + OidcProviderConfig config = tenantAwareAuth.createOidcProviderConfigAsync(createRequest).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertEquals("ClientId", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + + try { + // Get config provider + 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 config provider + 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()); + } finally { + // Delete config provider + tenantAwareAuth.deleteProviderConfigAsync(providerId).get(); + assertOidcProviderConfigDoesNotExist(tenantAwareAuth, providerId); + } + } + + @Test + public void testListOidcProviderConfigs() throws Exception { + final List providerIds = new ArrayList<>(); + try { + // Create provider configs + for (int i = 0; i < 3; i++) { + String providerId = "oidc.provider-id" + i; + providerIds.add(providerId); + OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest() + .setProviderId(providerId) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + tenantAwareAuth.createOidcProviderConfig(createRequest); + } + + // List provider configs + final AtomicInteger collected = new AtomicInteger(0); + ListProviderConfigsPage 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()); + + } finally { + // Delete provider configs + for (String providerId : providerIds) { + tenantAwareAuth.deleteProviderConfigAsync(providerId).get(); + } + } + } + + private 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, @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(); + } + } + + private static class RandomUser { + private final String uid; + private final String email; + + private RandomUser(String uid, String email) { + this.uid = uid; + this.email = email; + } + + 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 static void assertOidcProviderConfigDoesNotExist( + AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception { + try { + firebaseAuth.getOidcProviderConfigAsync(providerId).get(); + fail("No error thrown for getting a deleted provider config"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } + + 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"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } +} From 36c27bece218a71bfd4a0f9ed90f6f2ae6b1961f Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Thu, 7 May 2020 08:20:48 -0400 Subject: [PATCH 08/21] Add missing listOidcProviderConfigs method. (#406) Each asynchronous method is supposed to have a synchronous counterpart. This was supposed to be included when I initially added the operation to list OIDC provider configs. --- .../firebase/auth/AbstractFirebaseAuth.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index c5944f6e7..63d35d2ab 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -1198,6 +1198,22 @@ protected OidcProviderConfig execute() throws FirebaseAuthException { }; } + /** + * Gets a page of OIDC Auth provider configs starting from the specified {@code pageToken}. Page + * size will be limited to 100 provider configs. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @return 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 OIDC Auth provider configs starting from the specified {@code pageToken}. * @@ -1208,7 +1224,7 @@ protected OidcProviderConfig execute() throws FirebaseAuthException { * @return A {@link ListProviderConfigsPage} instance. * @throws IllegalArgumentException If the specified page token is empty, or max results value is * invalid. - * @throws FirebaseAuthException If an error occurs while retrieving user data. + * @throws FirebaseAuthException If an error occurs while retrieving provider config data. */ public ListProviderConfigsPage listOidcProviderConfigs( @Nullable String pageToken, int maxResults) throws FirebaseAuthException { @@ -1228,9 +1244,8 @@ public ListProviderConfigsPage listOidcProviderConfigs( */ public ApiFuture> listOidcProviderConfigsAsync( @Nullable String pageToken) { - return listOidcProviderConfigsAsync( - pageToken, - FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS); + int maxResults = FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS; + return listOidcProviderConfigsAsync(pageToken, maxResults); } /** From 732a90870a4503cf9232b75dcf1781bf4601498c Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Tue, 12 May 2020 14:00:28 -0400 Subject: [PATCH 09/21] Add validation to provider config ID. (#410) The OIDC Auth provider config ID must begin with "oidc.". This validation is being done in other APIs (e.g. Go), so we should do it here as well. I've moved assertValidUrl to the base class so that it can be reused for SamlProviderConfig, once that class is added. --- .../firebase/auth/OidcProviderConfig.java | 29 ++++++------------- .../google/firebase/auth/ProviderConfig.java | 18 +++++++++--- .../firebase/auth/OidcProviderConfigTest.java | 5 ++++ 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java index 689298b74..e2587dad2 100644 --- a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java +++ b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java @@ -20,13 +20,8 @@ import com.google.api.client.util.Key; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; import com.google.firebase.auth.ProviderConfig.AbstractCreateRequest; import com.google.firebase.auth.ProviderConfig.AbstractUpdateRequest; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; /** * Contains metadata associated with an OIDC Auth provider. @@ -59,14 +54,6 @@ public UpdateRequest updateRequest() { return new UpdateRequest(getProviderId()); } - private static void assertValidUrl(String url) throws IllegalArgumentException { - try { - new URL(url); - } catch (MalformedURLException e) { - throw new IllegalArgumentException(url + " is a malformed URL", e); - } - } - /** * A specification class for creating a new OIDC Auth provider. * @@ -90,7 +77,7 @@ public CreateRequest() { } * @param clientId a non-null, non-empty client ID string. */ public CreateRequest setClientId(String clientId) { - checkArgument(!Strings.isNullOrEmpty(clientId), "client ID must not be null or empty"); + checkArgument(!Strings.isNullOrEmpty(clientId), "Client ID must not be null or empty."); properties.put("clientId", clientId); return this; } @@ -98,10 +85,10 @@ public CreateRequest setClientId(String clientId) { /** * Sets the issuer for the new provider. * - * @param issuer a non-null, non-empty issuer string. + * @param issuer a non-null, non-empty issuer URL string. */ public CreateRequest setIssuer(String issuer) { - checkArgument(!Strings.isNullOrEmpty(issuer), "issuer must not be null or empty"); + checkArgument(!Strings.isNullOrEmpty(issuer), "Issuer must not be null or empty."); assertValidUrl(issuer); properties.put("issuer", issuer); return this; @@ -130,10 +117,12 @@ public static final class UpdateRequest extends AbstractUpdateRequest properties = new HashMap<>(); AbstractUpdateRequest(String providerId) { - checkArgument(!Strings.isNullOrEmpty(providerId), "provider ID must not be null or empty"); + checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); this.providerId = providerId; } @@ -128,7 +138,7 @@ String getProviderId() { * @param displayName a non-null, non-empty display name string. */ public T setDisplayName(String displayName) { - checkArgument(!Strings.isNullOrEmpty(displayName), "display name must not be null or empty"); + checkArgument(!Strings.isNullOrEmpty(displayName), "Display name must not be null or empty."); properties.put("displayName", displayName); return getThis(); } diff --git a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java index 2c62e5746..8079c5deb 100644 --- a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java +++ b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java @@ -114,6 +114,11 @@ 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 testUpdateRequestMissingClientId() { new OidcProviderConfig.UpdateRequest("oidc.provider-id").setClientId(null); From c323314c1e872f0d2942fc27746bf3d3c1b4069a Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 12 May 2020 11:37:38 -0700 Subject: [PATCH 10/21] fix(auth): Ensuring test user account cleanup with a Rule (#409) * fix(auth): Ensuring test user account cleanup with a Rule * Updated copyright holder --- .../google/firebase/auth/FirebaseAuthIT.java | 451 +++++++----------- .../auth/TenantAwareFirebaseAuthIT.java | 1 - .../google/firebase/auth/TenantManagerIT.java | 155 ++++++ 3 files changed, 324 insertions(+), 283 deletions(-) create mode 100644 src/test/java/com/google/firebase/auth/TenantManagerIT.java diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index e16f0c59a..754616a24 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -63,8 +63,9 @@ 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 +81,11 @@ 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(); @Test public void testGetNonExistingUser() throws Exception { @@ -221,28 +220,24 @@ public void testCreateUserWithParams() throws Exception { .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.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()); - 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.uid); } @Test @@ -348,227 +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 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()); + 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); - ListTenantsPage page = tenantManager.listTenantsAsync(null).get(); - while (page != null) { - for (Tenant tenant : page.getValues()) { - if (tenantIds.contains(tenant.getTenantId())) { + @Override + public void onSuccess(ListUsersPage result) { + for (ExportedUserRecord user : result.iterateAll()) { + if (uids.contains(user.getUid())) { 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()); - } + assertNotNull(user.getPasswordHash()); + assertNotNull(user.getPasswordSalt()); + 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 @@ -687,15 +560,12 @@ public void testImportUsers() throws Exception { .build(); UserImportResult result = auth.importUsersAsync(ImmutableList.of(user)).get(); + temporaryUser.registerUid(randomUser.uid); 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.uid).get(); + assertEquals(randomUser.email, savedUser.getEmail()); } @Test @@ -721,88 +591,73 @@ public void testImportUsersWithPassword() throws Exception { .setRounds(8) .setMemoryCost(14) .build())).get(); + temporaryUser.registerUid(randomUser.uid); 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.uid).get(); + assertEquals(randomUser.email, savedUser.getEmail()); + String idToken = signInWithPassword(randomUser.email, "password"); + assertFalse(Strings.isNullOrEmpty(idToken)); } @Test public void testGeneratePasswordResetLink() throws Exception { RandomUser user = RandomUser.create(); - auth.createUser(new UserRecord.CreateRequest() + temporaryUser.create(new UserRecord.CreateRequest() .setUid(user.uid) .setEmail(user.email) .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.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()); } @Test public void testGenerateEmailVerificationResetLink() throws Exception { RandomUser user = RandomUser.create(); - auth.createUser(new UserRecord.CreateRequest() + temporaryUser.create(new UserRecord.CreateRequest() .setUid(user.uid) .setEmail(user.email) .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.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")); } @Test public void testGenerateSignInWithEmailLink() throws Exception { RandomUser user = RandomUser.create(); - auth.createUser(new UserRecord.CreateRequest() + temporaryUser.create(new UserRecord.CreateRequest() .setUid(user.uid) .setEmail(user.email) .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.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()); } @Test @@ -1109,4 +964,36 @@ static UserRecord newUserWithParams(FirebaseAuth auth) throws Exception { .setPhotoUrl("https://example.com/photo.png") .setPassword("password")); } + + /** + * Creates temporary Firebase user accounts for testing, and deletes them at the end of each + * test case. + */ + private static final class TemporaryUser extends ExternalResource { + + private final List users = new ArrayList<>(); + + 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/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java index 32572c5a8..ddfc9fe7e 100644 --- a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java @@ -35,7 +35,6 @@ import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutureCallback; import com.google.api.core.ApiFutures; -import com.google.auth.oauth2.GoogleCredentials; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.MoreExecutors; import com.google.firebase.FirebaseApp; 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(); + } + } + } +} From 7b550455cd4c6159a05222db01656c23da85c6ac Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Wed, 13 May 2020 19:45:06 -0400 Subject: [PATCH 11/21] Rename OIDC delete operation and refactor integration tests. (#411) I've renamed the delete operation since we need separate methods for deleting OIDC and SAML provider configs. I've also created a ProviderConfigTestUtils class to house shared logic between TenantAwareFirebaseAuthIT and FirebaseAuthIT, and I've added a TemporaryProviderConfig class to make it easier for provider configs to be cleaned up automatically. I've structured this class in such a way that we can easily tack on logic to cleanup SAML provider configs as well. I've also decided to remove testGetUserWithMultipleTenantIds. I initially wrote this test to understand how tenant-aware auths and tenant-agnostic auths interacted with one another. In its existing state, it appears to be leaking created users, and in order to resolve this with TemporaryUser, we would need to declare multiple TemporaryUser objects as rules. I think it's better to avoid this complexity and remove this test. After all, this integration test is mainly making assertions about the server's behavior and not our behavior. --- .../firebase/auth/AbstractFirebaseAuth.java | 24 +-- .../firebase/auth/FirebaseUserManager.java | 12 +- .../google/firebase/auth/FirebaseAuthIT.java | 187 ++++++++---------- .../auth/FirebaseUserManagerTest.java | 6 +- .../auth/ProviderConfigTestUtils.java | 82 ++++++++ .../auth/TenantAwareFirebaseAuthIT.java | 175 ++++++---------- 6 files changed, 251 insertions(+), 235 deletions(-) create mode 100644 src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index 63d35d2ab..9fef3758f 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -1107,7 +1107,7 @@ public ApiFuture createOidcProviderConfigAsync( private CallableOperation createOidcProviderConfigOp(final OidcProviderConfig.CreateRequest request) { checkNotDestroyed(); - checkNotNull(request, "create request must not be null"); + checkNotNull(request, "Create request must not be null."); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override @@ -1147,7 +1147,7 @@ public ApiFuture updateOidcProviderConfigAsync( private CallableOperation updateOidcProviderConfigOp( final OidcProviderConfig.UpdateRequest request) { checkNotDestroyed(); - checkNotNull(request, "update request must not be null"); + checkNotNull(request, "Update request must not be null."); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override @@ -1188,7 +1188,7 @@ public ApiFuture getOidcProviderConfigAsync(@NonNull String private CallableOperation getOidcProviderConfigOp(final String providerId) { checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(providerId), "provider ID must not be null or empty"); + checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override @@ -1286,18 +1286,18 @@ protected ListProviderConfigsPage execute() } /** - * Deletes the provider config identified by the specified provider ID. + * Deletes the OIDC 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. * @throws FirebaseAuthException If an error occurs while deleting the provider config. */ - public void deleteProviderConfig(@NonNull String providerId) throws FirebaseAuthException { - deleteProviderConfigOp(providerId).call(); + public void deleteOidcProviderConfig(@NonNull String providerId) throws FirebaseAuthException { + deleteOidcProviderConfigOp(providerId).call(); } /** - * Similar to {@link #deleteProviderConfig} but performs the operation asynchronously. + * 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 @@ -1305,19 +1305,19 @@ public void deleteProviderConfig(@NonNull String providerId) throws FirebaseAuth * throws a {@link FirebaseAuthException}. * @throws IllegalArgumentException If the provider ID string is null or empty. */ - public ApiFuture deleteProviderConfigAsync(String providerId) { - return deleteProviderConfigOp(providerId).callAsync(firebaseApp); + public ApiFuture deleteOidcProviderConfigAsync(String providerId) { + return deleteOidcProviderConfigOp(providerId).callAsync(firebaseApp); } - private CallableOperation deleteProviderConfigOp( + private CallableOperation deleteOidcProviderConfigOp( final String providerId) { checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(providerId), "provider ID must not be null or empty"); + checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override protected Void execute() throws FirebaseAuthException { - userManager.deleteProviderConfig(providerId); + userManager.deleteOidcProviderConfig(providerId); return null; } }; diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 2b31d59a2..03d957bff 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -369,7 +369,7 @@ OidcProviderConfig createOidcProviderConfig( OidcProviderConfig.CreateRequest request) throws FirebaseAuthException { GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs"); String providerId = request.getProviderId(); - checkArgument(!Strings.isNullOrEmpty(providerId), "provider ID must not be null or empty"); + checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); url.set("oauthIdpConfigId", providerId); return sendRequest("POST", url, request.getProperties(), OidcProviderConfig.class); } @@ -378,7 +378,7 @@ OidcProviderConfig updateOidcProviderConfig(OidcProviderConfig.UpdateRequest req throws FirebaseAuthException { Map properties = request.getProperties(); checkArgument(!properties.isEmpty(), - "provider config update must have at least one property set"); + "Provider config update must have at least one property set."); GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(request.getProviderId())); url.put("updateMask", generateMask(properties)); @@ -396,7 +396,7 @@ ListOidcProviderConfigsResponse listOidcProviderConfigs(int maxResults, String p ImmutableMap.builder().put("pageSize", maxResults); if (pageToken != null) { checkArgument(!pageToken.equals( - ListTenantsPage.END_OF_LIST), "invalid end of list page token"); + ListTenantsPage.END_OF_LIST), "Invalid end of list page token."); builder.put("nextPageToken", pageToken); } @@ -410,7 +410,7 @@ ListOidcProviderConfigsResponse listOidcProviderConfigs(int maxResults, String p return response; } - void deleteProviderConfig(String providerId) throws FirebaseAuthException { + void deleteOidcProviderConfig(String providerId) throws FirebaseAuthException { GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId)); sendRequest("DELETE", url, null, GenericJson.class); } @@ -424,12 +424,12 @@ private static String generateMask(Map properties) { } private static String getTenantUrlSuffix(String tenantId) { - checkArgument(!Strings.isNullOrEmpty(tenantId), "tenant ID must not be null or empty"); + 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"); + checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); return "/oauthIdpConfigs/" + providerId; } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index 754616a24..9e24eff1f 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -46,6 +46,7 @@ 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.hash.Scrypt; import com.google.firebase.internal.Nullable; import com.google.firebase.testing.IntegrationTestUtils; @@ -84,8 +85,9 @@ public class FirebaseAuthIT { private static final FirebaseAuth auth = FirebaseAuth.getInstance( IntegrationTestUtils.ensureDefaultApp()); - @Rule - public final TemporaryUser temporaryUser = new TemporaryUser(); + @Rule public final TemporaryUser temporaryUser = new TemporaryUser(); + @Rule public final TemporaryProviderConfig temporaryProviderConfig = + new TemporaryProviderConfig(auth); @Test public void testGetNonExistingUser() throws Exception { @@ -664,124 +666,113 @@ public void testGenerateSignInWithEmailLink() throws Exception { public void testOidcProviderConfigLifecycle() throws Exception { // Create config provider String providerId = "oidc.provider-id"; - OidcProviderConfig.CreateRequest createRequest = + OidcProviderConfig config = temporaryProviderConfig.createOidcProviderConfig( new OidcProviderConfig.CreateRequest() .setProviderId(providerId) .setDisplayName("DisplayName") .setEnabled(true) .setClientId("ClientId") - .setIssuer("https://oidc.com/issuer"); - OidcProviderConfig config = auth.createOidcProviderConfigAsync(createRequest).get(); + .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()); - try { - // Get config provider - 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()); + // Get config provider + 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 config provider - 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()); - } finally { - // Delete config provider - auth.deleteProviderConfigAsync(providerId).get(); - } + // Update config provider + 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()); - assertOidcProviderConfigDoesNotExist(auth, providerId); + // Delete config provider + auth.deleteOidcProviderConfigAsync(providerId).get(); + ProviderConfigTestUtils.assertOidcProviderConfigDoesNotExist(auth, providerId); } @Test public void testListOidcProviderConfigs() throws Exception { final List providerIds = new ArrayList<>(); - try { - // Create provider configs - for (int i = 0; i < 3; i++) { - String providerId = "oidc.provider-id" + i; - providerIds.add(providerId); - OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest() + // Create provider configs + for (int i = 0; i < 3; i++) { + String providerId = "oidc.provider-id" + i; + providerIds.add(providerId); + OidcProviderConfig config = temporaryProviderConfig.createOidcProviderConfig( + new OidcProviderConfig.CreateRequest() .setProviderId(providerId) .setClientId("CLIENT_ID") - .setIssuer("https://oidc.com/issuer"); - auth.createOidcProviderConfig(createRequest); - } - - // Test list by batches - final AtomicInteger collected = new AtomicInteger(0); - ListProviderConfigsPage page = - auth.listOidcProviderConfigsAsync(null).get(); - while (page != null) { - for (OidcProviderConfig providerConfig : page.getValues()) { - if (checkProviderConfig(providerIds, providerConfig)) { - collected.incrementAndGet(); - } - } - page = page.getNextPage(); - } - assertEquals(providerIds.size(), collected.get()); + .setIssuer("https://oidc.com/issuer")); + } - // Test iterate all - collected.set(0); - page = auth.listOidcProviderConfigsAsync(null).get(); - for (OidcProviderConfig providerConfig : page.iterateAll()) { + // 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 (checkProviderConfig(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(); - } + page = page.getNextPage(); + } + assertEquals(providerIds.size(), collected.get()); - @Override - public void onSuccess(ListProviderConfigsPage result) { - for (OidcProviderConfig providerConfig : result.iterateAll()) { - if (checkProviderConfig(providerIds, providerConfig)) { - collected.incrementAndGet(); - } - } - semaphore.release(); - } - }, MoreExecutors.directExecutor()); - semaphore.acquire(); - assertEquals(providerIds.size(), collected.get()); - assertNull(error.get()); - } finally { - // Delete provider configs - for (String providerId : providerIds) { - auth.deleteProviderConfigAsync(providerId).get(); + // Test iterate all + collected.set(0); + page = auth.listOidcProviderConfigsAsync(null).get(); + for (OidcProviderConfig providerConfig : page.iterateAll()) { + if (checkProviderConfig(providerIds, providerConfig)) { + collected.incrementAndGet(); } } + assertEquals(providerIds.size(), collected.get()); + + // Test iterate async + collected.set(0); + final Semaphore semaphore = new Semaphore(0); + final AtomicReference 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 (checkProviderConfig(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 { @@ -924,23 +915,11 @@ private boolean checkProviderConfig(List providerIds, OidcProviderConfig } - private static void assertOidcProviderConfigDoesNotExist( - AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception { - try { - firebaseAuth.getOidcProviderConfigAsync(providerId).get(); - fail("No error thrown for getting a deleted provider config"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); - } - } - 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, diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 1f544352d..b18662e9d 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -1934,7 +1934,7 @@ public void testTenantAwareListOidcProviderConfigs() throws Exception { public void testDeleteProviderConfig() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); - FirebaseAuth.getInstance().deleteProviderConfig("PROVIDER_ID"); + FirebaseAuth.getInstance().deleteOidcProviderConfig("PROVIDER_ID"); checkRequestHeaders(interceptor); checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/PROVIDER_ID"); @@ -1946,7 +1946,7 @@ public void testDeleteProviderConfigWithNotFoundError() throws Exception { initializeAppForUserManagementWithStatusCode(404, "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); try { - FirebaseAuth.getInstance().deleteProviderConfig("UNKNOWN"); + FirebaseAuth.getInstance().deleteOidcProviderConfig("UNKNOWN"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); @@ -1962,7 +1962,7 @@ public void testTenantAwareDeleteProviderConfig() throws Exception { TenantAwareFirebaseAuth tenantAwareAuth = FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); - tenantAwareAuth.deleteProviderConfig("PROVIDER_ID"); + tenantAwareAuth.deleteOidcProviderConfig("PROVIDER_ID"); checkRequestHeaders(interceptor); checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/PROVIDER_ID"); 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..0cc636d47 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java @@ -0,0 +1,82 @@ +/* + * 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()); + } + } + + /** + * 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<>(); + + 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 user."); + auth.deleteOidcProviderConfig(providerId); + oidcIds.remove(providerId); + } + + @Override + protected synchronized void after() { + for (String id : oidcIds) { + try { + auth.deleteOidcProviderConfig(id); + } catch (Exception ignore) { + // Ignore + } + } + oidcIds.clear(); + } + } +} + diff --git a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java index ddfc9fe7e..e585f87b9 100644 --- a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java @@ -38,6 +38,7 @@ 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.internal.Nullable; import com.google.firebase.testing.IntegrationTestUtils; import java.io.IOException; @@ -52,7 +53,9 @@ import org.junit.AfterClass; import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExternalResource; public class TenantAwareFirebaseAuthIT { @@ -61,16 +64,17 @@ public class TenantAwareFirebaseAuthIT { private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); private static final HttpTransport transport = Utils.getDefaultTransport(); - private static FirebaseAuth auth; private static TenantManager tenantManager; private static TenantAwareFirebaseAuth tenantAwareAuth; private static String tenantId; + @Rule public final TemporaryProviderConfig temporaryProviderConfig = + new TemporaryProviderConfig(tenantAwareAuth); + @BeforeClass public static void setUpClass() throws Exception { FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp(); - auth = FirebaseAuth.getInstance(masterApp); - tenantManager = auth.getTenantManager(); + tenantManager = FirebaseAuth.getInstance(masterApp).getTenantManager(); Tenant.CreateRequest tenantCreateRequest = new Tenant.CreateRequest().setDisplayName("DisplayName"); tenantId = tenantManager.createTenant(tenantCreateRequest).getTenantId(); @@ -85,6 +89,7 @@ public static void tearDownClass() throws Exception { @Test public void testUserLifecycle() throws Exception { // Create user + // TODO(micahstairs): Use a temporary user here. UserRecord userRecord = tenantAwareAuth.createUserAsync(new UserRecord.CreateRequest()).get(); String uid = userRecord.getUid(); @@ -158,6 +163,7 @@ public void testListUsers() throws Exception { try { for (int i = 0; i < 3; i++) { + // TODO(micahstairs): Use a temporary user here. UserRecord.CreateRequest createRequest = new UserRecord.CreateRequest().setPassword("password"); uids.add(tenantAwareAuth.createUserAsync(createRequest).get().getUid()); @@ -229,39 +235,6 @@ public void onSuccess(ListUsersPage result) { } } - @Test - public void testGetUserWithMultipleTenantIds() throws Exception { - // Create second tenant. - Tenant.CreateRequest tenantCreateRequest = - new Tenant.CreateRequest().setDisplayName("DisplayName2"); - String tenantId2 = tenantManager.createTenant(tenantCreateRequest).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); - UserRecord tenantUserRecord1 = tenantAwareAuth.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(tenantAwareAuth, nonTenantUserRecord.getUid()); - assertUserDoesNotExist(tenantAwareAuth, tenantUserRecord2.getUid()); - assertUserDoesNotExist(tenantAwareAuth2, nonTenantUserRecord.getUid()); - assertUserDoesNotExist(tenantAwareAuth2, tenantUserRecord1.getUid()); - - // Make sure tenant-aware client can fetch users under that tenant. - assertNotNull(tenantAwareAuth.getUser(tenantUserRecord1.getUid())); - assertNotNull(tenantAwareAuth2.getUser(tenantUserRecord2.getUid())); - - // Delete second tenant. - tenantManager.deleteTenant(tenantId2); - } - @Test public void testCustomToken() throws Exception { String customToken = tenantAwareAuth.createCustomTokenAsync("user1").get(); @@ -277,7 +250,7 @@ public void testVerifyTokenWithWrongTenantAwareClient() throws Exception { String idToken = signInWithCustomToken(customToken, tenantId); try { - auth.getTenantManager().getAuthForTenant("OTHER").verifyIdTokenAsync(idToken).get(); + 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); @@ -290,81 +263,75 @@ public void testVerifyTokenWithWrongTenantAwareClient() throws Exception { public void testOidcProviderConfigLifecycle() throws Exception { // Create config provider String providerId = "oidc.provider-id"; - OidcProviderConfig.CreateRequest createRequest = - new OidcProviderConfig.CreateRequest() - .setProviderId(providerId) - .setDisplayName("DisplayName") - .setEnabled(true) - .setClientId("ClientId") - .setIssuer("https://oidc.com/issuer"); - OidcProviderConfig config = tenantAwareAuth.createOidcProviderConfigAsync(createRequest).get(); + 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()); - try { - // Get config provider - 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 config provider - 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()); - } finally { - // Delete config provider - tenantAwareAuth.deleteProviderConfigAsync(providerId).get(); - assertOidcProviderConfigDoesNotExist(tenantAwareAuth, providerId); - } + // Get config provider + 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 config provider + 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 config provider + temporaryProviderConfig.deleteOidcProviderConfig(providerId); + ProviderConfigTestUtils.assertOidcProviderConfigDoesNotExist(tenantAwareAuth, providerId); } @Test public void testListOidcProviderConfigs() throws Exception { final List providerIds = new ArrayList<>(); - try { - // Create provider configs - for (int i = 0; i < 3; i++) { - String providerId = "oidc.provider-id" + i; - providerIds.add(providerId); - OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest() + + // 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"); - tenantAwareAuth.createOidcProviderConfig(createRequest); - } - - // List provider configs - 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()); + .setIssuer("https://oidc.com/issuer")); + } - } finally { - // Delete provider configs - for (String providerId : providerIds) { - tenantAwareAuth.deleteProviderConfigAsync(providerId).get(); + // 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()); } private String randomPhoneNumber() { @@ -415,23 +382,11 @@ static RandomUser create() { } } - private static void assertOidcProviderConfigDoesNotExist( - AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception { - try { - firebaseAuth.getOidcProviderConfigAsync(providerId).get(); - fail("No error thrown for getting a deleted provider config"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); - } - } - 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, From 99b07bdbf46c40ce9d48a4ed4dd9be6dc34ce0da Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Mon, 18 May 2020 14:43:37 -0400 Subject: [PATCH 12/21] Make Javadoc comments consistent and validate provider ID in CreateRequest. (#418) This makes the formatting of the Javadoc comments more consistent, and it's now more clear when/why IllegalArgumentExceptions get thrown. This also adds validation to the provider ID in the CreateRequest to ensure that it has the correct prefix.' Improvements were also made to the tests. The JSON string is now more readable, the parameters are now in the correct order for assertEquals calls, and the IllegalArgumentException tests have been properly fleshed out. --- .../firebase/auth/OidcProviderConfig.java | 26 +++++++--- .../google/firebase/auth/ProviderConfig.java | 17 ++++-- .../firebase/auth/OidcProviderConfigTest.java | 52 +++++++++++++++---- 3 files changed, 71 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java index e2587dad2..03bcf1175 100644 --- a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java +++ b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java @@ -48,7 +48,7 @@ public String getIssuer() { * Returns a new {@link UpdateRequest}, which can be used to update the attributes of this * provider config. * - * @return a non-null {@link UpdateRequest} instance. + * @return A non-null {@link UpdateRequest} instance. */ public UpdateRequest updateRequest() { return new UpdateRequest(getProviderId()); @@ -74,7 +74,8 @@ public CreateRequest() { } /** * Sets the client ID for the new provider. * - * @param clientId a non-null, non-empty client ID string. + * @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."); @@ -85,7 +86,9 @@ public CreateRequest setClientId(String clientId) { /** * Sets the issuer for the new provider. * - * @param issuer a non-null, non-empty issuer URL string. + * @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."); @@ -97,6 +100,10 @@ public CreateRequest setIssuer(String issuer) { CreateRequest getThis() { return this; } + + void assertValidProviderIdFormat(String providerId) { + checkArgument(providerId.startsWith("oidc."), "Invalid OIDC provider ID: " + providerId); + } } /** @@ -116,9 +123,9 @@ public static final class UpdateRequest extends AbstractUpdateRequest getProperties() { } abstract T getThis(); + + abstract void assertValidProviderIdFormat(String providerId); } /** @@ -135,7 +141,8 @@ String getProviderId() { /** * Sets the display name for the existing provider. * - * @param displayName a non-null, non-empty display name string. + * @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."); @@ -146,7 +153,7 @@ public T setDisplayName(String displayName) { /** * 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 + * @param enabled A boolean indicating whether the user can sign in with the provider. */ public T setEnabled(boolean enabled) { properties.put("enabled", enabled); diff --git a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java index 8079c5deb..39153d312 100644 --- a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java +++ b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java @@ -32,23 +32,23 @@ 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\"" - + "}"; + ("{" + + " 'name': 'projects/projectId/oauthIdpConfigs/oidc.provider-id'," + + " 'displayName': 'DISPLAY_NAME'," + + " 'enabled': true," + + " 'clientId': 'CLIENT_ID'," + + " 'issuer': 'https://oidc.com/issuer'" + + "}").replace("'", "\""); @Test public void testJsonSerialization() throws IOException { OidcProviderConfig config = jsonFactory.fromString(OIDC_JSON_STRING, OidcProviderConfig.class); - assertEquals(config.getProviderId(), "oidc.provider-id"); - assertEquals(config.getDisplayName(), "DISPLAY_NAME"); + assertEquals("oidc.provider-id", config.getProviderId()); + assertEquals("DISPLAY_NAME", config.getDisplayName()); assertTrue(config.isEnabled()); - assertEquals(config.getClientId(), "CLIENT_ID"); - assertEquals(config.getIssuer(), "https://oidc.com/issuer"); + assertEquals("CLIENT_ID", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); } @Test @@ -70,11 +70,31 @@ public void testCreateRequest() throws IOException { 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"); @@ -119,11 +139,21 @@ 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"); From 537b7a72db2798c910c42183f0b90432edc19144 Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Mon, 18 May 2020 19:01:19 -0400 Subject: [PATCH 13/21] Add class for SAML provider config. (#419) Adds a class for the SAML provider config. --- .../firebase/auth/SamlProviderConfig.java | 189 ++++++++++++++++++ .../firebase/auth/OidcProviderConfigTest.java | 2 +- .../firebase/auth/SamlProviderConfigTest.java | 158 +++++++++++++++ .../com/google/firebase/auth/TenantTest.java | 4 +- 4 files changed, 350 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/google/firebase/auth/SamlProviderConfig.java create mode 100644 src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java 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..9b29f05b1 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/SamlProviderConfig.java @@ -0,0 +1,189 @@ +/* + * 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.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"); + } + + 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 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; + } + + // TODO(micahstairs): Add 'addAllX509Certificates' method. + + /** + * 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; + } + + // TODO(micahstairs): Add 'setRequestSigningEnabled' method. + + CreateRequest getThis() { + return this; + } + + void assertValidProviderIdFormat(String providerId) { + checkArgument(providerId.startsWith("saml."), "Invalid SAML provider ID: " + providerId); + } + } +} diff --git a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java index 39153d312..fa5c6d4b0 100644 --- a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java +++ b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java @@ -41,7 +41,7 @@ public class OidcProviderConfigTest { + "}").replace("'", "\""); @Test - public void testJsonSerialization() throws IOException { + public void testJsonDeserialization() throws IOException { OidcProviderConfig config = jsonFactory.fromString(OIDC_JSON_STRING, OidcProviderConfig.class); assertEquals("oidc.provider-id", config.getProviderId()); 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..162c3b63e --- /dev/null +++ b/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java @@ -0,0 +1,158 @@ +/* + * 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(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 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"); + } +} 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"); From 0751674bdf4a2b4ce39abf7653edf4ff7ccfdf98 Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Thu, 21 May 2020 19:26:12 -0400 Subject: [PATCH 14/21] Add operations to create and delete SAML provider configs. (#420) This adds operations to create and delete SAML provider configs. --- .../firebase/auth/AbstractFirebaseAuth.java | 112 ++++++- .../firebase/auth/FirebaseUserManager.java | 25 +- .../firebase/auth/OidcProviderConfig.java | 25 +- .../google/firebase/auth/ProviderConfig.java | 14 +- .../firebase/auth/SamlProviderConfig.java | 23 +- .../google/firebase/auth/FirebaseAuthIT.java | 39 ++- .../auth/FirebaseUserManagerTest.java | 293 +++++++++++++++++- .../auth/ProviderConfigTestUtils.java | 30 +- .../auth/TenantAwareFirebaseAuthIT.java | 35 +++ src/test/resources/saml.json | 17 + 10 files changed, 567 insertions(+), 46 deletions(-) create mode 100644 src/test/resources/saml.json diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index 9fef3758f..27efaf01e 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -1076,13 +1076,15 @@ protected String execute() throws FirebaseAuthException { } /** - * Creates a new provider OIDC Auth config with the attributes contained in the specified {@link - * OidcProviderConfig.CreateRequest}. + * Creates a new OIDC 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( @@ -1098,6 +1100,8 @@ public OidcProviderConfig createOidcProviderConfig( * 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) { @@ -1108,6 +1112,7 @@ public ApiFuture createOidcProviderConfigAsync( 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 @@ -1162,7 +1167,8 @@ protected OidcProviderConfig execute() throws FirebaseAuthException { * * @param providerId A provider ID string. * @return An {@link OidcProviderConfig} instance. - * @throws IllegalArgumentException If the provider ID string is null or empty. + * @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) @@ -1179,7 +1185,8 @@ public OidcProviderConfig getOidcProviderConfig(@NonNull String providerId) * {@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. + * @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); @@ -1188,7 +1195,7 @@ public ApiFuture getOidcProviderConfigAsync(@NonNull String private CallableOperation getOidcProviderConfigOp(final String providerId) { checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); + OidcProviderConfig.checkOidcProviderId(providerId); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override @@ -1289,7 +1296,8 @@ protected ListProviderConfigsPage execute() * Deletes the OIDC 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. + * @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 { @@ -1303,7 +1311,8 @@ public void deleteOidcProviderConfig(@NonNull String providerId) throws Firebase * @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. + * @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); @@ -1312,7 +1321,7 @@ public ApiFuture deleteOidcProviderConfigAsync(String providerId) { private CallableOperation deleteOidcProviderConfigOp( final String providerId) { checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); + OidcProviderConfig.checkOidcProviderId(providerId); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override @@ -1323,6 +1332,93 @@ protected Void execute() throws FirebaseAuthException { }; } + /** + * 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); + } + }; + } + + /** + * 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; } diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 03d957bff..2a848589c 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -302,6 +302,8 @@ Tenant createTenant(Tenant.CreateRequest request) throws FirebaseAuthException { Tenant updateTenant(Tenant.UpdateRequest request) throws FirebaseAuthException { Map properties = request.getProperties(); + // TODO(micahstairs): Move this check so that argument validation happens outside the + // CallableOperation. 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)); @@ -368,15 +370,22 @@ String getEmailActionLink(EmailLinkType type, String email, OidcProviderConfig createOidcProviderConfig( OidcProviderConfig.CreateRequest request) throws FirebaseAuthException { GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs"); - String providerId = request.getProviderId(); - checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); - url.set("oauthIdpConfigId", providerId); + 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(); + // TODO(micahstairs): Move this check so that argument validation happens outside the + // CallableOperation. checkArgument(!properties.isEmpty(), "Provider config update must have at least one property set."); GenericUrl url = @@ -415,6 +424,11 @@ void deleteOidcProviderConfig(String providerId) throws FirebaseAuthException { 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 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 @@ -433,6 +447,11 @@ private static String getOidcUrlSuffix(String providerId) { 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/OidcProviderConfig.java b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java index 03bcf1175..c8c29dc47 100644 --- a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java +++ b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java @@ -54,6 +54,12 @@ 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. * @@ -71,6 +77,19 @@ public static final class CreateRequest extends AbstractCreateRequest properties = new HashMap<>(); String providerId; - /** - * 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 if the format is - * invalid. - */ - public T setProviderId(String providerId) { - checkArgument( - !Strings.isNullOrEmpty(providerId), "Provider ID name must not be null or empty."); - assertValidProviderIdFormat(providerId); + T setProviderId(String providerId) { this.providerId = providerId; return getThis(); } @@ -117,8 +107,6 @@ Map getProperties() { } abstract T getThis(); - - abstract void assertValidProviderIdFormat(String providerId); } /** diff --git a/src/main/java/com/google/firebase/auth/SamlProviderConfig.java b/src/main/java/com/google/firebase/auth/SamlProviderConfig.java index 9b29f05b1..e73781f03 100644 --- a/src/main/java/com/google/firebase/auth/SamlProviderConfig.java +++ b/src/main/java/com/google/firebase/auth/SamlProviderConfig.java @@ -71,6 +71,12 @@ public String getCallbackUrl() { return (String) spConfig.get("callbackUri"); } + 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) { @@ -106,6 +112,19 @@ public static final class CreateRequest extends AbstractCreateRequest result) { assertNull(error.get()); } + @Test + public void testSamlProviderConfigLifecycle() throws Exception { + // Create config provider + 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()); + + // TODO(micahstairs): Once implemented, add tests for getting and updating the SAML provider + // config. + + // Delete config provider + temporaryProviderConfig.deleteSamlProviderConfig(providerId); + + // TODO(micahstairs): Once the operation to get a SAML config is implemented, add an assertion + // that the SAML provider does not exist. + } + private Map parseLinkParameters(String link) throws Exception { Map result = new HashMap<>(); int queryBegin = link.indexOf('?'); @@ -914,7 +948,6 @@ private boolean checkProviderConfig(List providerIds, OidcProviderConfig return false; } - private static void assertUserDoesNotExist(AbstractFirebaseAuth firebaseAuth, String uid) throws Exception { try { diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index b18662e9d..ff67a8c1d 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -1831,6 +1831,32 @@ public void testGetOidcProviderConfig() throws Exception { 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 = @@ -1931,41 +1957,279 @@ public void testTenantAwareListOidcProviderConfigs() throws Exception { } @Test - public void testDeleteProviderConfig() throws Exception { + 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")); + // TODO(micahstairs): Add 'signRequest' to the create request once that field is added to + // SamlProviderConfig. + 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("oidc.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"); + + SamlProviderConfig config = tenantAwareAuth.createSamlProviderConfig(createRequest); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs"); + } + + @Test + public void testDeleteSamlProviderConfig() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); - FirebaseAuth.getInstance().deleteOidcProviderConfig("PROVIDER_ID"); + FirebaseAuth.getInstance().deleteSamlProviderConfig("saml.provider-id"); checkRequestHeaders(interceptor); - checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/PROVIDER_ID"); + 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 testDeleteProviderConfigWithNotFoundError() throws Exception { + 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().deleteOidcProviderConfig("UNKNOWN"); + 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 + "/oauthIdpConfigs/UNKNOWN"); + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.UNKNOWN"); } @Test - public void testTenantAwareDeleteProviderConfig() throws Exception { + public void testTenantAwareDeleteSamlProviderConfig() throws Exception { TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( "TENANT_ID", "{}"); TenantAwareFirebaseAuth tenantAwareAuth = FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); - tenantAwareAuth.deleteOidcProviderConfig("PROVIDER_ID"); + tenantAwareAuth.deleteSamlProviderConfig("saml.provider-id"); checkRequestHeaders(interceptor); - checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/PROVIDER_ID"); + String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs/saml.provider-id"; + checkUrl(interceptor, "DELETE", expectedUrl); } private static TestResponseInterceptor initializeAppForUserManagementWithStatusCode( @@ -2092,6 +2356,17 @@ private static void checkOidcProviderConfig(OidcProviderConfig config, String pr 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/ProviderConfigTestUtils.java b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java index 0cc636d47..52263335a 100644 --- a/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java +++ b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java @@ -47,6 +47,7 @@ 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; @@ -61,13 +62,30 @@ synchronized OidcProviderConfig createOidcProviderConfig( synchronized void deleteOidcProviderConfig(String providerId) throws FirebaseAuthException { checkArgument(oidcIds.contains(providerId), - "Provider ID is not currently associated with a temporary user."); + "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); @@ -76,6 +94,16 @@ protected synchronized void after() { } } 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/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java index e585f87b9..48a0c7d18 100644 --- a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java @@ -35,6 +35,7 @@ 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; @@ -334,6 +335,40 @@ public void testListOidcProviderConfigs() throws Exception { assertEquals(providerIds.size(), collected.get()); } + @Test + public void testSamlProviderConfigLifecycle() throws Exception { + // Create config provider + 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()); + + // TODO(micahstairs): Once implemented, add tests for getting and updating the SAML provider + // config. + + // Delete config provider + temporaryProviderConfig.deleteSamlProviderConfig(providerId); + + // TODO(micahstairs): Once the operation to get a SAML config is implemented, add an assertion + // that the SAML provider does not exist. + } + private String randomPhoneNumber() { Random random = new Random(); StringBuilder builder = new StringBuilder("+1"); 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" + } +} From 7732833e82a18623b9a330c507f3566d79c16e09 Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Tue, 26 May 2020 14:30:46 -0400 Subject: [PATCH 15/21] Add get operation for SAML provider configs. (#421) --- .../firebase/auth/AbstractFirebaseAuth.java | 43 ++++++++++++ .../firebase/auth/FirebaseUserManager.java | 5 ++ .../google/firebase/auth/FirebaseAuthIT.java | 29 +++++--- .../auth/FirebaseUserManagerTest.java | 69 +++++++++++++++++++ .../auth/ProviderConfigTestUtils.java | 12 ++++ .../auth/TenantAwareFirebaseAuthIT.java | 29 +++++--- 6 files changed, 165 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index 27efaf01e..1d9062e5a 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -1379,6 +1379,49 @@ protected SamlProviderConfig execute() throws FirebaseAuthException { }; } + /** + * Gets the SAML provider Auth config 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 '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 will be 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); + } + }; + } + /** * Deletes the SAML Auth provider config identified by the specified provider ID. * diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 2a848589c..f603b66f2 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -399,6 +399,11 @@ OidcProviderConfig getOidcProviderConfig(String providerId) throws FirebaseAuthE 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 = diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index cdd03e7b2..3ac9288b7 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -664,7 +664,7 @@ public void testGenerateSignInWithEmailLink() throws Exception { @Test public void testOidcProviderConfigLifecycle() throws Exception { - // Create config provider + // Create provider config String providerId = "oidc.provider-id"; OidcProviderConfig config = temporaryProviderConfig.createOidcProviderConfig( new OidcProviderConfig.CreateRequest() @@ -679,7 +679,7 @@ public void testOidcProviderConfigLifecycle() throws Exception { assertEquals("ClientId", config.getClientId()); assertEquals("https://oidc.com/issuer", config.getIssuer()); - // Get config provider + // Get provider config config = auth.getOidcProviderConfigAsync(providerId).get(); assertEquals(providerId, config.getProviderId()); assertEquals("DisplayName", config.getDisplayName()); @@ -687,7 +687,7 @@ public void testOidcProviderConfigLifecycle() throws Exception { assertEquals("ClientId", config.getClientId()); assertEquals("https://oidc.com/issuer", config.getIssuer()); - // Update config provider + // Update provider config OidcProviderConfig.UpdateRequest updateRequest = new OidcProviderConfig.UpdateRequest(providerId) .setDisplayName("NewDisplayName") @@ -701,7 +701,7 @@ public void testOidcProviderConfigLifecycle() throws Exception { assertEquals("NewClientId", config.getClientId()); assertEquals("https://oidc.com/new-issuer", config.getIssuer()); - // Delete config provider + // Delete provider config temporaryProviderConfig.deleteOidcProviderConfig(providerId); ProviderConfigTestUtils.assertOidcProviderConfigDoesNotExist(auth, providerId); } @@ -777,7 +777,7 @@ public void onSuccess(ListProviderConfigsPage result) { @Test public void testSamlProviderConfigLifecycle() throws Exception { - // Create config provider + // Create provider config String providerId = "saml.provider-id"; SamlProviderConfig config = temporaryProviderConfig.createSamlProviderConfig( new SamlProviderConfig.CreateRequest() @@ -799,14 +799,21 @@ public void testSamlProviderConfigLifecycle() throws Exception { assertEquals("RP_ENTITY_ID", config.getRpEntityId()); assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); - // TODO(micahstairs): Once implemented, add tests for getting and updating the SAML provider - // config. + 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()); - // Delete config provider - temporaryProviderConfig.deleteSamlProviderConfig(providerId); + // TODO(micahstairs): Once implemented, add tests for updating the SAML provider config. - // TODO(micahstairs): Once the operation to get a SAML config is implemented, add an assertion - // that the SAML provider does not exist. + // Delete provider config + temporaryProviderConfig.deleteSamlProviderConfig(providerId); + ProviderConfigTestUtils.assertSamlProviderConfigDoesNotExist(auth, providerId); } private Map parseLinkParameters(String link) throws Exception { diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index ff67a8c1d..4df8692fe 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -2169,6 +2169,75 @@ public void testTenantAwareCreateSamlProvider() throws Exception { checkUrl(interceptor, "POST", TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs"); } + @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 testDeleteSamlProviderConfig() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); diff --git a/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java index 52263335a..262c48f37 100644 --- a/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java +++ b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java @@ -40,6 +40,18 @@ static void assertOidcProviderConfigDoesNotExist( } } + 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. */ diff --git a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java index 48a0c7d18..51b5d5461 100644 --- a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java @@ -262,7 +262,7 @@ public void testVerifyTokenWithWrongTenantAwareClient() throws Exception { @Test public void testOidcProviderConfigLifecycle() throws Exception { - // Create config provider + // Create provider config String providerId = "oidc.provider-id"; OidcProviderConfig config = temporaryProviderConfig.createOidcProviderConfig( @@ -277,14 +277,14 @@ public void testOidcProviderConfigLifecycle() throws Exception { assertEquals("ClientId", config.getClientId()); assertEquals("https://oidc.com/issuer", config.getIssuer()); - // Get config provider + // 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 config provider + // Update provider config OidcProviderConfig.UpdateRequest updateRequest = new OidcProviderConfig.UpdateRequest(providerId) .setDisplayName("NewDisplayName") @@ -298,7 +298,7 @@ public void testOidcProviderConfigLifecycle() throws Exception { assertEquals("NewClientId", config.getClientId()); assertEquals("https://oidc.com/new-issuer", config.getIssuer()); - // Delete config provider + // Delete provider config temporaryProviderConfig.deleteOidcProviderConfig(providerId); ProviderConfigTestUtils.assertOidcProviderConfigDoesNotExist(tenantAwareAuth, providerId); } @@ -337,7 +337,7 @@ public void testListOidcProviderConfigs() throws Exception { @Test public void testSamlProviderConfigLifecycle() throws Exception { - // Create config provider + // Create provider config String providerId = "saml.provider-id"; SamlProviderConfig config = temporaryProviderConfig.createSamlProviderConfig( new SamlProviderConfig.CreateRequest() @@ -359,14 +359,21 @@ public void testSamlProviderConfigLifecycle() throws Exception { assertEquals("RP_ENTITY_ID", config.getRpEntityId()); assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); - // TODO(micahstairs): Once implemented, add tests for getting and updating the SAML provider - // config. + 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()); - // Delete config provider - temporaryProviderConfig.deleteSamlProviderConfig(providerId); + // TODO(micahstairs): Once implemented, add tests for updating the SAML provider config. - // TODO(micahstairs): Once the operation to get a SAML config is implemented, add an assertion - // that the SAML provider does not exist. + // Delete provider config + temporaryProviderConfig.deleteSamlProviderConfig(providerId); + ProviderConfigTestUtils.assertSamlProviderConfigDoesNotExist(tenantAwareAuth, providerId); } private String randomPhoneNumber() { From de24a2b255c5bcd7b18523c0adce840d13d15a3e Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Wed, 27 May 2020 14:36:54 -0400 Subject: [PATCH 16/21] Add operation to update SAML provider configs. (#424) --- .../firebase/auth/AbstractFirebaseAuth.java | 50 +++++++ .../firebase/auth/FirebaseUserManager.java | 36 +++-- .../firebase/auth/SamlProviderConfig.java | 112 +++++++++++++++ .../google/firebase/auth/FirebaseAuthIT.java | 12 +- .../auth/FirebaseUserManagerTest.java | 134 +++++++++++++++++- .../auth/TenantAwareFirebaseAuthIT.java | 12 +- 6 files changed, 341 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index 1d9062e5a..ea5f6427a 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -1129,6 +1129,7 @@ protected OidcProviderConfig execute() throws FirebaseAuthException { * @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( @@ -1143,6 +1144,8 @@ public OidcProviderConfig updateOidcProviderConfig( * @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) { @@ -1153,6 +1156,8 @@ private CallableOperation updateOidcP 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 @@ -1379,6 +1384,51 @@ protected SamlProviderConfig execute() throws FirebaseAuthException { }; } + /** + * Updates an existing SAML Auth provider config with the attributes contained in the specified + * {@link OidcProviderConfig.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 provider Auth config corresponding to the specified provider ID. * diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index f603b66f2..75ec5e2d8 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -306,7 +306,7 @@ Tenant updateTenant(Tenant.UpdateRequest request) throws FirebaseAuthException { // CallableOperation. 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); } @@ -384,16 +384,21 @@ SamlProviderConfig createSamlProviderConfig( OidcProviderConfig updateOidcProviderConfig(OidcProviderConfig.UpdateRequest request) throws FirebaseAuthException { Map properties = request.getProperties(); - // TODO(micahstairs): Move this check so that argument validation happens outside the - // CallableOperation. - checkArgument(!properties.isEmpty(), - "Provider config update must have at least one property set."); GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(request.getProviderId())); - url.put("updateMask", generateMask(properties)); + 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); @@ -434,12 +439,19 @@ void deleteSamlProviderConfig(String providerId) throws FirebaseAuthException { sendRequest("DELETE", url, null, GenericJson.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())); + 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) { diff --git a/src/main/java/com/google/firebase/auth/SamlProviderConfig.java b/src/main/java/com/google/firebase/auth/SamlProviderConfig.java index e73781f03..c8970cc6d 100644 --- a/src/main/java/com/google/firebase/auth/SamlProviderConfig.java +++ b/src/main/java/com/google/firebase/auth/SamlProviderConfig.java @@ -71,6 +71,16 @@ 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."), @@ -201,4 +211,106 @@ 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; + } + + // TODO(micahstairs): Add 'addAllX509Certificates' method. + + /** + * 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; + } + + // TODO(micahstairs): Add 'setRequestSigningEnabled' method. + + UpdateRequest getThis() { + return this; + } + } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index 3ac9288b7..75ceb8b89 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -809,7 +809,17 @@ public void testSamlProviderConfigLifecycle() throws Exception { assertEquals("RP_ENTITY_ID", config.getRpEntityId()); assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); - // TODO(micahstairs): Once implemented, add tests for updating the SAML provider config. + // 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); diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 4df8692fe..ea0db8d13 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -2068,7 +2068,7 @@ public void testCreateSamlProvider() throws Exception { @Test public void testCreateSamlProviderMinimal() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + TestUtils.loadResource("saml.json")); // Only the 'enabled', 'displayName', and 'signRequest' fields can be omitted from a SAML // provider config creation request. SamlProviderConfig.CreateRequest createRequest = @@ -2169,6 +2169,138 @@ public void testTenantAwareCreateSamlProvider() throws Exception { checkUrl(interceptor, "POST", TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs"); } + @Test + public void testUpdateSamlProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + // TODO(micahstairs): Add 'signRequest' to the create request once that field is added to + // SamlProviderConfig. + 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( diff --git a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java index 51b5d5461..9e22a84a5 100644 --- a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java @@ -369,7 +369,17 @@ public void testSamlProviderConfigLifecycle() throws Exception { assertEquals("RP_ENTITY_ID", config.getRpEntityId()); assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); - // TODO(micahstairs): Once implemented, add tests for updating the SAML provider config. + // 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); From 638240f77befb8181b8a36876f6c6fe76a6357db Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Thu, 28 May 2020 20:51:36 -0400 Subject: [PATCH 17/21] Add operation to list SAML provider configs. (#426) Adds a list operation for SAML provider configs. I also set 'suppressLoadErrors' to true to avoid class information error. See https://stackoverflow.com/questions/27938039/unable-to-get-class-information-for-checkstyle for more context. --- checkstyle.xml | 7 +- .../firebase/auth/AbstractFirebaseAuth.java | 97 ++++++++++++++++++- .../firebase/auth/FirebaseUserManager.java | 23 ++++- .../auth/ListProviderConfigsPage.java | 18 +++- .../firebase/auth/OidcProviderConfig.java | 2 +- .../ListOidcProviderConfigsResponse.java | 15 +-- .../ListSamlProviderConfigsResponse.java | 52 ++++++++++ .../google/firebase/auth/FirebaseAuthIT.java | 92 +++++++++++++++++- .../auth/FirebaseUserManagerTest.java | 70 +++++++++++++ .../auth/TenantAwareFirebaseAuthIT.java | 38 ++++++++ src/test/resources/listSaml.json | 35 +++++++ 11 files changed, 421 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponse.java create mode 100644 src/test/resources/listSaml.json diff --git a/checkstyle.xml b/checkstyle.xml index 663918173..da46b4844 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -42,15 +42,15 @@ - + - - + + @@ -229,6 +229,7 @@ + diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index ea5f6427a..e6dbcbf18 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -31,6 +31,7 @@ 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.UserRecord; @@ -1244,8 +1245,8 @@ public ListProviderConfigsPage listOidcProviderConfigs( } /** - * Similar to {@link #listlistOidcProviderConfigs(String)} but performs the operation - * asynchronously. Page size will be limited to 100 provider configs. + * Similar to {@link #listOidcProviderConfigs(String)} but performs the operation asynchronously. + * Page size will be limited to 100 provider configs. * * @param pageToken A non-empty page token string, or null to retrieve the first page of provider * configs. @@ -1386,7 +1387,7 @@ protected SamlProviderConfig execute() throws FirebaseAuthException { /** * Updates an existing SAML Auth provider config with the attributes contained in the specified - * {@link OidcProviderConfig.UpdateRequest}. + * {@link SamlProviderConfig.UpdateRequest}. * * @param request A non-null {@link SamlProviderConfig.UpdateRequest} instance. * @return A {@link SamlProviderConfig} instance corresponding to the updated provider config. @@ -1433,7 +1434,7 @@ protected SamlProviderConfig execute() throws FirebaseAuthException { * Gets the SAML provider Auth config corresponding to the specified provider ID. * * @param providerId A provider ID string. - * @return An {@link OidcProviderConfig} instance. + * @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. @@ -1472,6 +1473,94 @@ protected SamlProviderConfig execute() throws FirebaseAuthException { }; } + /** + * Gets a page of SAML Auth provider configs starting from the specified {@code pageToken}. Page + * size will be limited to 100 provider configs. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @return 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 will be limited to 100 provider configs. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @return An {@code ApiFuture} which will complete successfully with a + * {@link ListProviderConfigsPage} instance. If an error occurs while retrieving provider + * config data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty. + */ + public ApiFuture> 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. * diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 75ec5e2d8..72699ad3c 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -45,6 +45,7 @@ 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; @@ -415,7 +416,7 @@ ListOidcProviderConfigsResponse listOidcProviderConfigs(int maxResults, String p ImmutableMap.builder().put("pageSize", maxResults); if (pageToken != null) { checkArgument(!pageToken.equals( - ListTenantsPage.END_OF_LIST), "Invalid end of list page token."); + ListProviderConfigsPage.END_OF_LIST), "Invalid end of list page token."); builder.put("nextPageToken", pageToken); } @@ -429,6 +430,26 @@ ListOidcProviderConfigsResponse listOidcProviderConfigs(int maxResults, String p 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); diff --git a/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java b/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java index 2ab7f736f..4530c0fae 100644 --- a/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java +++ b/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java @@ -25,6 +25,7 @@ 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; @@ -205,7 +206,7 @@ static class DefaultOidcProviderConfigSource 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. diff --git a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java index c8c29dc47..23125244b 100644 --- a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java +++ b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java @@ -138,7 +138,7 @@ public static final class UpdateRequest extends AbstractUpdateRequest providerConfigs, - String pageToken) { - this.providerConfigs = providerConfigs; - this.pageToken = pageToken; - } - - public ListOidcProviderConfigsResponse() { } - @Override public List getProviderConfigs() { return providerConfigs == null ? ImmutableList.of() : providerConfigs; @@ -58,6 +47,6 @@ public boolean hasProviderConfigs() { @Override public String getPageToken() { - return pageToken == null ? "" : pageToken; + return Strings.nullToEmpty(pageToken); } } 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/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index 75ceb8b89..a43d67f9f 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -727,7 +727,7 @@ public void testListOidcProviderConfigs() throws Exception { auth.listOidcProviderConfigsAsync(null).get(); while (page != null) { for (OidcProviderConfig providerConfig : page.getValues()) { - if (checkProviderConfig(providerIds, providerConfig)) { + if (checkOidcProviderConfig(providerIds, providerConfig)) { collected.incrementAndGet(); } } @@ -739,7 +739,7 @@ public void testListOidcProviderConfigs() throws Exception { collected.set(0); page = auth.listOidcProviderConfigsAsync(null).get(); for (OidcProviderConfig providerConfig : page.iterateAll()) { - if (checkProviderConfig(providerIds, providerConfig)) { + if (checkOidcProviderConfig(providerIds, providerConfig)) { collected.incrementAndGet(); } } @@ -763,7 +763,7 @@ public void onFailure(Throwable t) { @Override public void onSuccess(ListProviderConfigsPage result) { for (OidcProviderConfig providerConfig : result.iterateAll()) { - if (checkProviderConfig(providerIds, providerConfig)) { + if (checkOidcProviderConfig(providerIds, providerConfig)) { collected.incrementAndGet(); } } @@ -826,6 +826,78 @@ public void testSamlProviderConfigLifecycle() throws Exception { 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 { Map result = new HashMap<>(); int queryBegin = link.indexOf('?'); @@ -956,7 +1028,7 @@ static RandomUser create() { } } - private boolean checkProviderConfig(List providerIds, OidcProviderConfig config) { + private boolean checkOidcProviderConfig(List providerIds, OidcProviderConfig config) { if (providerIds.contains(config.getProviderId())) { assertEquals("CLIENT_ID", config.getClientId()); assertEquals("https://oidc.com/issuer", config.getIssuer()); @@ -965,6 +1037,18 @@ private boolean checkProviderConfig(List providerIds, OidcProviderConfig return false; } + 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 { diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index ea0db8d13..5a4520888 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -2370,6 +2370,76 @@ public void testGetTenantAwareSamlProviderConfig() throws Exception { 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("{}"); diff --git a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java index 9e22a84a5..95c8cad5c 100644 --- a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java @@ -386,6 +386,44 @@ public void testSamlProviderConfigLifecycle() throws Exception { 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 randomPhoneNumber() { Random random = new Random(); StringBuilder builder = new StringBuilder("+1"); 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" + } + } ] +} From aa96da043dbf30c92c6079fe8da3c26bf3ee142e Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Tue, 2 Jun 2020 20:18:15 -0400 Subject: [PATCH 18/21] Finish implementing SAML provider config. (#428) I've implemented addAllX509Certificates and removed the TODOs for the request signing, since we are not ready to expose that yet. I've also added unit tests for SamlProviderConfig.UpdateRequest, because those were missed in a previous PR. On an unrelated note, I've also added a comment describing why 'suppressLoadErrors' needs to be set. --- checkstyle.xml | 2 + .../firebase/auth/SamlProviderConfig.java | 41 ++++- .../auth/FirebaseUserManagerTest.java | 8 +- .../firebase/auth/SamlProviderConfigTest.java | 165 ++++++++++++++++++ 4 files changed, 205 insertions(+), 11 deletions(-) diff --git a/checkstyle.xml b/checkstyle.xml index da46b4844..77e2dba54 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -229,6 +229,8 @@ + + diff --git a/src/main/java/com/google/firebase/auth/SamlProviderConfig.java b/src/main/java/com/google/firebase/auth/SamlProviderConfig.java index c8970cc6d..e74478bea 100644 --- a/src/main/java/com/google/firebase/auth/SamlProviderConfig.java +++ b/src/main/java/com/google/firebase/auth/SamlProviderConfig.java @@ -27,6 +27,7 @@ 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; @@ -177,7 +178,23 @@ public CreateRequest addX509Certificate(String x509Certificate) { return this; } - // TODO(micahstairs): Add 'addAllX509Certificates' method. + /** + * 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. @@ -205,8 +222,6 @@ public CreateRequest setCallbackUrl(String callbackUrl) { return this; } - // TODO(micahstairs): Add 'setRequestSigningEnabled' method. - CreateRequest getThis() { return this; } @@ -279,7 +294,23 @@ public UpdateRequest addX509Certificate(String x509Certificate) { return this; } - // TODO(micahstairs): Add 'addAllX509Certificates' method. + /** + * 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. @@ -307,8 +338,6 @@ public UpdateRequest setCallbackUrl(String callbackUrl) { return this; } - // TODO(micahstairs): Add 'setRequestSigningEnabled' method. - UpdateRequest getThis() { return this; } diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 5a4520888..baff2e6af 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -2023,8 +2023,6 @@ public void testTenantAwareDeleteOidcProviderConfig() throws Exception { public void testCreateSamlProvider() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement( TestUtils.loadResource("saml.json")); - // TODO(micahstairs): Add 'signRequest' to the create request once that field is added to - // SamlProviderConfig. SamlProviderConfig.CreateRequest createRequest = new SamlProviderConfig.CreateRequest() .setProviderId("saml.provider-id") @@ -2048,6 +2046,7 @@ public void testCreateSamlProvider() throws Exception { 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()); @@ -2058,6 +2057,7 @@ public void testCreateSamlProvider() throws Exception { 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()); @@ -2163,7 +2163,7 @@ public void testTenantAwareCreateSamlProvider() throws Exception { TenantAwareFirebaseAuth tenantAwareAuth = FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); - SamlProviderConfig config = tenantAwareAuth.createSamlProviderConfig(createRequest); + tenantAwareAuth.createSamlProviderConfig(createRequest); checkRequestHeaders(interceptor); checkUrl(interceptor, "POST", TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs"); @@ -2173,8 +2173,6 @@ public void testTenantAwareCreateSamlProvider() throws Exception { public void testUpdateSamlProvider() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement( TestUtils.loadResource("saml.json")); - // TODO(micahstairs): Add 'signRequest' to the create request once that field is added to - // SamlProviderConfig. SamlProviderConfig.UpdateRequest updateRequest = new SamlProviderConfig.UpdateRequest("saml.provider-id") .setDisplayName("DISPLAY_NAME") diff --git a/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java b/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java index 162c3b63e..135175227 100644 --- a/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java +++ b/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java @@ -106,6 +106,29 @@ public void testCreateRequest() throws IOException { 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); @@ -141,6 +164,16 @@ 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); @@ -155,4 +188,136 @@ public void testCreateRequestMissingCallbackUrl() { 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"); + } } From 0fa48b2a74191d309afda6af67d97ba23fa11668 Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Thu, 4 Jun 2020 13:24:49 -0400 Subject: [PATCH 19/21] Move check in FirebaseUserManager.updateTenant outside CallableOperation. (#431) --- .../google/firebase/auth/FirebaseUserManager.java | 3 --- .../java/com/google/firebase/auth/TenantManager.java | 12 +++++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 72699ad3c..c65e44deb 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -303,9 +303,6 @@ Tenant createTenant(Tenant.CreateRequest request) throws FirebaseAuthException { Tenant updateTenant(Tenant.UpdateRequest request) throws FirebaseAuthException { Map properties = request.getProperties(); - // TODO(micahstairs): Move this check so that argument validation happens outside the - // CallableOperation. - checkArgument(!properties.isEmpty(), "Tenant update must have at least one property set"); GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(request.getTenantId())); url.put("updateMask", Joiner.on(",").join(generateMask(properties))); return sendRequest("PATCH", url, properties, Tenant.class); 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 { From d32705af8a044ff75869787b47974834f09b5837 Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Thu, 4 Jun 2020 16:15:00 -0400 Subject: [PATCH 20/21] Refactor shared code into UserTestUtils. (#432) --- .../google/firebase/auth/FirebaseAuthIT.java | 171 ++++++----------- .../com/google/firebase/auth/GetUsersIT.java | 14 +- .../auth/TenantAwareFirebaseAuthIT.java | 176 +++++++----------- .../google/firebase/auth/UserTestUtils.java | 124 ++++++++++++ 4 files changed, 252 insertions(+), 233 deletions(-) create mode 100644 src/test/java/com/google/firebase/auth/UserTestUtils.java diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index a43d67f9f..f651e3d3f 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -47,6 +47,8 @@ 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; @@ -56,8 +58,6 @@ 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; @@ -85,7 +85,7 @@ public class FirebaseAuthIT { private static final FirebaseAuth auth = FirebaseAuth.getInstance( IntegrationTestUtils.ensureDefaultApp()); - @Rule public final TemporaryUser temporaryUser = new TemporaryUser(); + @Rule public final TemporaryUser temporaryUser = new TemporaryUser(auth); @Rule public final TemporaryProviderConfig temporaryProviderConfig = new TemporaryProviderConfig(auth); @@ -211,22 +211,21 @@ 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 = temporaryUser.create(user); - assertEquals(randomUser.uid, userRecord.getUid()); + assertEquals(randomUser.getUid(), userRecord.getUid()); assertEquals("Random User", 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()); @@ -239,7 +238,7 @@ public void testCreateUserWithParams() throws Exception { assertTrue(providers.contains("password")); assertTrue(providers.contains("phone")); - checkRecreateUser(randomUser.uid); + checkRecreateUser(randomUser.getUid()); } @Test @@ -264,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"); @@ -277,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()); @@ -299,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()); @@ -309,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")); @@ -555,29 +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.uid); + temporaryUser.registerUid(randomUser.getUid()); assertEquals(1, result.getSuccessCount()); assertEquals(0, result.getFailureCount()); - UserRecord savedUser = auth.getUserAsync(randomUser.uid).get(); - assertEquals(randomUser.email, savedUser.getEmail()); + 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(); @@ -593,46 +591,46 @@ public void testImportUsersWithPassword() throws Exception { .setRounds(8) .setMemoryCost(14) .build())).get(); - temporaryUser.registerUid(randomUser.uid); + temporaryUser.registerUid(randomUser.getUid()); assertEquals(1, result.getSuccessCount()); assertEquals(0, result.getFailureCount()); - UserRecord savedUser = auth.getUserAsync(randomUser.uid).get(); - assertEquals(randomUser.email, savedUser.getEmail()); - String idToken = signInWithPassword(randomUser.email, "password"); + 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(); + RandomUser user = UserTestUtils.generateRandomUserInfo(); temporaryUser.create(new UserRecord.CreateRequest() - .setUid(user.uid) - .setEmail(user.email) + .setUid(user.getUid()) + .setEmail(user.getEmail()) .setEmailVerified(false) .setPassword("password")); - String link = auth.generatePasswordResetLink(user.email, ActionCodeSettings.builder() + 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.email, "password", "newpassword", + String email = resetPassword(user.getEmail(), "password", "newpassword", linkParams.get("oobCode")); - assertEquals(user.email, email); + assertEquals(user.getEmail(), email); // Password reset also verifies the user's email - assertTrue(auth.getUser(user.uid).isEmailVerified()); + assertTrue(auth.getUser(user.getUid()).isEmailVerified()); } @Test public void testGenerateEmailVerificationResetLink() throws Exception { - RandomUser user = RandomUser.create(); + RandomUser user = UserTestUtils.generateRandomUserInfo(); temporaryUser.create(new UserRecord.CreateRequest() - .setUid(user.uid) - .setEmail(user.email) + .setUid(user.getUid()) + .setEmail(user.getEmail()) .setEmailVerified(false) .setPassword("password")); - String link = auth.generateEmailVerificationLink(user.email, ActionCodeSettings.builder() + String link = auth.generateEmailVerificationLink(user.getEmail(), ActionCodeSettings.builder() .setUrl(ACTION_LINK_CONTINUE_URL) .setHandleCodeInApp(false) .build()); @@ -645,21 +643,21 @@ public void testGenerateEmailVerificationResetLink() throws Exception { @Test public void testGenerateSignInWithEmailLink() throws Exception { - RandomUser user = RandomUser.create(); + RandomUser user = UserTestUtils.generateRandomUserInfo(); temporaryUser.create(new UserRecord.CreateRequest() - .setUid(user.uid) - .setEmail(user.email) + .setUid(user.getUid()) + .setEmail(user.getEmail()) .setEmailVerified(false) .setPassword("password")); - String link = auth.generateSignInWithEmailLink(user.email, ActionCodeSettings.builder() + 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.email, linkParams.get("oobCode")); + String idToken = signInWithEmailLink(user.getEmail(), linkParams.get("oobCode")); assertFalse(Strings.isNullOrEmpty(idToken)); - assertTrue(auth.getUser(user.uid).isEmailVerified()); + assertTrue(auth.getUser(user.getUid()).isEmailVerified()); } @Test @@ -913,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); } @@ -1011,23 +1000,6 @@ 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; - } - - 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 checkOidcProviderConfig(List providerIds, OidcProviderConfig config) { if (providerIds.contains(config.getProviderId())) { assertEquals("CLIENT_ID", config.getClientId()); @@ -1068,45 +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")); } - - /** - * Creates temporary Firebase user accounts for testing, and deletes them at the end of each - * test case. - */ - private static final class TemporaryUser extends ExternalResource { - - private final List users = new ArrayList<>(); - - 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/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/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java index 95c8cad5c..a6d83dccf 100644 --- a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java @@ -40,13 +40,13 @@ 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.Random; -import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicInteger; @@ -69,6 +69,7 @@ public class TenantAwareFirebaseAuthIT { private static TenantAwareFirebaseAuth tenantAwareAuth; private static String tenantId; + @Rule public final TemporaryUser temporaryUser = new TemporaryUser(tenantAwareAuth); @Rule public final TemporaryProviderConfig temporaryProviderConfig = new TemporaryProviderConfig(tenantAwareAuth); @@ -90,8 +91,7 @@ public static void tearDownClass() throws Exception { @Test public void testUserLifecycle() throws Exception { // Create user - // TODO(micahstairs): Use a temporary user here. - UserRecord userRecord = tenantAwareAuth.createUserAsync(new UserRecord.CreateRequest()).get(); + UserRecord userRecord = temporaryUser.create(new UserRecord.CreateRequest()); String uid = userRecord.getUid(); // Get user @@ -110,12 +110,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"); @@ -123,8 +122,8 @@ public void testUserLifecycle() throws Exception { assertEquals(uid, userRecord.getUid()); assertEquals(tenantId, 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()); @@ -145,7 +144,7 @@ public void testUserLifecycle() throws Exception { assertEquals(uid, userRecord.getUid()); assertEquals(tenantId, userRecord.getTenantId()); assertNull(userRecord.getDisplayName()); - assertEquals(randomUser.email, userRecord.getEmail()); + assertEquals(randomUser.getEmail(), userRecord.getEmail()); assertNull(userRecord.getPhoneNumber()); assertNull(userRecord.getPhotoUrl()); assertTrue(userRecord.isEmailVerified()); @@ -155,85 +154,78 @@ public void testUserLifecycle() throws Exception { // Delete user tenantAwareAuth.deleteUserAsync(userRecord.getUid()).get(); - assertUserDoesNotExist(tenantAwareAuth, userRecord.getUid()); + UserTestUtils.assertUserDoesNotExist(tenantAwareAuth, userRecord.getUid()); } @Test public void testListUsers() throws Exception { final List uids = new ArrayList<>(); - try { - for (int i = 0; i < 3; i++) { - // TODO(micahstairs): Use a temporary user here. - UserRecord.CreateRequest createRequest = - new UserRecord.CreateRequest().setPassword("password"); - uids.add(tenantAwareAuth.createUserAsync(createRequest).get().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()); + 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 = tenantAwareAuth.listUsersAsync(null).get(); - for (ExportedUserRecord user : page.iterateAll()) { + // 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(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()); 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(); - } + 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()); - } + @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(); + semaphore.release(); } - } + }, MoreExecutors.directExecutor()); + semaphore.acquire(); + assertEquals(uids.size(), collected.get()); + assertNull(error.get()); } @Test @@ -424,15 +416,6 @@ public void testListSamlProviderConfigs() throws Exception { assertEquals(providerIds.size(), collected.get()); } - private 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, @Nullable String tenantId) throws IOException { final GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key=" @@ -454,33 +437,4 @@ private String signInWithCustomToken( response.disconnect(); } } - - private static class RandomUser { - private final String uid; - private final String email; - - private RandomUser(String uid, String email) { - this.uid = uid; - this.email = email; - } - - 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 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()); - } - } } 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(); + } + } +} + From a196aa4cbad3042a5b142e1b41762dc5a63e66a0 Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Fri, 12 Jun 2020 18:30:53 -0400 Subject: [PATCH 21/21] Address Kevin's feedback. --- .../firebase/auth/AbstractFirebaseAuth.java | 35 ++++++++++--------- .../firebase/auth/OidcProviderConfig.java | 7 ++-- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index e6dbcbf18..a864c4f6c 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -447,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. @@ -1077,8 +1077,8 @@ protected String execute() throws FirebaseAuthException { } /** - * Creates a new OIDC Auth provider config with the attributes contained in the specified - * {@link OidcProviderConfig.CreateRequest}. + * 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 @@ -1124,8 +1124,8 @@ protected OidcProviderConfig execute() throws FirebaseAuthException { } /** - * Updates an existing OIDC Auth provider config with the attributes contained in the specified - * {@link OidcProviderConfig.UpdateRequest}. + * 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. @@ -1169,7 +1169,7 @@ protected OidcProviderConfig execute() throws FirebaseAuthException { } /** - * Gets the provider OIDC Auth config corresponding to the specified provider ID. + * Gets the OpenID Connect auth provider corresponding to the specified provider ID. * * @param providerId A provider ID string. * @return An {@link OidcProviderConfig} instance. @@ -1184,7 +1184,7 @@ public OidcProviderConfig getOidcProviderConfig(@NonNull String providerId) /** * Similar to {@link #getOidcProviderConfig(String)} but performs the operation asynchronously. - * Page size will be limited to 100 provider configs. + * Page size is limited to 100 provider configs. * * @param providerId A provider ID string. * @return An {@code ApiFuture} which will complete successfully with an @@ -1212,8 +1212,8 @@ protected OidcProviderConfig execute() throws FirebaseAuthException { } /** - * Gets a page of OIDC Auth provider configs starting from the specified {@code pageToken}. Page - * size will be limited to 100 provider configs. + * 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. @@ -1228,7 +1228,8 @@ public ListProviderConfigsPage listOidcProviderConfigs( } /** - * Gets a page of OIDC Auth provider configs starting from the specified {@code pageToken}. + * 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. @@ -1246,7 +1247,7 @@ public ListProviderConfigsPage listOidcProviderConfigs( /** * Similar to {@link #listOidcProviderConfigs(String)} but performs the operation asynchronously. - * Page size will be limited to 100 provider configs. + * 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. @@ -1299,7 +1300,7 @@ protected ListProviderConfigsPage execute() } /** - * Deletes the OIDC Auth provider config identified by the specified provider ID. + * 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 @@ -1431,7 +1432,7 @@ protected SamlProviderConfig execute() throws FirebaseAuthException { } /** - * Gets the SAML provider Auth config corresponding to the specified provider ID. + * Gets the SAML Auth provider config corresponding to the specified provider ID. * * @param providerId A provider ID string. * @return An {@link SamlProviderConfig} instance. @@ -1446,7 +1447,7 @@ public SamlProviderConfig getSamlProviderConfig(@NonNull String providerId) /** * Similar to {@link #getSamlProviderConfig(String)} but performs the operation asynchronously. - * Page size will be limited to 100 provider configs. + * Page size is limited to 100 provider configs. * * @param providerId A provider ID string. * @return An {@code ApiFuture} which will complete successfully with an @@ -1475,7 +1476,7 @@ protected SamlProviderConfig execute() throws FirebaseAuthException { /** * Gets a page of SAML Auth provider configs starting from the specified {@code pageToken}. Page - * size will be limited to 100 provider configs. + * 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. @@ -1509,7 +1510,7 @@ public ListProviderConfigsPage listSamlProviderConfigs( /** * Similar to {@link #listSamlProviderConfigs(String)} but performs the operation asynchronously. - * Page size will be limited to 100 provider configs. + * 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. diff --git a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java index 23125244b..90a49e624 100644 --- a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java +++ b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java @@ -72,8 +72,7 @@ public static final class CreateRequest extends AbstractCreateRequestThe returned object should be passed to - * {@link AbstractFirebaseAuth#createOidcProviderConfig(CreateRequest)} to register the provider - * information persistently. + * {@link AbstractFirebaseAuth#createOidcProviderConfig(CreateRequest)} to save the config. */ public CreateRequest() { } @@ -135,8 +134,8 @@ public static final class UpdateRequest extends AbstractUpdateRequestThe returned object should be passed to - * {@link AbstractFirebaseAuth#updateOidcProviderConfig(CreateRequest)} to update the provider - * information persistently. + * {@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