Skip to content

Commit

Permalink
feature: Per-zone SAML SP metadata
Browse files Browse the repository at this point in the history
- Made SAML SP metadata generation operation zone-aware.

[#187846376]
  • Loading branch information
hsinn0 committed Jul 3, 2024
1 parent 10cb97d commit f90a2c0
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
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;
Expand All @@ -15,7 +17,8 @@
import java.util.function.Function;

@Slf4j
public class ConfiguratorRelyingPartyRegistrationRepository implements RelyingPartyRegistrationRepository {
public class ConfiguratorRelyingPartyRegistrationRepository
implements RelyingPartyRegistrationRepository, ZoneAware {

private final SamlIdentityProviderConfigurator configurator;
private final KeyWithCert keyWithCert;
Expand Down Expand Up @@ -54,7 +57,7 @@ public RelyingPartyRegistration findByRegistrationId(String registrationId) {
return buildRelyingPartyRegistration(registrationId, identityProviderDefinition);
}
}
return null;
return buildDefaultRelyingPartyRegistration();
}

private RelyingPartyRegistration buildRelyingPartyRegistration(String registrationId, SamlIdentityProviderDefinition def) {
Expand All @@ -78,4 +81,41 @@ private RelyingPartyRegistration buildRelyingPartyRegistration(String registrati
)
.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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.cloudfoundry.identity.uaa.zone;

public interface ZoneAware {
default IdentityZone retrieveZone() {
return IdentityZoneHolder.get();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,47 @@ void samlSPMetadata() {
.contains("<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>");
}

@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<String> 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("<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/>")
//.contains("<ds:SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256\"/>")
// login.saml.signRequest
.contains("AuthnRequestsSigned=\"true\"")
// login.saml.wantAssertionSigned
.contains("WantAssertionsSigned=\"true\"")
// login.saml.nameID
// .contains("<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>");
.contains("/saml/SSO/alias/" + zoneId + ".cloudfoundry-saml-login"); // TODO: Improve this check
}

@Test
void contentTypes() {
String loginUrl = baseUrl + "/login";
Expand Down

0 comments on commit f90a2c0

Please sign in to comment.