Skip to content

Commit

Permalink
wip: Identity Zone data for default zone
Browse files Browse the repository at this point in the history
Signed-off-by: Duane May <[email protected]>
Signed-off-by: Peter Chen <[email protected]>
  • Loading branch information
duanemay authored and peterhaochen47 committed Jul 9, 2024
1 parent 70019ee commit b6cb65b
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 66 deletions.
1 change: 1 addition & 0 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ dependencies {

testImplementation(libraries.jsonPathAssert)
testImplementation(libraries.guavaTestLib)
testImplementation(libraries.xmlUnit)

implementation(libraries.commonsIo)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ public void afterPropertiesSet() throws InvalidIdentityZoneDetailsException {
definition.getLinks().getSelfService().setSelfServiceLinksEnabled(selfServiceLinksEnabled);
definition.getLinks().setHomeRedirect(homeRedirect);
definition.getSamlConfig().setCertificate(samlSpCertificate);
// TODO: This needs to pull from the default saml config
definition.getSamlConfig().setWantAssertionSigned(false);
definition.getSamlConfig().setPrivateKey(samlSpPrivateKey);
definition.getSamlConfig().setPrivateKeyPassword(samlSpPrivateKeyPassphrase);
definition.getSamlConfig().setDisableInResponseToCheck(disableSamlInResponseToCheck);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.cloudfoundry.identity.uaa.provider.saml;

import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.cloudfoundry.identity.uaa.zone.SamlConfig;
import org.cloudfoundry.identity.uaa.zone.ZoneAware;
import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
Expand All @@ -11,14 +13,11 @@
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResolver;
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.web.DefaultRelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
Expand All @@ -27,80 +26,73 @@
@RestController
public class SamlMetadataEndpoint implements ZoneAware {
public static final String DEFAULT_REGISTRATION_ID = "example";
private static final String DEFAULT_FILE_NAME = "saml-sp.xml";
private static final String APPLICATION_XML_CHARSET_UTF_8 = "application/xml; charset=UTF-8";
private static final String CONTENT_DISPOSITION_FORMAT = "attachment; filename=\"%s\"; filename*=UTF-8''%s";

// @todo - this should be a Zone aware resolver
private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver;
private final Saml2MetadataResolver saml2MetadataResolver;
private final IdentityZoneManager identityZoneManager;

private String fileName;
private String encodedFileName;

private final Boolean wantAssertionSigned;
private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;

public SamlMetadataEndpoint(RelyingPartyRegistrationRepository relyingPartyRegistrationRepository,
SamlConfigProps samlConfigProps) {
IdentityZoneManager identityZoneManager) {
Assert.notNull(relyingPartyRegistrationRepository, "relyingPartyRegistrationRepository cannot be null");
this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository;
this.relyingPartyRegistrationResolver = new DefaultRelyingPartyRegistrationResolver(relyingPartyRegistrationRepository);
this.identityZoneManager = identityZoneManager;
OpenSamlMetadataResolver resolver = new OpenSamlMetadataResolver();
this.saml2MetadataResolver = resolver;
resolver.setEntityDescriptorCustomizer(new EntityDescriptorCustomizer());
this.wantAssertionSigned = samlConfigProps.getWantAssertionSigned();
setFileName(DEFAULT_FILE_NAME);
}

private class EntityDescriptorCustomizer implements Consumer<OpenSamlMetadataResolver.EntityDescriptorParameters> {
@Override
public void accept(OpenSamlMetadataResolver.EntityDescriptorParameters entityDescriptorParameters) {
SamlConfig samlConfig = identityZoneManager.getCurrentIdentityZone().getConfig().getSamlConfig();

EntityDescriptor descriptor = entityDescriptorParameters.getEntityDescriptor();
SPSSODescriptor spssodescriptor = descriptor.getSPSSODescriptor(SAMLConstants.SAML20P_NS);
spssodescriptor.setWantAssertionsSigned(wantAssertionSigned);
spssodescriptor.setWantAssertionsSigned(samlConfig.isWantAssertionSigned());
spssodescriptor.setAuthnRequestsSigned(entityDescriptorParameters.getRelyingPartyRegistration().getAssertingPartyDetails().getWantAuthnRequestsSigned());
}
}

@GetMapping(value = "/saml/metadata", produces = APPLICATION_XML_CHARSET_UTF_8)
public ResponseEntity<String> legacyMetadataEndpoint(HttpServletRequest request) {
return metadataEndpoint(DEFAULT_REGISTRATION_ID, request);
public ResponseEntity<String> legacyMetadataEndpoint() {
return metadataEndpoint(DEFAULT_REGISTRATION_ID);
}

@GetMapping(value = "/saml/metadata/{registrationId}", produces = APPLICATION_XML_CHARSET_UTF_8)
public ResponseEntity<String> metadataEndpoint(@PathVariable String registrationId, HttpServletRequest request) {
public ResponseEntity<String> metadataEndpoint(@PathVariable String registrationId) {
RelyingPartyRegistration relyingPartyRegistration = relyingPartyRegistrationRepository.findByRegistrationId(registrationId);
if (relyingPartyRegistration == null) {
return ResponseEntity.status(HttpServletResponse.SC_UNAUTHORIZED).build();
}
String metadata = saml2MetadataResolver.resolve(relyingPartyRegistration);

// @todo - fileName may need to be dynamic based on registrationID
String[] fileNames = retrieveZoneAwareFileNames();
String contentDisposition = ContentDispositionFilename.getContentDisposition(retrieveZone());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, String.format(
CONTENT_DISPOSITION_FORMAT, fileNames[0], fileNames[1]))
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(metadata);
}
}

public void setFileName(String fileName) {
encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
this.fileName = fileName;
}
record ContentDispositionFilename(String fileName) {
private static final String CONTENT_DISPOSITION_FORMAT = "attachment; filename=\"%s\"; filename*=UTF-8''%s";
private static final String DEFAULT_FILE_NAME = "saml-sp.xml";

private String[] retrieveZoneAwareFileNames() {
IdentityZone zone = retrieveZone();
String[] fileNames = new String[2];
static ContentDispositionFilename retrieveZoneAwareContentDispositionFilename(IdentityZone zone) {
if (zone.isUaa()) {
fileNames[0] = fileName;
fileNames[1] = encodedFileName;
}
else {
fileNames[0] = "saml-" + zone.getSubdomain() + "-sp.xml";
fileNames[1] = URLEncoder.encode(fileNames[0],
StandardCharsets.UTF_8);
return new ContentDispositionFilename(DEFAULT_FILE_NAME);
}
return fileNames;
String filename = "saml-%s-sp.xml".formatted(zone.getSubdomain());
return new ContentDispositionFilename(filename);
}

static String getContentDisposition(IdentityZone zone) {
return retrieveZoneAwareContentDispositionFilename(zone).getContentDisposition();
}

String getContentDisposition() {
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
return CONTENT_DISPOSITION_FORMAT.formatted(fileName, encodedFileName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,16 @@ void defaultSamlKeys() throws Exception {
assertThat(uaa.getConfig().getSamlConfig().getCertificate()).isEqualTo(SamlTestUtils.PROVIDER_CERTIFICATE);
}

@Test
void samlWantAssertionSigned() throws Exception {
bootstrap.setSamlSpPrivateKey(SamlTestUtils.PROVIDER_PRIVATE_KEY);
bootstrap.setSamlSpCertificate(SamlTestUtils.PROVIDER_CERTIFICATE);
bootstrap.setSamlSpPrivateKeyPassphrase(SamlTestUtils.PROVIDER_PRIVATE_KEY_PASSWORD);
bootstrap.afterPropertiesSet();
IdentityZone uaa = provisioning.retrieve(IdentityZone.getUaaZoneId());
assertThat(uaa.getConfig().getSamlConfig().isWantAssertionSigned()).isEqualTo(false);
}

@Test
void enableInResponseTo() throws Exception {
bootstrap.setDisableSamlInResponseToCheck(false);
Expand Down Expand Up @@ -253,7 +263,6 @@ void logoutRedirect() throws Exception {
assertThat(config.getLinks().getLogout().isDisableRedirectParameter()).isFalse();
}


@Test
void testPrompts() throws Exception {
List<Prompt> prompts = Arrays.asList(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package org.cloudfoundry.identity.uaa.provider.saml;

import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration;
import org.cloudfoundry.identity.uaa.zone.SamlConfig;
import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockHttpServletRequest;
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.Saml2MessageBinding;
import org.xmlunit.assertj.XmlAssert;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.cloudfoundry.identity.uaa.provider.saml.Saml2TestUtils.xmlNamespaces;
import static org.cloudfoundry.identity.uaa.provider.saml.TestSaml2X509Credentials.relyingPartySigningCredential;
import static org.cloudfoundry.identity.uaa.provider.saml.TestSaml2X509Credentials.relyingPartyVerifyingCredential;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class SamlMetadataEndpointTest {
private static final String ASSERTION_CONSUMER_SERVICE = "https://acsl";
private static final String REGISTRATION_ID = "regId";
private static final String ENTITY_ID = "entityId";

SamlMetadataEndpoint endpoint;

@Mock
RelyingPartyRegistrationRepository repository;
@Mock
IdentityZoneManager identityZoneManager;
@Mock
RelyingPartyRegistration registration;
@Mock
IdentityZone identityZone;
@Mock
IdentityZoneConfiguration identityZoneConfiguration;
@Mock
SamlConfig samlConfig;
@Mock
RelyingPartyRegistration.AssertingPartyDetails assertingPartyDetails;

@BeforeEach
void setUp() {
endpoint = spy(new SamlMetadataEndpoint(repository, identityZoneManager));
when(repository.findByRegistrationId(REGISTRATION_ID)).thenReturn(registration);
when(registration.getEntityId()).thenReturn(ENTITY_ID);
when(registration.getSigningX509Credentials()).thenReturn(List.of(relyingPartySigningCredential()));
when(registration.getDecryptionX509Credentials()).thenReturn(List.of(relyingPartyVerifyingCredential()));
when(registration.getAssertionConsumerServiceBinding()).thenReturn(Saml2MessageBinding.REDIRECT);
when(registration.getAssertionConsumerServiceLocation()).thenReturn(ASSERTION_CONSUMER_SERVICE);
when(identityZoneManager.getCurrentIdentityZone()).thenReturn(identityZone);
when(identityZone.getConfig()).thenReturn(identityZoneConfiguration);
when(identityZoneConfiguration.getSamlConfig()).thenReturn(samlConfig);
when(registration.getAssertingPartyDetails()).thenReturn(assertingPartyDetails);
}

@Test
void testDefaultFileName() {
ResponseEntity<String> response = endpoint.metadataEndpoint(REGISTRATION_ID);
assertThat(response.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION))
.isEqualTo("attachment; filename=\"saml-sp.xml\"; filename*=UTF-8''saml-sp.xml");
}

@Test
void testZonedFileName() {
when(identityZone.isUaa()).thenReturn(false);
when(identityZone.getSubdomain()).thenReturn("testzone1");
when(endpoint.retrieveZone()).thenReturn(identityZone);

ResponseEntity<String> response = endpoint.metadataEndpoint(REGISTRATION_ID);
assertThat(response.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION))
.isEqualTo("attachment; filename=\"saml-testzone1-sp.xml\"; filename*=UTF-8''saml-testzone1-sp.xml");
}

@Test
void testDefaultMetadataXml() {
when(samlConfig.isWantAssertionSigned()).thenReturn(true);
when(assertingPartyDetails.getWantAuthnRequestsSigned()).thenReturn(true);

ResponseEntity<String> response = endpoint.metadataEndpoint(REGISTRATION_ID);
XmlAssert xmlAssert =XmlAssert.assertThat(response.getBody()).withNamespaceContext(xmlNamespaces());
xmlAssert.valueByXPath("//md:EntityDescriptor/@entityID").isEqualTo(ENTITY_ID);
xmlAssert.valueByXPath("//md:SPSSODescriptor/@AuthnRequestsSigned").isEqualTo(true);
xmlAssert.valueByXPath("//md:SPSSODescriptor/@WantAssertionsSigned").isEqualTo(true);
xmlAssert.valueByXPath("//md:AssertionConsumerService/@Location").isEqualTo(ASSERTION_CONSUMER_SERVICE);
}

@Test
void testDefaultMetadataXml_alternateValues() {
when(samlConfig.isWantAssertionSigned()).thenReturn(false);
when(assertingPartyDetails.getWantAuthnRequestsSigned()).thenReturn(false);

ResponseEntity<String> response = endpoint.metadataEndpoint(REGISTRATION_ID);
XmlAssert xmlAssert =XmlAssert.assertThat(response.getBody()).withNamespaceContext(xmlNamespaces());
xmlAssert.valueByXPath("//md:SPSSODescriptor/@AuthnRequestsSigned").isEqualTo(false);
xmlAssert.valueByXPath("//md:SPSSODescriptor/@WantAssertionsSigned").isEqualTo(false);
}
}
Loading

0 comments on commit b6cb65b

Please sign in to comment.