Update custom-urls Sample to use Docker-based IdP

Issue gh-127
This commit is contained in:
Josh Cummings
2024-11-04 15:49:34 -07:00
parent 29d9bec07f
commit 8eb06469e2
16 changed files with 276 additions and 23 deletions

View File

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

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CheckStyle-IDEA" serialisationVersion="2">
<checkstyleVersion>10.18.1</checkstyleVersion>
<scanScope>JavaOnly</scanScope>
<option name="thirdPartyClasspath" />
<option name="activeLocationIds" />
<option name="locations">
<list>
<ConfigurationLocation id="bundled-sun-checks" type="BUNDLED" scope="All" description="Sun Checks">(bundled)</ConfigurationLocation>
<ConfigurationLocation id="bundled-google-checks" type="BUNDLED" scope="All" description="Google Checks">(bundled)</ConfigurationLocation>
</list>
</option>
</component>
</project>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<DBN-PSQL>
<case-options enabled="false">
<option name="KEYWORD_CASE" value="lower" />
<option name="FUNCTION_CASE" value="lower" />
<option name="PARAMETER_CASE" value="lower" />
<option name="DATATYPE_CASE" value="lower" />
<option name="OBJECT_CASE" value="preserve" />
</case-options>
<formatting-settings enabled="false" />
</DBN-PSQL>
<DBN-SQL>
<case-options enabled="false">
<option name="KEYWORD_CASE" value="lower" />
<option name="FUNCTION_CASE" value="lower" />
<option name="PARAMETER_CASE" value="lower" />
<option name="DATATYPE_CASE" value="lower" />
<option name="OBJECT_CASE" value="preserve" />
</case-options>
<formatting-settings enabled="false">
<option name="STATEMENT_SPACING" value="one_line" />
<option name="CLAUSE_CHOP_DOWN" value="chop_down_if_statement_long" />
<option name="ITERATION_ELEMENTS_WRAPPING" value="chop_down_if_not_single" />
</formatting-settings>
</DBN-SQL>
</code_scheme>
</component>
</project>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://repo.spring.io/milestone" />
</remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="https://repo.spring.io/snapshot" />
</remote-repository>
<remote-repository>
<option name="id" value="maven3" />
<option name="name" value="maven3" />
<option name="url" value="https://build.shibboleth.net/nexus/content/repositories/releases/" />
</remote-repository>
</component>
</project>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK" />
</project>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../../../../.." vcs="Git" />
</component>
</project>

View File

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

View File

@@ -21,18 +21,19 @@ import java.util.List;
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.request.MockMvcRequestBuilder
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 @@ public class CustomUrlsApplicationITests {
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 @@ public class CustomUrlsApplicationITests {
}
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 @@ public class CustomUrlsApplicationITests {
private HtmlForm findForm(HtmlPage login) {
for (HtmlForm form : login.getForms()) {
try {
if (form.getId().equals("form19")) {
if (form.getNameAttribute().equals("f")) {
return form;
}
}

View File

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

View File

@@ -0,0 +1 @@
org.springframework.boot.env.EnvironmentPostProcessor=example.PreDockerComposeServerPortInitializer

View File

@@ -21,12 +21,6 @@ import org.springframework.context.annotation.Configuration;
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 @@ import org.springframework.security.web.SecurityFilterChain;
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 @@ public class SecurityConfiguration {
)
.saml2Login(Customizer.withDefaults())
.saml2Logout(Customizer.withDefaults())
.addFilterBefore(metadataFilter, Saml2WebSsoAuthenticationFilter.class);
.saml2Metadata(Customizer.withDefaults());
// @formatter:on
return http.build();
}

View File

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

View File

@@ -1,5 +1,8 @@
<?php
$port = getenv("PORT");
// Spring Security SP
$metadata["http://localhost:$port/saml2/metadata"] = array(
'AssertionConsumerService' => "https://localhost:$port/login/saml2/sso",
'SingleLogoutService' => "https://localhost:$port/logout/saml2/slo",
@@ -10,4 +13,23 @@ $metadata["http://localhost:$port/saml2/metadata"] = array(
'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,
);
?>

View File

@@ -1,5 +1,8 @@
<?php
$port = getenv("PORT");
// Spring Security SP
$metadata["http://localhost:$port/saml2/metadata"] = array(
'AssertionConsumerService' => "http://localhost:$port/login/saml2/sso",
'SingleLogoutService' => "http://localhost:$port/logout/saml2/slo",
@@ -10,4 +13,23 @@ $metadata["http://localhost:$port/saml2/metadata"] = array(
'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,
);
?>