From 8eb06469e2e95d830954e9cee75608743b3cc246 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 4 Nov 2024 15:49:34 -0700 Subject: [PATCH] Update custom-urls Sample to use Docker-based IdP Issue gh-127 --- .../java/saml2/custom-urls/.idea/.gitignore | 8 +++ .../custom-urls/.idea/checkstyle-idea.xml | 15 ++++ .../java/saml2/custom-urls/.idea/codeStyles | 31 ++++++++ .../java/saml2/custom-urls/.idea/compiler.xml | 6 ++ .../java/saml2/custom-urls/.idea/gradle.xml | 15 ++++ .../custom-urls/.idea/jarRepositories.xml | 35 ++++++++++ .../java/saml2/custom-urls/.idea/misc.xml | 8 +++ .../java/saml2/custom-urls/.idea/vcs.xml | 6 ++ .../java/saml2/custom-urls/build.gradle | 10 +++ .../example/CustomUrlsApplicationITests.java | 20 +++--- ...PreDockerComposeServerPortInitializer.java | 70 +++++++++++++++++++ .../resources/META-INF/spring.factories | 1 + .../java/example/SecurityConfiguration.java | 15 +--- .../src/main/resources/application.yml | 15 +++- .../docker/metadata/one-relyingparties.php | 22 ++++++ .../docker/metadata/two-relyingparties.php | 22 ++++++ 16 files changed, 276 insertions(+), 23 deletions(-) create mode 100644 servlet/spring-boot/java/saml2/custom-urls/.idea/.gitignore create mode 100644 servlet/spring-boot/java/saml2/custom-urls/.idea/checkstyle-idea.xml create mode 100644 servlet/spring-boot/java/saml2/custom-urls/.idea/codeStyles create mode 100644 servlet/spring-boot/java/saml2/custom-urls/.idea/compiler.xml create mode 100644 servlet/spring-boot/java/saml2/custom-urls/.idea/gradle.xml create mode 100644 servlet/spring-boot/java/saml2/custom-urls/.idea/jarRepositories.xml create mode 100644 servlet/spring-boot/java/saml2/custom-urls/.idea/misc.xml create mode 100644 servlet/spring-boot/java/saml2/custom-urls/.idea/vcs.xml create mode 100644 servlet/spring-boot/java/saml2/custom-urls/src/integTest/java/example/PreDockerComposeServerPortInitializer.java create mode 100644 servlet/spring-boot/java/saml2/custom-urls/src/integTest/resources/META-INF/spring.factories diff --git a/servlet/spring-boot/java/saml2/custom-urls/.idea/.gitignore b/servlet/spring-boot/java/saml2/custom-urls/.idea/.gitignore new file mode 100644 index 000000000..13566b81b --- /dev/null +++ b/servlet/spring-boot/java/saml2/custom-urls/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/servlet/spring-boot/java/saml2/custom-urls/.idea/checkstyle-idea.xml b/servlet/spring-boot/java/saml2/custom-urls/.idea/checkstyle-idea.xml new file mode 100644 index 000000000..0389c4061 --- /dev/null +++ b/servlet/spring-boot/java/saml2/custom-urls/.idea/checkstyle-idea.xml @@ -0,0 +1,15 @@ + + + + 10.18.1 + JavaOnly + + + \ No newline at end of file diff --git a/servlet/spring-boot/java/saml2/custom-urls/.idea/codeStyles b/servlet/spring-boot/java/saml2/custom-urls/.idea/codeStyles new file mode 100644 index 000000000..72a9bbb58 --- /dev/null +++ b/servlet/spring-boot/java/saml2/custom-urls/.idea/codeStyles @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/servlet/spring-boot/java/saml2/custom-urls/.idea/compiler.xml b/servlet/spring-boot/java/saml2/custom-urls/.idea/compiler.xml new file mode 100644 index 000000000..b86273d94 --- /dev/null +++ b/servlet/spring-boot/java/saml2/custom-urls/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/servlet/spring-boot/java/saml2/custom-urls/.idea/gradle.xml b/servlet/spring-boot/java/saml2/custom-urls/.idea/gradle.xml new file mode 100644 index 000000000..f9163b40e --- /dev/null +++ b/servlet/spring-boot/java/saml2/custom-urls/.idea/gradle.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/servlet/spring-boot/java/saml2/custom-urls/.idea/jarRepositories.xml b/servlet/spring-boot/java/saml2/custom-urls/.idea/jarRepositories.xml new file mode 100644 index 000000000..5938bdd69 --- /dev/null +++ b/servlet/spring-boot/java/saml2/custom-urls/.idea/jarRepositories.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/servlet/spring-boot/java/saml2/custom-urls/.idea/misc.xml b/servlet/spring-boot/java/saml2/custom-urls/.idea/misc.xml new file mode 100644 index 000000000..7c47dd48d --- /dev/null +++ b/servlet/spring-boot/java/saml2/custom-urls/.idea/misc.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/servlet/spring-boot/java/saml2/custom-urls/.idea/vcs.xml b/servlet/spring-boot/java/saml2/custom-urls/.idea/vcs.xml new file mode 100644 index 000000000..bc5997070 --- /dev/null +++ b/servlet/spring-boot/java/saml2/custom-urls/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/servlet/spring-boot/java/saml2/custom-urls/build.gradle b/servlet/spring-boot/java/saml2/custom-urls/build.gradle index f2484a768..8767d557e 100644 --- a/servlet/spring-boot/java/saml2/custom-urls/build.gradle +++ b/servlet/spring-boot/java/saml2/custom-urls/build.gradle @@ -12,6 +12,15 @@ repositories { maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" } } +sourceSets.main.java.srcDirs += "$projectDir/../identity-provider/src/main/java" +sourceSets.main.resources.srcDirs += "$projectDir/../identity-provider/src/main/resources" + +if (plugins.hasPlugin("io.spring.javaformat")) { + tasks.formatMain { + dependsOn(":servlet:spring-boot:java:saml2:identity-provider:formatMain") + } +} + dependencies { constraints { implementation "org.opensaml:opensaml-saml-api:5.1.3" @@ -26,6 +35,7 @@ dependencies { testImplementation 'org.htmlunit:htmlunit' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' + runtimeOnly "org.springframework.boot:spring-boot-docker-compose" } tasks.withType(Test).configureEach { diff --git a/servlet/spring-boot/java/saml2/custom-urls/src/integTest/java/example/CustomUrlsApplicationITests.java b/servlet/spring-boot/java/saml2/custom-urls/src/integTest/java/example/CustomUrlsApplicationITests.java index fb4c78e39..f65671e23 100644 --- a/servlet/spring-boot/java/saml2/custom-urls/src/integTest/java/example/CustomUrlsApplicationITests.java +++ b/servlet/spring-boot/java/saml2/custom-urls/src/integTest/java/example/CustomUrlsApplicationITests.java @@ -21,18 +21,19 @@ import org.htmlunit.ElementNotFoundException; import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlButton; import org.htmlunit.html.HtmlElement; import org.htmlunit.html.HtmlForm; import org.htmlunit.html.HtmlInput; import org.htmlunit.html.HtmlPage; import org.htmlunit.html.HtmlPasswordInput; -import org.htmlunit.html.HtmlSubmitInput; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.web.servlet.MockMvc; import static org.assertj.core.api.Assertions.assertThat; @@ -40,10 +41,13 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@SpringBootTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @AutoConfigureMockMvc public class CustomUrlsApplicationITests { + @LocalServerPort + int port; + @Autowired MockMvc mvc; @@ -59,7 +63,7 @@ void setup() { void authenticationAttemptWhenValidThenShowsUserEmailAddress() throws Exception { performLogin(); HtmlPage home = (HtmlPage) this.webClient.getCurrentWindow().getEnclosedPage(); - assertThat(home.asNormalizedText()).contains("You're email address is testuser2@spring.security.saml"); + assertThat(home.asNormalizedText()).contains("You're email address is user1@example.org"); } @Test @@ -83,14 +87,14 @@ void metadataWhenGetThenForwardToUrl() throws Exception { } private void performLogin() throws Exception { - HtmlPage login = this.webClient.getPage("/"); + HtmlPage login = this.webClient.getPage("http://localhost:" + this.port + "/saml/login"); this.webClient.waitForBackgroundJavaScript(10000); HtmlForm form = findForm(login); HtmlInput username = form.getInputByName("username"); HtmlPasswordInput password = form.getInputByName("password"); - HtmlSubmitInput submit = login.getHtmlElementById("okta-signin-submit"); - username.type("testuser2@spring.security.saml"); - password.type("12345678"); + HtmlButton submit = (HtmlButton) form.getElementsByTagName("button").iterator().next(); + username.type("user1"); + password.type("user1pass"); submit.click(); this.webClient.waitForBackgroundJavaScript(10000); } @@ -98,7 +102,7 @@ private void performLogin() throws Exception { private HtmlForm findForm(HtmlPage login) { for (HtmlForm form : login.getForms()) { try { - if (form.getId().equals("form19")) { + if (form.getNameAttribute().equals("f")) { return form; } } diff --git a/servlet/spring-boot/java/saml2/custom-urls/src/integTest/java/example/PreDockerComposeServerPortInitializer.java b/servlet/spring-boot/java/saml2/custom-urls/src/integTest/java/example/PreDockerComposeServerPortInitializer.java new file mode 100644 index 000000000..96e373dba --- /dev/null +++ b/servlet/spring-boot/java/saml2/custom-urls/src/integTest/java/example/PreDockerComposeServerPortInitializer.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-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.IOException; +import java.net.ServerSocket; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.PropertySource; + +/** + * Spring Boot doesn't determine the port before the docker containers are loaded, so + * we'll decide the test port here and override the associated properties. + * + * @author Josh Cummings + */ +public class PreDockerComposeServerPortInitializer implements EnvironmentPostProcessor { + + private static final Integer port = getPort(); + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + environment.getPropertySources().addFirst(new ServerPortPropertySource(port)); + } + + private static Integer getPort() { + try (ServerSocket serverSocket = new ServerSocket(0)) { + return serverSocket.getLocalPort(); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private static class ServerPortPropertySource extends PropertySource { + + ServerPortPropertySource(Integer port) { + super("server.port.override", port); + } + + @Override + public Object getProperty(String name) { + if ("server.port".equals(name)) { + return getSource(); + } + if ("SERVER_PORT".equals(name)) { + return getSource(); + } + return null; + } + + } + +} diff --git a/servlet/spring-boot/java/saml2/custom-urls/src/integTest/resources/META-INF/spring.factories b/servlet/spring-boot/java/saml2/custom-urls/src/integTest/resources/META-INF/spring.factories new file mode 100644 index 000000000..f4d2ae40c --- /dev/null +++ b/servlet/spring-boot/java/saml2/custom-urls/src/integTest/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.env.EnvironmentPostProcessor=example.PreDockerComposeServerPortInitializer \ No newline at end of file diff --git a/servlet/spring-boot/java/saml2/custom-urls/src/main/java/example/SecurityConfiguration.java b/servlet/spring-boot/java/saml2/custom-urls/src/main/java/example/SecurityConfiguration.java index ea58cac7f..97b41f446 100644 --- a/servlet/spring-boot/java/saml2/custom-urls/src/main/java/example/SecurityConfiguration.java +++ b/servlet/spring-boot/java/saml2/custom-urls/src/main/java/example/SecurityConfiguration.java @@ -21,12 +21,6 @@ import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver; -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.security.saml2.provider.service.web.Saml2MetadataFilter; -import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter; import org.springframework.security.web.SecurityFilterChain; @Configuration @@ -34,12 +28,7 @@ public class SecurityConfiguration { @Bean - SecurityFilterChain securityFilterChain(HttpSecurity http, - RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) throws Exception { - RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = new DefaultRelyingPartyRegistrationResolver( - relyingPartyRegistrationRepository); - Saml2MetadataFilter metadataFilter = new Saml2MetadataFilter(relyingPartyRegistrationResolver, - new OpenSamlMetadataResolver()); + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // @formatter:off http .authorizeHttpRequests((authorize) -> authorize @@ -48,7 +37,7 @@ SecurityFilterChain securityFilterChain(HttpSecurity http, ) .saml2Login(Customizer.withDefaults()) .saml2Logout(Customizer.withDefaults()) - .addFilterBefore(metadataFilter, Saml2WebSsoAuthenticationFilter.class); + .saml2Metadata(Customizer.withDefaults()); // @formatter:on return http.build(); } diff --git a/servlet/spring-boot/java/saml2/custom-urls/src/main/resources/application.yml b/servlet/spring-boot/java/saml2/custom-urls/src/main/resources/application.yml index f32f09ac6..b004b612e 100644 --- a/servlet/spring-boot/java/saml2/custom-urls/src/main/resources/application.yml +++ b/servlet/spring-boot/java/saml2/custom-urls/src/main/resources/application.yml @@ -1,4 +1,14 @@ +logging.level: + org.springframework.security: TRACE + spring: + docker: + compose: + file: docker:docker/compose.yml + readiness: + wait: never + skip: + in-tests: false security: filter: dispatcher-types: async, error, request, forward @@ -6,12 +16,13 @@ spring: relyingparty: registration: one: + entity-id: "{baseUrl}/saml/metadata" signing.credentials: - private-key-location: classpath:credentials/rp-private.key certificate-location: classpath:credentials/rp-certificate.crt - assertingparty.metadata-uri: https://dev-05937739.okta.com/app/exk598vc9bHhwoTXM5d7/sso/saml/metadata + assertingparty.metadata-uri: http://idp-one.7f000001.nip.io/simplesaml/saml2/idp/metadata.php singlelogout: - binding: POST + binding: REDIRECT url: "{baseUrl}/saml/logout" responseUrl: "{baseUrl}/saml/SingleLogout" acs: diff --git a/servlet/spring-boot/java/saml2/identity-provider/src/main/resources/docker/metadata/one-relyingparties.php b/servlet/spring-boot/java/saml2/identity-provider/src/main/resources/docker/metadata/one-relyingparties.php index a08fa8c4d..46894211f 100644 --- a/servlet/spring-boot/java/saml2/identity-provider/src/main/resources/docker/metadata/one-relyingparties.php +++ b/servlet/spring-boot/java/saml2/identity-provider/src/main/resources/docker/metadata/one-relyingparties.php @@ -1,5 +1,8 @@ "https://localhost:$port/login/saml2/sso", 'SingleLogoutService' => "https://localhost:$port/logout/saml2/slo", @@ -10,4 +13,23 @@ 'validate.authnrequest' => FALSE, 'redirect.sign' => TRUE, ); + +// Spring Security SAML SP + +$metadata["http://localhost:$port/saml/metadata"] = array( + 'AssertionConsumerService' => "http://localhost:$port/saml/SSO", + 'SingleLogoutService' => [ + [ + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + 'Location' => "http://localhost:$port/saml/logout", + 'ResponseLocation' => "http://localhost:$port/saml/SingleLogout" + ] + ], + 'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', + 'simplesaml.nameidattribute' => 'emailAddress', + 'assertion.encryption' => FALSE, + 'nameid.encryption' => FALSE, + 'validate.authnrequest' => FALSE, + 'redirect.sign' => TRUE, +); ?> diff --git a/servlet/spring-boot/java/saml2/identity-provider/src/main/resources/docker/metadata/two-relyingparties.php b/servlet/spring-boot/java/saml2/identity-provider/src/main/resources/docker/metadata/two-relyingparties.php index a82934eaf..9cf7f3152 100644 --- a/servlet/spring-boot/java/saml2/identity-provider/src/main/resources/docker/metadata/two-relyingparties.php +++ b/servlet/spring-boot/java/saml2/identity-provider/src/main/resources/docker/metadata/two-relyingparties.php @@ -1,5 +1,8 @@ "http://localhost:$port/login/saml2/sso", 'SingleLogoutService' => "http://localhost:$port/logout/saml2/slo", @@ -10,4 +13,23 @@ 'validate.authnrequest' => FALSE, 'redirect.sign' => TRUE, ); + +// Spring Security SAML SP + +$metadata["http://localhost:$port/saml/metadata"] = array( + 'AssertionConsumerService' => "http://localhost:$port/saml/SSO", + 'SingleLogoutService' => [ + [ + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + 'Location' => "http://localhost:$port/saml/logout", + 'ResponseLocation' => "http://localhost:$port/saml/SingleLogout" + ] + ], + 'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', + 'simplesaml.nameidattribute' => 'emailAddress', + 'assertion.encryption' => FALSE, + 'nameid.encryption' => FALSE, + 'validate.authnrequest' => FALSE, + 'redirect.sign' => TRUE, +); ?>