diff --git a/servlet/spring-boot/java/saml2/refreshable-metadata/src/integTest/java/example/Saml2LoginApplicationITests.java b/servlet/spring-boot/java/saml2/refreshable-metadata/src/integTest/java/example/Saml2LoginApplicationITests.java index 3ef8a01..3387eea 100644 --- a/servlet/spring-boot/java/saml2/refreshable-metadata/src/integTest/java/example/Saml2LoginApplicationITests.java +++ b/servlet/spring-boot/java/saml2/refreshable-metadata/src/integTest/java/example/Saml2LoginApplicationITests.java @@ -82,7 +82,7 @@ public class Saml2LoginApplicationITests { } private void performLogin() throws Exception { - HtmlPage login = this.webClient.getPage("http://localhost:" + this.port + "/saml2/authenticate/one"); + HtmlPage login = this.webClient.getPage("http://localhost:" + this.port); this.webClient.waitForBackgroundJavaScript(10000); HtmlForm form = findForm(login); HtmlInput username = form.getInputByName("username"); diff --git a/servlet/spring-boot/java/saml2/refreshable-metadata/src/main/java/example/RefreshableRelyingPartyRegistrationRepository.java b/servlet/spring-boot/java/saml2/refreshable-metadata/src/main/java/example/RefreshableRelyingPartyRegistrationRepository.java deleted file mode 100644 index e54c310..0000000 --- a/servlet/spring-boot/java/saml2/refreshable-metadata/src/main/java/example/RefreshableRelyingPartyRegistrationRepository.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package example; - -import java.io.InputStream; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.security.interfaces.RSAPrivateKey; -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; - -import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties; -import org.springframework.core.io.Resource; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.security.converter.RsaKeyConverters; -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.stereotype.Component; - -@Component -public class RefreshableRelyingPartyRegistrationRepository - implements RelyingPartyRegistrationRepository, Iterable { - - private final Map relyingPartyRegistrations = new ConcurrentHashMap<>(); - - private final Saml2RelyingPartyProperties relyingPartyProperties; - - public RefreshableRelyingPartyRegistrationRepository(Saml2RelyingPartyProperties relyingPartyProperties) { - this.relyingPartyProperties = relyingPartyProperties; - refreshMetadata(); - } - - @Override - public RelyingPartyRegistration findByRegistrationId(String registrationId) { - return this.relyingPartyRegistrations.get(registrationId); - } - - @Override - public Iterator iterator() { - return this.relyingPartyRegistrations.values().iterator(); - } - - @Scheduled(fixedDelay = 30, timeUnit = TimeUnit.MINUTES) - public void refreshMetadata() { - for (Map.Entry byRegistrationId : this.relyingPartyProperties - .getRegistration() - .entrySet()) { - fetchMetadata(byRegistrationId.getKey(), byRegistrationId.getValue()); - } - } - - private void fetchMetadata(String registrationId, Saml2RelyingPartyProperties.Registration registration) { - RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations - .fromMetadataLocation(registration.getAssertingparty().getMetadataUri()) - .entityId(registration.getEntityId()) - .assertionConsumerServiceLocation(registration.getAcs().getLocation()) - .singleLogoutServiceLocation(registration.getSinglelogout().getUrl()) - .singleLogoutServiceBinding(registration.getSinglelogout().getBinding()) - .signingX509Credentials((credentials) -> registration.getSigning() - .getCredentials() - .stream() - .map(this::asSigningCredential) - .forEach(credentials::add)) - .registrationId(registrationId) - .build(); - this.relyingPartyRegistrations.put(relyingPartyRegistration.getRegistrationId(), relyingPartyRegistration); - } - - private Saml2X509Credential asSigningCredential( - Saml2RelyingPartyProperties.Registration.Signing.Credential properties) { - RSAPrivateKey privateKey = readPrivateKey(properties.getPrivateKeyLocation()); - X509Certificate certificate = readCertificate(properties.getCertificateLocation()); - return new Saml2X509Credential(privateKey, certificate, Saml2X509Credential.Saml2X509CredentialType.SIGNING); - } - - private RSAPrivateKey readPrivateKey(Resource location) { - try (InputStream inputStream = location.getInputStream()) { - return RsaKeyConverters.pkcs8().convert(inputStream); - } - catch (Exception ex) { - throw new IllegalArgumentException(ex); - } - } - - private X509Certificate readCertificate(Resource location) { - try (InputStream inputStream = location.getInputStream()) { - return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(inputStream); - } - catch (Exception ex) { - throw new IllegalArgumentException(ex); - } - } - -} diff --git a/servlet/spring-boot/java/saml2/refreshable-metadata/src/main/java/example/RelyingPartyMetadata.java b/servlet/spring-boot/java/saml2/refreshable-metadata/src/main/java/example/RelyingPartyMetadata.java new file mode 100644 index 0000000..c9eb27d --- /dev/null +++ b/servlet/spring-boot/java/saml2/refreshable-metadata/src/main/java/example/RelyingPartyMetadata.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package example; + +import java.io.IOException; +import java.io.InputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.io.ApplicationResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties("saml2") +public class RelyingPartyMetadata { + + private final ResourceLoader resourceLoader = new ApplicationResourceLoader(); + + private String entityId = "{baseUrl}/saml2/metadata"; + + private String sso = "{baseUrl}/login/saml2/sso"; + + private SingleLogout slo = new SingleLogout(); + + private X509Certificate certificate; + + private RSAPrivateKey key; + + public RelyingPartyRegistration apply(RelyingPartyRegistration.Builder builder) { + Saml2X509Credential signing = Saml2X509Credential.signing(this.key, this.certificate); + return builder.entityId(this.entityId) + .assertionConsumerServiceLocation(this.sso) + .singleLogoutServiceBinding(this.slo.getBinding()) + .singleLogoutServiceLocation(this.slo.getLocation()) + .singleLogoutServiceResponseLocation(this.slo.getResponseLocation()) + .signingX509Credentials((c) -> c.add(signing)) + .build(); + } + + public void setEntityId(String entityId) { + this.entityId = entityId; + } + + public void setSso(String sso) { + this.sso = sso; + } + + public void setSlo(SingleLogout slo) { + this.slo = slo; + } + + public void setCertificate(String certificate) { + Resource source = this.resourceLoader.getResource(certificate); + try (InputStream in = source.getInputStream()) { + CertificateFactory certificates = CertificateFactory.getInstance("X.509"); + this.certificate = (X509Certificate) certificates.generateCertificate(in); + } + catch (CertificateException | IOException ex) { + throw new IllegalArgumentException(ex); + } + } + + public void setKey(RSAPrivateKey key) { + this.key = key; + } + + public static class SingleLogout { + + private Saml2MessageBinding binding = Saml2MessageBinding.REDIRECT; + + private String location = "{baseUrl}/logout/saml2/slo"; + + private String responseLocation = "{baseUrl}/logout/saml2/slo"; + + public Saml2MessageBinding getBinding() { + return this.binding; + } + + public void setBinding(Saml2MessageBinding binding) { + this.binding = binding; + } + + public String getLocation() { + return this.location; + } + + public void setLocation(String location) { + this.location = location; + } + + public String getResponseLocation() { + if (this.responseLocation == null) { + return this.location; + } + return this.responseLocation; + } + + public void setResponseLocation(String responseLocation) { + this.responseLocation = responseLocation; + } + + } + +} diff --git a/servlet/spring-boot/java/saml2/refreshable-metadata/src/main/java/example/SecurityConfiguration.java b/servlet/spring-boot/java/saml2/refreshable-metadata/src/main/java/example/SecurityConfiguration.java index 1145289..6e70818 100644 --- a/servlet/spring-boot/java/saml2/refreshable-metadata/src/main/java/example/SecurityConfiguration.java +++ b/servlet/spring-boot/java/saml2/refreshable-metadata/src/main/java/example/SecurityConfiguration.java @@ -16,9 +16,13 @@ package example; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadataRepository; +import org.springframework.security.saml2.provider.service.registration.OpenSaml5AssertingPartyMetadataRepository; import org.springframework.security.web.SecurityFilterChain; import static org.springframework.security.config.Customizer.withDefaults; @@ -26,6 +30,10 @@ import static org.springframework.security.config.Customizer.withDefaults; @Configuration public class SecurityConfiguration { + static { + OpenSamlInitializationService.initialize(); + } + @Bean SecurityFilterChain app(HttpSecurity http) throws Exception { // @formatter:off @@ -39,4 +47,9 @@ public class SecurityConfiguration { return http.build(); } + @Bean + AssertingPartyMetadataRepository assertingParties(@Value("${saml2.ap.metadata}") String location) { + return OpenSaml5AssertingPartyMetadataRepository.withTrustedMetadataLocation(location).build(); + } + } diff --git a/servlet/spring-boot/java/saml2/refreshable-metadata/src/main/java/example/SourcedRelyingPartyRegistrationRepository.java b/servlet/spring-boot/java/saml2/refreshable-metadata/src/main/java/example/SourcedRelyingPartyRegistrationRepository.java new file mode 100644 index 0000000..57ea504 --- /dev/null +++ b/servlet/spring-boot/java/saml2/refreshable-metadata/src/main/java/example/SourcedRelyingPartyRegistrationRepository.java @@ -0,0 +1,63 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package example; + +import java.util.Iterator; + +import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata; +import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadataRepository; +import org.springframework.security.saml2.provider.service.registration.IterableRelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.stereotype.Component; + +@Component +public class SourcedRelyingPartyRegistrationRepository implements IterableRelyingPartyRegistrationRepository { + + private final AssertingPartyMetadataRepository assertingParties; + + private final RelyingPartyMetadata metadata; + + public SourcedRelyingPartyRegistrationRepository(AssertingPartyMetadataRepository assertingParties, + RelyingPartyMetadata metadata) { + this.assertingParties = assertingParties; + this.metadata = metadata; + } + + @Override + public RelyingPartyRegistration findByRegistrationId(String registrationId) { + AssertingPartyMetadata metadata = this.assertingParties.findByEntityId(registrationId); + return this.metadata.apply(RelyingPartyRegistration.withAssertingPartyMetadata(metadata)); + } + + @Override + public Iterator iterator() { + Iterator assertingParties = this.assertingParties.iterator(); + RelyingPartyMetadata metadata = this.metadata; + return new Iterator<>() { + @Override + public boolean hasNext() { + return assertingParties.hasNext(); + } + + @Override + public RelyingPartyRegistration next() { + return metadata.apply(RelyingPartyRegistration.withAssertingPartyMetadata(assertingParties.next())); + } + }; + } + +} diff --git a/servlet/spring-boot/java/saml2/refreshable-metadata/src/main/resources/application.yml b/servlet/spring-boot/java/saml2/refreshable-metadata/src/main/resources/application.yml index 7bacfaa..e93525a 100644 --- a/servlet/spring-boot/java/saml2/refreshable-metadata/src/main/resources/application.yml +++ b/servlet/spring-boot/java/saml2/refreshable-metadata/src/main/resources/application.yml @@ -4,21 +4,11 @@ spring: file: classpath:docker/compose.yml readiness: wait: never - security: - saml2: - relyingparty: - registration: - one: - entity-id: "{baseUrl}/saml2/metadata" - acs.location: "{baseUrl}/login/saml2/sso" - signing.credentials: - - private-key-location: classpath:credentials/rp-private.key - certificate-location: classpath:credentials/rp-certificate.crt - singlelogout: - binding: REDIRECT - url: "{baseUrl}/logout/saml2/slo" - assertingparty: - metadata-uri: http://idp-one.7f000001.nip.io/simplesaml/saml2/idp/metadata.php logging.level: org.springframework.security: TRACE + +saml2: + certificate: classpath:credentials/rp-certificate.crt + key: classpath:credentials/rp-private.key + ap.metadata: http://idp-one.7f000001.nip.io/simplesaml/saml2/idp/metadata.php