diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index 4315a26ac..97d8ec340 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -939,13 +939,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( @@ -961,6 +963,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) { @@ -971,6 +975,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 @@ -1025,7 +1030,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) @@ -1042,7 +1048,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); @@ -1051,7 +1058,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 @@ -1152,7 +1159,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 { @@ -1166,7 +1174,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); @@ -1175,7 +1184,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 @@ -1186,6 +1195,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 690a832fa..9bdc59cb5 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -252,6 +252,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)); @@ -318,15 +320,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 = @@ -365,6 +374,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 @@ -383,6 +397,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('?'); @@ -813,7 +847,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 08d3c3dac..84307d309 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -1606,6 +1606,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 = @@ -1706,41 +1732,279 @@ public void testTenantAwareListOidcProviderConfigs() throws Exception { } @Test - public void testDeleteProviderConfig() throws Exception { + public void testDeleteOidcProviderConfig() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); - FirebaseAuth.getInstance().deleteOidcProviderConfig("PROVIDER_ID"); + FirebaseAuth.getInstance().deleteOidcProviderConfig("oidc.provider-id"); checkRequestHeaders(interceptor); - checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/PROVIDER_ID"); + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); } @Test - public void testDeleteProviderConfigWithNotFoundError() throws Exception { + 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("UNKNOWN"); + 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/UNKNOWN"); + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.UNKNOWN"); } @Test - public void testTenantAwareDeleteProviderConfig() throws Exception { + public void testTenantAwareDeleteOidcProviderConfig() throws Exception { TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( "TENANT_ID", "{}"); TenantAwareFirebaseAuth tenantAwareAuth = FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); - tenantAwareAuth.deleteOidcProviderConfig("PROVIDER_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, "DELETE", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/PROVIDER_ID"); + 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().deleteSamlProviderConfig("saml.provider-id"); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testDeleteSamlProviderMissingId() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + + try { + FirebaseAuth.getInstance().deleteSamlProviderConfig(null); + fail("No error thrown for missing provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testDeleteSamlProviderInvalidId() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + + try { + FirebaseAuth.getInstance().deleteSamlProviderConfig("oidc.invalid-saml-provider-id"); + fail("No error thrown for invalid provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testDeleteSamlProviderConfigWithNotFoundError() throws Exception { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().deleteSamlProviderConfig("saml.UNKNOWN"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.UNKNOWN"); + } + + @Test + public void testTenantAwareDeleteSamlProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + "{}"); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + tenantAwareAuth.deleteSamlProviderConfig("saml.provider-id"); + + checkRequestHeaders(interceptor); + String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs/saml.provider-id"; + checkUrl(interceptor, "DELETE", expectedUrl); } private static TestResponseInterceptor initializeAppForUserManagementWithStatusCode( @@ -1867,6 +2131,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" + } +}