From d3a9463069561fa215cc95aeac754629b4049486 Mon Sep 17 00:00:00 2001 From: Hongchol Sinn Date: Fri, 5 Jul 2024 12:17:55 -0700 Subject: [PATCH] feature: Per-zone SAML SP metadata - Done basic implementation for default registration ID. - Additional changed is needed, e.g. NameIDFormat value population, zone-specific metadata filename, etc. - `ConfiguratorRelyingPartyRegistrationRepository` class change was made based on previous WIP version of the same class, but needs to be rewritten as current version of the class is different. Made a temporary change to keep it working as it is for now. - Need to look into registration-ID-based implementation. [#187846376] --- ...torRelyingPartyRegistrationRepository.java | 69 ++++++++++++++++++- ...ingRelyingPartyRegistrationRepository.java | 5 +- .../saml/RelyingPartyRegistrationBuilder.java | 4 +- .../identity/uaa/zone/ZoneAware.java | 7 ++ .../uaa/integration/feature/SamlLoginIT.java | 46 +++++++++++++ 5 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/zone/ZoneAware.java diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/ConfiguratorRelyingPartyRegistrationRepository.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/ConfiguratorRelyingPartyRegistrationRepository.java index 007baf1368a..73951335d8e 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/ConfiguratorRelyingPartyRegistrationRepository.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/ConfiguratorRelyingPartyRegistrationRepository.java @@ -3,15 +3,24 @@ import lombok.extern.slf4j.Slf4j; import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.util.KeyWithCert; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.ZoneAware; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.util.Assert; import java.util.List; +import static org.cloudfoundry.identity.uaa.provider.saml.RelyingPartyRegistrationBuilder.assertionConsumerServiceLocationFunction; +import static org.cloudfoundry.identity.uaa.provider.saml.RelyingPartyRegistrationBuilder.singleLogoutServiceResponseLocationFunction; + @Slf4j -public class ConfiguratorRelyingPartyRegistrationRepository implements RelyingPartyRegistrationRepository { +public class ConfiguratorRelyingPartyRegistrationRepository + implements RelyingPartyRegistrationRepository, ZoneAware { private final SamlIdentityProviderConfigurator configurator; private final KeyWithCert keyWithCert; @@ -46,6 +55,62 @@ public RelyingPartyRegistration findByRegistrationId(String registrationId) { keyWithCert, identityProviderDefinition.getMetaDataLocation(), registrationId); } } - return null; + return buildDefaultRelyingPartyRegistration(); + } + + private RelyingPartyRegistration buildRelyingPartyRegistration(String registrationId, SamlIdentityProviderDefinition def) { + return RelyingPartyRegistrations + .fromMetadataLocation(def.getMetaDataLocation()) + .entityId(samlEntityID) + .nameIdFormat(def.getNameID()) + .registrationId(registrationId) + .assertionConsumerServiceLocation(assertionConsumerServiceLocationFunction.apply(samlEntityID)) + .assertingPartyDetails(details -> details + .wantAuthnRequestsSigned(samlSignRequest) + ) + .signingX509Credentials(cred -> cred + .add(Saml2X509Credential.signing(keyWithCert.getPrivateKey(), keyWithCert.getCertificate())) + ) + .decryptionX509Credentials(cred -> cred + .add(Saml2X509Credential.decryption(keyWithCert.getPrivateKey(), keyWithCert.getCertificate())) + ) + .build(); + } + + private RelyingPartyRegistration buildDefaultRelyingPartyRegistration() { + String samlEntityID, samlServiceUri; + IdentityZone zone = retrieveZone(); + if (zone.isUaa()) { + samlEntityID = this.samlEntityID; + samlServiceUri = this.samlEntityID; + } + else if (zone.getConfig() != null && zone.getConfig().getSamlConfig() != null) { + + samlEntityID = zone.getConfig().getSamlConfig().getEntityID(); + samlServiceUri = zone.getSubdomain() + "." + this.samlEntityID; + } + else { + return null; + } + + return RelyingPartyRegistrations + .fromMetadataLocation("dummy-saml-idp-metadata.xml") + .entityId(samlEntityID) +// .nameIdFormat(def.getNameID()) +// .registrationId(registrationId) + .assertionConsumerServiceLocation(assertionConsumerServiceLocationFunction.apply(samlServiceUri)) + .singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocationFunction.apply(samlServiceUri)) + // TODO: Verify that we should accept both POST and REDIRECT bindings + .singleLogoutServiceBindings(c -> {c.add(Saml2MessageBinding.REDIRECT); c.add(Saml2MessageBinding.POST);}) + .assertingPartyDetails(details -> details + .wantAuthnRequestsSigned(samlSignRequest) + ) + .signingX509Credentials(cred -> cred + .add(Saml2X509Credential.signing(keyWithCert.getPrivateKey(), keyWithCert.getCertificate())) + ) + .decryptionX509Credentials(cred -> cred + .add(Saml2X509Credential.decryption(keyWithCert.getPrivateKey(), keyWithCert.getCertificate())) + ) + .build(); } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/DelegatingRelyingPartyRegistrationRepository.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/DelegatingRelyingPartyRegistrationRepository.java index 754ea1385fa..ec71e1c0e86 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/DelegatingRelyingPartyRegistrationRepository.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/DelegatingRelyingPartyRegistrationRepository.java @@ -1,5 +1,7 @@ package org.cloudfoundry.identity.uaa.provider.saml; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.cloudfoundry.identity.uaa.zone.ZoneAware; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.util.Assert; @@ -34,9 +36,10 @@ public DelegatingRelyingPartyRegistrationRepository(RelyingPartyRegistrationRepo */ @Override public RelyingPartyRegistration findByRegistrationId(String registrationId) { + boolean isDefaultZone = IdentityZoneHolder.isUaa(); for (RelyingPartyRegistrationRepository repository : this.delegates) { RelyingPartyRegistration registration = repository.findByRegistrationId(registrationId); - if (registration != null) { + if (registration != null && (isDefaultZone || repository instanceof ZoneAware)) { return registration; } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/RelyingPartyRegistrationBuilder.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/RelyingPartyRegistrationBuilder.java index 7391c319f9e..c596f72c80c 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/RelyingPartyRegistrationBuilder.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/RelyingPartyRegistrationBuilder.java @@ -16,8 +16,8 @@ @Slf4j public class RelyingPartyRegistrationBuilder { - private static final UnaryOperator assertionConsumerServiceLocationFunction = "{baseUrl}/saml/SSO/alias/%s"::formatted; - private static final UnaryOperator singleLogoutServiceResponseLocationFunction = "{baseUrl}/saml/SingleLogout/alias/%s"::formatted; + static final UnaryOperator assertionConsumerServiceLocationFunction = "{baseUrl}/saml/SSO/alias/%s"::formatted; + static final UnaryOperator singleLogoutServiceResponseLocationFunction = "{baseUrl}/saml/SingleLogout/alias/%s"::formatted; private static final UnaryOperator singleLogoutServiceLocationFunction = "{baseUrl}/saml/SingleLogout/alias/%s"::formatted; private RelyingPartyRegistrationBuilder() { diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/ZoneAware.java b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/ZoneAware.java new file mode 100644 index 00000000000..d4b39605d8e --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/ZoneAware.java @@ -0,0 +1,7 @@ +package org.cloudfoundry.identity.uaa.zone; + +public interface ZoneAware { + default IdentityZone retrieveZone() { + return IdentityZoneHolder.get(); + } +} diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java index e67cce36542..341359277c7 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java @@ -109,6 +109,7 @@ import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.USER_ATTRIBUTE_PREFIX; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.http.HttpMethod.GET; import static org.springframework.http.HttpMethod.POST; @@ -231,6 +232,51 @@ void samlSPMetadata() { .contains("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"); } + @Test + void samlSPMetadataForZone() { + String zoneId = "testzone1"; + String zoneUrl = baseUrl.replace("localhost", zoneId + ".localhost"); + + //identity client token + RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[]{"zones.write", "zones.read", "scim.zones"}, "identity", "identitysecret") + ); + RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "admin", "adminsecret") + ); + + //create the zone + IdentityZoneConfiguration config = new IdentityZoneConfiguration(); + config.getCorsPolicy().getDefaultConfiguration().setAllowedMethods(List.of(GET.toString(), POST.toString())); + config.getSamlConfig().setEntityID(zoneId + "-saml-login"); + IntegrationTestUtils.createZoneOrUpdateSubdomain(identityClient, baseUrl, zoneId, zoneId, config); + + RestTemplate request = new RestTemplate(); + ResponseEntity response = request.getForEntity( + zoneUrl + "/saml/metadata", String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + String metadataXml = response.getBody(); + + // The SAML SP metadata should match the following UAA configs: + // login.entityID + assertThat(metadataXml).contains("entityID=\"" + zoneId + "-saml-login\"") + // TODO: Are DigestMethod and SignatureMethod needed? + // login.saml.signatureAlgorithm + //.contains("") + //.contains("") + // login.saml.signRequest + .contains("AuthnRequestsSigned=\"true\"") + // login.saml.wantAssertionSigned + .contains("WantAssertionsSigned=\"true\"") + // login.saml.nameID +// .contains("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"); + .contains("/saml/SSO/alias/" + zoneId + ".cloudfoundry-saml-login"); // TODO: Improve this check + + // TODO: Zone-aware filename not implemented yet. +// assertEquals("saml-" + zoneId + "-sp.xml", +// response.getHeaders().getContentDisposition().getFilename()); + } + @Test void contentTypes() { String loginUrl = "%s/login".formatted(baseUrl);