Skip to content

Commit

Permalink
feature: Per-zone SAML SP metadata
Browse files Browse the repository at this point in the history
- 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]
  • Loading branch information
hsinn0 committed Jul 5, 2024
1 parent 0d3a595 commit d3a9463
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}
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
Expand Up @@ -16,8 +16,8 @@
@Slf4j
public class RelyingPartyRegistrationBuilder {

private static final UnaryOperator<String> assertionConsumerServiceLocationFunction = "{baseUrl}/saml/SSO/alias/%s"::formatted;
private static final UnaryOperator<String> singleLogoutServiceResponseLocationFunction = "{baseUrl}/saml/SingleLogout/alias/%s"::formatted;
static final UnaryOperator<String> assertionConsumerServiceLocationFunction = "{baseUrl}/saml/SSO/alias/%s"::formatted;
static final UnaryOperator<String> singleLogoutServiceResponseLocationFunction = "{baseUrl}/saml/SingleLogout/alias/%s"::formatted;
private static final UnaryOperator<String> singleLogoutServiceLocationFunction = "{baseUrl}/saml/SingleLogout/alias/%s"::formatted;

private RelyingPartyRegistrationBuilder() {
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 @@ -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;
Expand Down Expand Up @@ -231,6 +232,51 @@ 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

// 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);
Expand Down

0 comments on commit d3a9463

Please sign in to comment.