Polish refreshable-metadata Sample

This commit is contained in:
Josh Cummings
2024-11-06 15:45:14 -07:00
parent 0a7da94e22
commit 2e02987d4c
6 changed files with 208 additions and 128 deletions

View File

@@ -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");

View File

@@ -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<RelyingPartyRegistration> {
private final Map<String, RelyingPartyRegistration> 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<RelyingPartyRegistration> iterator() {
return this.relyingPartyRegistrations.values().iterator();
}
@Scheduled(fixedDelay = 30, timeUnit = TimeUnit.MINUTES)
public void refreshMetadata() {
for (Map.Entry<String, Saml2RelyingPartyProperties.Registration> 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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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<RelyingPartyRegistration> iterator() {
Iterator<AssertingPartyMetadata> 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()));
}
};
}
}

View File

@@ -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