Browse Source

Polish refreshable-metadata Sample

Josh Cummings 9 months ago
parent
commit
2e02987d4c

+ 1 - 1
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");

+ 0 - 112
servlet/spring-boot/java/saml2/refreshable-metadata/src/main/java/example/RefreshableRelyingPartyRegistrationRepository.java

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

+ 126 - 0
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;
+		}
+
+	}
+
+}

+ 13 - 0
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();
+	}
+
 }

+ 63 - 0
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<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()));
+			}
+		};
+	}
+
+}

+ 5 - 15
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