diff --git a/servlet/spring-boot/java/saml2/login/README.adoc b/servlet/spring-boot/java/saml2/login/README.adoc index 73bf4ade2..e68573144 100644 --- a/servlet/spring-boot/java/saml2/login/README.adoc +++ b/servlet/spring-boot/java/saml2/login/README.adoc @@ -19,7 +19,7 @@ The following features are implemented in the MVP: 1. Receive and validate a SAML 2.0 Response containing an assertion, and create a corresponding authentication in Spring Security 2. Send a SAML 2.0 AuthNRequest to an Identity Provider 3. Provide a framework for components used in SAML 2.0 authentication that can be swapped by configuration -4. Work against the Okta SAML 2.0 IDP reference implementation +4. Work against the SimpleSAMLPHP reference implementation === SAML 2.0 Single Logout @@ -31,6 +31,11 @@ You can refer to the https://docs.spring.io/spring-security/reference/servlet/sa == Run the Sample +=== Prerequisites + +This sample requires Docker in order to stand up the identity provider. +If you don't have Docker, you can alternatively disable Docker in `application.yml` and stand up your own IdP. + === Start up the Sample Boot Application ``` ./gradlew :servlet:spring-boot:java:saml2:login:bootRun @@ -40,12 +45,12 @@ You can refer to the https://docs.spring.io/spring-security/reference/servlet/sa http://localhost:8080/ -You will be redirect to the Okta SAML 2.0 IDP +You will be redirected to a chooser page where you can pick between one of two identity providers. === Type in your credentials ``` -User: testuser2@spring.security.saml -Password: 12345678 +User: user1 +Password: user1pass ``` diff --git a/servlet/spring-boot/java/saml2/login/build.gradle b/servlet/spring-boot/java/saml2/login/build.gradle index 5b92b6bdd..8767d557e 100644 --- a/servlet/spring-boot/java/saml2/login/build.gradle +++ b/servlet/spring-boot/java/saml2/login/build.gradle @@ -12,6 +12,14 @@ 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 { @@ -27,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/login/src/integTest/java/example/PreDockerComposeServerPortInitializer.java b/servlet/spring-boot/java/saml2/login/src/integTest/java/example/PreDockerComposeServerPortInitializer.java new file mode 100644 index 000000000..96e373dba --- /dev/null +++ b/servlet/spring-boot/java/saml2/login/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/login/src/integTest/java/example/Saml2LoginApplicationITests.java b/servlet/spring-boot/java/saml2/login/src/integTest/java/example/Saml2LoginApplicationITests.java index f8e68a321..5328297e7 100644 --- a/servlet/spring-boot/java/saml2/login/src/integTest/java/example/Saml2LoginApplicationITests.java +++ b/servlet/spring-boot/java/saml2/login/src/integTest/java/example/Saml2LoginApplicationITests.java @@ -21,28 +21,28 @@ 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.test.web.servlet.MockMvc; +import org.springframework.boot.test.web.server.LocalServerPort; import static org.assertj.core.api.Assertions.assertThat; -@SpringBootTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @AutoConfigureMockMvc public class Saml2LoginApplicationITests { - @Autowired - MockMvc mvc; + @LocalServerPort + int port; @Autowired WebClient webClient; @@ -56,7 +56,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 @@ -82,14 +82,14 @@ void logoutWhenRelyingPartyInitiatedLogoutThenLoginPageWithLogoutParam() throws } private void performLogin() throws Exception { - HtmlPage login = this.webClient.getPage("/"); + HtmlPage login = this.webClient.getPage("http://localhost:" + this.port + "/saml2/authenticate/one"); 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); } @@ -97,7 +97,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/login/src/integTest/resources/META-INF/spring.factories b/servlet/spring-boot/java/saml2/login/src/integTest/resources/META-INF/spring.factories new file mode 100644 index 000000000..f4d2ae40c --- /dev/null +++ b/servlet/spring-boot/java/saml2/login/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/login/src/main/resources/application.yml b/servlet/spring-boot/java/saml2/login/src/main/resources/application.yml index 26d950f32..da7ea5969 100644 --- a/servlet/spring-boot/java/saml2/login/src/main/resources/application.yml +++ b/servlet/spring-boot/java/saml2/login/src/main/resources/application.yml @@ -2,15 +2,34 @@ logging.level: org.springframework.security: TRACE spring: + docker: + compose: + file: docker:docker/compose.yml + readiness: + wait: never + skip: + in-tests: false 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: POST + binding: REDIRECT url: "{baseUrl}/logout/saml2/slo" - assertingparty.metadata-uri: https://dev-05937739.okta.com/app/exk46xofd8NZvFCpS5d7/sso/saml/metadata + assertingparty.metadata-uri: http://idp-one.7f000001.nip.io/simplesaml/saml2/idp/metadata.php + two: + 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-two.7f000001.nip.io/simplesaml/saml2/idp/metadata.php \ No newline at end of file