diff --git a/Jenkinsfile b/Jenkinsfile index dc2a92cae..6179b6acf 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -36,7 +36,7 @@ pipeline { steps { sh ''' docker stop shibui || true && docker rm shibui || true - docker run -d --restart always --name shibui -p 8080:8080 -v /etc/shibui/application.properties:/application.properties -m 3GB --memory-swap=3GB unicon/shibui:latest + docker run -d --restart always --name shibui -p 8080:8080 -v /etc/shibui:/conf -v /etc/shibui/application.yml:/application.yml -m 3GB --memory-swap=3GB unicon/shibui-pac4j:latest ''' } } diff --git a/backend/Dockerfile b/backend/Dockerfile index cec9c4c44..2bce3ec96 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -3,7 +3,8 @@ FROM gcr.io/distroless/java ARG JAR_FILE COPY ${JAR_FILE} app.jar +COPY loader.properties loader.properties EXPOSE 8080 -CMD ["app.jar"] \ No newline at end of file +ENTRYPOINT ["/usr/bin/java", "-jar", "app.jar"] \ No newline at end of file diff --git a/backend/build.gradle b/backend/build.gradle index f3204d4f6..0710d1888 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -34,16 +34,36 @@ processResources.dependsOn(':ui:npm_run_buildProd') bootWar.dependsOn(':ui:npm_run_buildProd') bootWar.baseName = 'shibui' bootWar { - manifest { - attributes("Manifest-Version" : "1.0", "Implementation-Version" : "${project.version}") - } - from(tasks.findByPath(':ui:npm_run_buildProd').outputs) { + manifest { + attributes( + "Manifest-Version" : "1.0", + "Implementation-Version" : "${project.version}" + ) + } + from(tasks.findByPath(':ui:npm_run_buildProd').outputs) { // into '/' into '/public' } archiveName = "${baseName}.war" } +bootJar.dependsOn ':ui:npm_run_buildProd' +bootJar.baseName = 'shibui' +bootJar { + manifest { + attributes( + "Manifest-Version" : "1.0", + "Implementation-Version" : "${project.version}", + 'Main-Class': 'org.springframework.boot.loader.PropertiesLauncher' + ) + } + from(tasks.findByPath(':ui:npm_run_buildProd').outputs) { + // into '/' + into '/public' + } + archiveName = "${baseName}.jar" +} + springBoot { mainClassName = 'edu.internet2.tier.shibboleth.admin.ui.ShibbolethUiApplication' buildInfo() @@ -213,12 +233,13 @@ jacocoTestReport { } } -tasks.docker.dependsOn tasks.build +tasks.docker.dependsOn tasks.bootJar docker { name 'unicon/shibui' tags 'latest' pull true noCache true - files tasks.bootWar.outputs - buildArgs(['JAR_FILE': 'shibui.war']) -} + files tasks.bootJar.outputs + files 'src/main/docker-files/loader.properties' + buildArgs(['JAR_FILE': 'shibui.jar']) +} \ No newline at end of file diff --git a/backend/src/main/docker-files/loader.properties b/backend/src/main/docker-files/loader.properties new file mode 100644 index 000000000..646068881 --- /dev/null +++ b/backend/src/main/docker-files/loader.properties @@ -0,0 +1 @@ +loader.path=libs/ diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java index 1fc74cf9d..a1ccd6c25 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java @@ -3,21 +3,28 @@ import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.Profile; import org.springframework.context.event.EventListener; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.stereotype.Component; @SpringBootApplication +@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = "edu.internet2.tier.shibboleth.admin.ui.configuration.auto.*")) @EntityScan(basePackages = "edu.internet2.tier.shibboleth.admin.ui.domain") @EnableJpaAuditing @EnableScheduling +@EnableWebSecurity public class ShibbolethUiApplication extends SpringBootServletInitializer { @Override diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/WebSecurityConfig.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java similarity index 86% rename from backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/WebSecurityConfig.java rename to backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java index 4ef76087f..2e334f75f 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/WebSecurityConfig.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java @@ -1,12 +1,15 @@ -package edu.internet2.tier.shibboleth.admin.ui.configuration; +package edu.internet2.tier.shibboleth.admin.ui.configuration.auto; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.security.servlet.SpringBootWebSecurityConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.firewall.HttpFirewall; @@ -18,7 +21,9 @@ * * Workaround for slashes in URL from [https://stackoverflow.com/questions/48453980/spring-5-0-3-requestrejectedexception-the-request-was-rejected-because-the-url] */ -@EnableWebSecurity +@Configuration +@AutoConfigureBefore(SpringBootWebSecurityConfiguration.class) +@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) public class WebSecurityConfig { @Value("${shibui.logout-url:/dashboard}") @@ -35,7 +40,7 @@ public HttpFirewall allowUrlEncodedSlashHttpFirewall() { } @Bean - @Profile("default") + @Profile("!no-auth") public WebSecurityConfigurerAdapter defaultAuth() { return new WebSecurityConfigurerAdapter() { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RequestInitiator.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RequestInitiator.java new file mode 100644 index 000000000..78fd0028e --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RequestInitiator.java @@ -0,0 +1,50 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.core.xml.util.AttributeMap; + +import javax.annotation.Nonnull; + +public class RequestInitiator extends AbstractElementExtensibleXMLObject implements org.opensaml.saml.ext.saml2mdreqinit.RequestInitiator { + private String binding; + @Override + public String getBinding() { + return this.binding; + } + + @Override + public void setBinding(String binding) { + this.binding = binding; + } + + private String location; + + @Override + public String getLocation() { + return location; + } + + @Override + public void setLocation(String location) { + this.location = location; + } + + private String responseLocation; + + @Override + public String getResponseLocation() { + return this.responseLocation; + } + + @Override + public void setResponseLocation(String location) { + this.responseLocation = location; + } + + private AttributeMap attributeMap = new AttributeMap(this); + + @Nonnull + @Override + public AttributeMap getUnknownAttributes() { + return this.attributeMap; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RequestInitiatorBuilder.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RequestInitiatorBuilder.java new file mode 100644 index 000000000..98d554e37 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RequestInitiatorBuilder.java @@ -0,0 +1,43 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import org.opensaml.saml.common.AbstractSAMLObjectBuilder; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.w3c.dom.Element; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.xml.namespace.QName; + +public class RequestInitiatorBuilder extends AbstractSAMLObjectBuilder { + + /** + * Constructor. + */ + public RequestInitiatorBuilder() { + + } + + /** {@inheritDoc} */ + public RequestInitiator buildObject() { + return buildObject(SAMLConstants.SAML20MDRI_NS, org.opensaml.saml.ext.saml2mdreqinit.RequestInitiator.DEFAULT_ELEMENT_LOCAL_NAME, + SAMLConstants.SAML20MDRI_PREFIX); + } + + /** {@inheritDoc} */ + public RequestInitiator buildObject(final String namespaceURI, final String localName, + final String namespacePrefix) { + RequestInitiator o = new RequestInitiator(); + o.setNamespaceURI(namespaceURI); + o.setElementLocalName(localName); + o.setNamespacePrefix(namespacePrefix); + return o; + } + + @Nonnull + @Override + public RequestInitiator buildObject(@Nullable String namespaceURI, @Nonnull String localName, @Nullable String namespacePrefix, @Nullable QName schemaType) { + RequestInitiator requestInitiator = buildObject(namespaceURI, localName, namespacePrefix); + requestInitiator.setSchemaType(schemaType); + return requestInitiator; + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptor.java index fa6600543..fc235c110 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptor.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptor.java @@ -83,11 +83,7 @@ public void setSupportedProtocols(List supportedProtocols) { @Override public boolean isSupportedProtocol(String s) { - return isSupportedProtocol; - } - - public void setIsSupportedProtocol(boolean isSupportedProtocol) { - this.isSupportedProtocol = isSupportedProtocol; + return this.supportedProtocols.contains(s); } @Override diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SPSSODescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SPSSODescriptor.java index 5688a19cb..cda00fe4f 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SPSSODescriptor.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SPSSODescriptor.java @@ -32,7 +32,7 @@ public class SPSSODescriptor extends SSODescriptor implements org.opensaml.saml. @Override public Boolean isAuthnRequestsSigned() { - return isAuthnRequestsSigned; + return this.isAuthnRequestsSigned == null ? false : this.isAuthnRequestsSigned; } @Override @@ -55,7 +55,7 @@ public void setAuthnRequestsSigned(XSBooleanValue xsBooleanValue) { @Override public Boolean getWantAssertionsSigned() { - return wantAssertionsSigned; + return wantAssertionsSigned == null ? false : wantAssertionsSigned; } @Override diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Data.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Data.java index 10fc4bdb7..46c58324a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Data.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Data.java @@ -65,7 +65,7 @@ public List getX509SubjectNames() { @Nonnull @Override public List getX509Certificates() { - return Arrays.asList(this.xmlObjects.stream().filter(i -> i instanceof org.opensaml.xmlsec.signature.X509Certificate).toArray(org.opensaml.xmlsec.signature.X509Certificate[]::new)); + return new ArrayList<>(Arrays.asList(this.xmlObjects.stream().filter(i -> i instanceof org.opensaml.xmlsec.signature.X509Certificate).toArray(org.opensaml.xmlsec.signature.X509Certificate[]::new))); } public void addX509Certificate(edu.internet2.tier.shibboleth.admin.ui.domain.X509Certificate x509Certificate) { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/InitializationService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/InitializationService.java index 38bdb6784..4a554110c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/InitializationService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/InitializationService.java @@ -2,7 +2,6 @@ import org.opensaml.core.config.InitializationException; import org.opensaml.core.config.Initializer; -import org.opensaml.core.xml.config.XMLObjectProviderInitializer; import java.util.ServiceLoader; @@ -15,7 +14,11 @@ protected InitializationService() { public static synchronized void initialize() throws InitializationException { final ServiceLoader serviceLoader = ServiceLoader.load(Initializer.class); for (Initializer initializer : serviceLoader) { - if (initializer.getClass().equals(org.opensaml.saml.config.impl.XMLObjectProviderInitializer.class) || initializer.getClass().equals(XMLObjectProviderInitializer.class) || initializer.getClass().equals(org.opensaml.xmlsec.config.impl.XMLObjectProviderInitializer.class)) { + if ( + initializer.getClass().equals(org.opensaml.saml.config.impl.XMLObjectProviderInitializer.class) + || initializer.getClass().equals(org.opensaml.core.xml.config.XMLObjectProviderInitializer.class) + || initializer.getClass().equals(org.opensaml.xmlsec.config.impl.XMLObjectProviderInitializer.class) + ) { continue; } initializer.init(); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/JPAXMLObjectProviderInitializer.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/JPAXMLObjectProviderInitializer.java index f21fdc93d..2b6718dd1 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/JPAXMLObjectProviderInitializer.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/JPAXMLObjectProviderInitializer.java @@ -13,7 +13,12 @@ protected String[] getConfigResources() { "/jpa-saml2-assertion-config.xml", "/jpa-schema-config.xml", "/jpa-saml2-metadata-ui-config.xml", - "/jpa-signature-config.xml" + "/jpa-signature-config.xml", + "/encryption-config.xml", + "/saml2-metadata-algorithm-config.xml", + "/jpa-saml2-metadata-reqinit-config.xml", + "/saml2-protocol-config.xml", + "/modified-saml2-assertion-config.xml" }; } } diff --git a/backend/src/main/resources/META-INF/spring.factories b/backend/src/main/resources/META-INF/spring.factories index fc0a891d0..c03acd3ec 100644 --- a/backend/src/main/resources/META-INF/spring.factories +++ b/backend/src/main/resources/META-INF/spring.factories @@ -1,2 +1,4 @@ org.springframework.boot.env.EnvironmentPostProcessor=\ - edu.internet2.tier.shibboleth.admin.ui.configuration.postprocessors.IdpHomeValueSettingEnvironmentPostProcessor \ No newline at end of file + edu.internet2.tier.shibboleth.admin.ui.configuration.postprocessors.IdpHomeValueSettingEnvironmentPostProcessor +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + edu.internet2.tier.shibboleth.admin.ui.configuration.auto.WebSecurityConfig \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 7b290d0d4..f97ab24a7 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -45,7 +45,7 @@ spring.jpa.hibernate.use-new-id-generator-mappings=true # shibui.metadata-dir=/opt/shibboleth-idp/metadata/generated shibui.logout-url=/dashboard -spring.profiles.active=default +# spring.profiles.active=default #shibui.default-password= diff --git a/backend/src/main/resources/jpa-saml2-metadata-reqinit-config.xml b/backend/src/main/resources/jpa-saml2-metadata-reqinit-config.xml new file mode 100644 index 000000000..868961e13 --- /dev/null +++ b/backend/src/main/resources/jpa-saml2-metadata-reqinit-config.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/main/resources/modified-saml2-assertion-config.xml b/backend/src/main/resources/modified-saml2-assertion-config.xml new file mode 100644 index 000000000..1f9d649a0 --- /dev/null +++ b/backend/src/main/resources/modified-saml2-assertion-config.xml @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/Pac4jTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/Pac4jTest.groovy new file mode 100644 index 000000000..16b48b7c8 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/Pac4jTest.groovy @@ -0,0 +1,19 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain + +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import spock.lang.Specification + +class Pac4jTest extends Specification { + OpenSamlObjects openSamlObjects = new OpenSamlObjects().with { + init() + it + } + + def "test unmarshalling pac4j created metadata"() { + when: + def metadata = openSamlObjects.unmarshalFromXml this.class.getResourceAsStream('/metadata/SHIBUI-808.xml').bytes + + then: + noExceptionThrown() + } +} diff --git a/backend/src/test/resources/metadata/SHIBUI-808.xml b/backend/src/test/resources/metadata/SHIBUI-808.xml new file mode 100644 index 000000000..9cfab103c --- /dev/null +++ b/backend/src/test/resources/metadata/SHIBUI-808.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + MIICpzCCAY+gAwIBAgIBATANBgkqhkiG9w0BAQUFADAXMRUwEwYDVQQDDAw1YjhmNDI2MmEzZmQw + HhcNMTgwOTI2MTU0NzQ4WhcNMTkwOTI2MTU0NzQ5WjAXMRUwEwYDVQQDDAw1YjhmNDI2MmEzZmQw + ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCEFup+XxSTiXS/XnT1R7vvPZfUG+jDwbgE + 5JInMzOIyQna0KnynNM/Zxe/ZtlU+ArOVC5b5WtMomyefLxyxKQI/+aCM5OkJ8gpsJAfDnQDhB0r + dwMZ/n9T2iktiKIS963v4n9+nacx/vlD2t+FFvnHSBcoNUIncvp2lKh9JZNzuGMkipeXibb5wGjN + KYC0MBpXX90lHH9L0xP7+B9hHOI3rnwVKzHwh5oEuDSH8h2ZYQMDAEPHmLSbGP4F1N0Zr/FY+tK1 + LlY0LKxoFk1a6OKUDIT2IHljR7oqaBJRGkMFgFJK0cL7DKtrHZoNLUdaiWVWfoNaW/4k2LarBLLZ + cO77AgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAGgJCR5v2F+d8/Jvl92OSaKldY07mUFTDtxHUpKw + dl0lnL3c75g4ISjFPIztuDzsAB9x8vHu4+5+rm383to4i5TL/DoOZmiFAn6WIGe0c+qE+JyNG30G + mmVjHP33s9qy0tqJe/9qtpAqXdQIC0XuqmfW49J62iE/vtClSVn8tUof7TRtrH1QGxkZJjLbG7JC + F9Z1JbvgN2pMvElhpX4SnjALUd10lB2stlKjEc5cB2BnqcOyikTxgA+zDnemy6k6JFxi9/oxKNuW + tGEr1nX42AyL1k6IpSgZikBGlwI3Rj69FoRMQayhG7pK5/XdzZ8D8YbresX2qHA5EbNrcajBsGI= + + + + + + + MIICpzCCAY+gAwIBAgIBATANBgkqhkiG9w0BAQUFADAXMRUwEwYDVQQDDAw1YjhmNDI2MmEzZmQw + HhcNMTgwOTI2MTU0NzQ4WhcNMTkwOTI2MTU0NzQ5WjAXMRUwEwYDVQQDDAw1YjhmNDI2MmEzZmQw + ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCEFup+XxSTiXS/XnT1R7vvPZfUG+jDwbgE + 5JInMzOIyQna0KnynNM/Zxe/ZtlU+ArOVC5b5WtMomyefLxyxKQI/+aCM5OkJ8gpsJAfDnQDhB0r + dwMZ/n9T2iktiKIS963v4n9+nacx/vlD2t+FFvnHSBcoNUIncvp2lKh9JZNzuGMkipeXibb5wGjN + KYC0MBpXX90lHH9L0xP7+B9hHOI3rnwVKzHwh5oEuDSH8h2ZYQMDAEPHmLSbGP4F1N0Zr/FY+tK1 + LlY0LKxoFk1a6OKUDIT2IHljR7oqaBJRGkMFgFJK0cL7DKtrHZoNLUdaiWVWfoNaW/4k2LarBLLZ + cO77AgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAGgJCR5v2F+d8/Jvl92OSaKldY07mUFTDtxHUpKw + dl0lnL3c75g4ISjFPIztuDzsAB9x8vHu4+5+rm383to4i5TL/DoOZmiFAn6WIGe0c+qE+JyNG30G + mmVjHP33s9qy0tqJe/9qtpAqXdQIC0XuqmfW49J62iE/vtClSVn8tUof7TRtrH1QGxkZJjLbG7JC + F9Z1JbvgN2pMvElhpX4SnjALUd10lB2stlKjEc5cB2BnqcOyikTxgA+zDnemy6k6JFxi9/oxKNuW + tGEr1nX42AyL1k6IpSgZikBGlwI3Rj69FoRMQayhG7pK5/XdzZ8D8YbresX2qHA5EbNrcajBsGI= + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + + diff --git a/docs/CUSTOMIZATIONS.md b/docs/CUSTOMIZATIONS.md new file mode 100644 index 000000000..0423fbce9 --- /dev/null +++ b/docs/CUSTOMIZATIONS.md @@ -0,0 +1,48 @@ +# Custom Development + +For a full example of creating a custom module for the application, see the `pac4j-module` project. + +As a Spring Boot application built with gradle, there are many opportunities available for +custom development against the application. Note that most customizations should be done in +separate modules, not directly modifying the source to the base application. + +## Application Classpath + +To get customizations in the application, one must make the changes available on the application +classpath. Depending on the deployment method, one has a few options for doing this. + +1. Take advantage of the `Properties Launcher` in the executable JAR +2. Put resources in the appropriate `WEB-INF` directory for a WAR deployment + +If one is adding Spring configuration, register the configuration classes with the Spring +Autoconfiguration service by adding a file called `META-INF/spring.factories` to the JAR +file containing the custom java class and referencing any configuration classes: + +```properties +org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.example.SomeConfiguration +``` + +For more information, see [https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-auto-configuration.html#boot-features-locating-auto-configuration-candidates] + +### Executable JAR + +The executable JAR uses the Properties Launcher provided by Spring Boot ([https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.html#executable-jar-property-launcher-features]). +The easiest way to add something to the classpath is to create a file named `loader.properties` in the same directory +as the jar with the following: + +```properties +loader.path=libs/ +``` + +The noted directory will be added to the classpath, along with any JAR files contained in the directory. + +### WAR + +If deploying a WAR, one would use the standard packaging for providing items to the classpath. + +* JAR files should be placed in `WEB-INF/lib` +* all other resources should be placed `WEB-INF/classes` + +It is highly recommended that a WAR overlay be used to prevent changing the version fingerprint. Overlay +methods exist for both Maven ([https://maven.apache.org/plugins/maven-war-plugin/overlays.html]) and +Gradle ([https://github.com/scalding/gradle-waroverlay-plugin]) projects. \ No newline at end of file diff --git a/INTERNATIONALIZATION.md b/docs/INTERNATIONALIZATION.md similarity index 88% rename from INTERNATIONALIZATION.md rename to docs/INTERNATIONALIZATION.md index c6aadf2e1..08bc37874 100644 --- a/INTERNATIONALIZATION.md +++ b/docs/INTERNATIONALIZATION.md @@ -4,6 +4,11 @@ The Shibboleth UI leverages the messages_*_*.properties files common to Java/Spring applications. The default files are located in `backend > src > main > resources > i18n`. +To use a custom file, it is recommended that you make a copy of the appropriate file and add it to the classpath in the +`i18n` package. See [CUSTOMIZATIONS] for more information. For instance, if using the executable JAR and wanting to +customize the English file, one would create a file called `1ibs/i18n/messages_en.properties` in the same directory as the +JAR file and set the appropriate `loader.properties`. + This will allow any piece of static text in the application to be modified dynamically. ## Usage diff --git a/gradle.properties b/gradle.properties index ad64aeafd..678f56673 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,9 +2,8 @@ name=shibui group=edu.internet2.tier.shibboleth.admin.ui version=1.0.1-SNAPSHOT -shibboleth.version=3.4.0-SNAPSHOT -opensaml.version=3.4.0-SNAPSHOT -xmltooling.version=1.4.7-SNAPSHOT +shibboleth.version=3.4.0 +opensaml.version=3.4.0 spring-boot.version=2.0.0.RELEASE diff --git a/pac4j-module/Dockerfile b/pac4j-module/Dockerfile new file mode 100644 index 000000000..6c78f991e --- /dev/null +++ b/pac4j-module/Dockerfile @@ -0,0 +1,3 @@ +FROM unicon/shibui + +COPY *.jar /libs/ \ No newline at end of file diff --git a/pac4j-module/build.gradle b/pac4j-module/build.gradle new file mode 100644 index 000000000..657add297 --- /dev/null +++ b/pac4j-module/build.gradle @@ -0,0 +1,62 @@ +plugins { + id 'java' + id 'com.palantir.docker' version '0.20.1' + id 'jacoco' + id 'io.franzbecker.gradle-lombok' version '1.13' + id 'org.springframework.boot' version '2.0.0.RELEASE' apply false + id 'io.spring.dependency-management' version '1.0.6.RELEASE' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + maven () { + url 'https://oss.sonatype.org/content/groups/public' + } + jcenter() + maven { + url 'https://build.shibboleth.net/nexus/content/groups/public' + artifactUrls = ['https://build.shibboleth.net/nexus/content/repositories/thirdparty-snapshots'] + } +} + +lombok { + version = "1.16.20" + sha256 = "c5178b18caaa1a15e17b99ba5e4023d2de2ebc18b58cde0f5a04ca4b31c10e6d" +} + +dependencyManagement { + imports { + mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES + } +} + +dependencies { + compileOnly project(':backend') + + compile "org.pac4j:spring-security-pac4j:4.0.0" + compile "org.pac4j:pac4j-core:3.3.0-SNAPSHOT" + compile "org.pac4j:pac4j-saml:3.3.0-SNAPSHOT", { + // opensaml libraries are provided + exclude group: 'org.opensaml' + } + + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" + + docker project(':backend') +} + +docker { + name 'unicon/shibui-pac4j' + tags 'latest-pac4j' + files configurations.runtime, tasks.jar.outputs + noCache true +} + +task testme(type: Copy) { + from configurations.runtime + into temporaryDir +} + +tasks.docker.dependsOn(tasks.jar, ':backend:docker') \ No newline at end of file diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java new file mode 100644 index 000000000..1c28b1ee1 --- /dev/null +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java @@ -0,0 +1,34 @@ +package net.unicon.shibui.pac4j; + +import org.pac4j.core.client.Clients; +import org.pac4j.core.config.Config; +import org.pac4j.saml.client.SAML2Client; +import org.pac4j.saml.client.SAML2ClientConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class Pac4jConfiguration { + @Bean + public Config config(final Pac4jConfigurationProperties pac4jConfigurationProperties) { + final SAML2ClientConfiguration saml2ClientConfiguration = new SAML2ClientConfiguration(); + saml2ClientConfiguration.setKeystorePath(pac4jConfigurationProperties.getKeystorePath()); + saml2ClientConfiguration.setKeystorePassword(pac4jConfigurationProperties.getKeystorePassword()); + saml2ClientConfiguration.setPrivateKeyPassword(pac4jConfigurationProperties.getPrivateKeyPassword()); + saml2ClientConfiguration.setIdentityProviderMetadataPath(pac4jConfigurationProperties.getIdentityProviderMetadataPath()); + saml2ClientConfiguration.setMaximumAuthenticationLifetime(pac4jConfigurationProperties.getMaximumAuthenticationLifetime()); + saml2ClientConfiguration.setServiceProviderEntityId(pac4jConfigurationProperties.getServiceProviderEntityId()); + saml2ClientConfiguration.setServiceProviderMetadataPath(pac4jConfigurationProperties.getServiceProviderMetadataPath()); + saml2ClientConfiguration.setForceServiceProviderMetadataGeneration(pac4jConfigurationProperties.isForceServiceProviderMetadataGeneration()); + saml2ClientConfiguration.setWantsAssertionsSigned(pac4jConfigurationProperties.isWantAssertionsSigned()); + + final SAML2Client saml2Client = new SAML2Client(saml2ClientConfiguration); + saml2Client.setName("Saml2Client"); + + final Clients clients = new Clients(pac4jConfigurationProperties.getCallbackUrl(), saml2Client); + + final Config config = new Config(clients); + return config; + } +} diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java new file mode 100644 index 000000000..afb1369a1 --- /dev/null +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java @@ -0,0 +1,99 @@ +package net.unicon.shibui.pac4j; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "shibui.pac4j") +public class Pac4jConfigurationProperties { + private String keystorePath = "/tmp/samlKeystore.jks"; + private String keystorePassword = "changeit"; + private String privateKeyPassword = "changeit"; + private String identityProviderMetadataPath = "/tmp/idp-metadata.xml"; + private int maximumAuthenticationLifetime = 3600; + private String serviceProviderEntityId = "https://unicon.net/shibui"; + private String serviceProviderMetadataPath = "/tmp/sp-metadata.xml"; + private boolean forceServiceProviderMetadataGeneration = false; + private String callbackUrl; + private boolean wantAssertionsSigned = true; + + public String getKeystorePath() { + return keystorePath; + } + + public void setKeystorePath(String keystorePath) { + this.keystorePath = keystorePath; + } + + public String getKeystorePassword() { + return keystorePassword; + } + + public void setKeystorePassword(String keystorePassword) { + this.keystorePassword = keystorePassword; + } + + public String getPrivateKeyPassword() { + return privateKeyPassword; + } + + public void setPrivateKeyPassword(String privateKeyPassword) { + this.privateKeyPassword = privateKeyPassword; + } + + public String getIdentityProviderMetadataPath() { + return identityProviderMetadataPath; + } + + public void setIdentityProviderMetadataPath(String identityProviderMetadataPath) { + this.identityProviderMetadataPath = identityProviderMetadataPath; + } + + public int getMaximumAuthenticationLifetime() { + return maximumAuthenticationLifetime; + } + + public void setMaximumAuthenticationLifetime(int maximumAuthenticationLifetime) { + this.maximumAuthenticationLifetime = maximumAuthenticationLifetime; + } + + public String getServiceProviderEntityId() { + return serviceProviderEntityId; + } + + public void setServiceProviderEntityId(String serviceProviderEntityId) { + this.serviceProviderEntityId = serviceProviderEntityId; + } + + public String getServiceProviderMetadataPath() { + return serviceProviderMetadataPath; + } + + public void setServiceProviderMetadataPath(String serviceProviderMetadataPath) { + this.serviceProviderMetadataPath = serviceProviderMetadataPath; + } + + public boolean isForceServiceProviderMetadataGeneration() { + return forceServiceProviderMetadataGeneration; + } + + public void setForceServiceProviderMetadataGeneration(boolean forceServiceProviderMetadataGeneration) { + this.forceServiceProviderMetadataGeneration = forceServiceProviderMetadataGeneration; + } + + public String getCallbackUrl() { + return callbackUrl; + } + + public void setCallbackUrl(String callbackUrl) { + this.callbackUrl = callbackUrl; + } + + public boolean isWantAssertionsSigned() { + return wantAssertionsSigned; + } + + public void setWantAssertionsSigned(boolean wantAssertionsSigned) { + this.wantAssertionsSigned = wantAssertionsSigned; + } +} diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jSAMLConfigurationManager.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jSAMLConfigurationManager.java new file mode 100644 index 000000000..dd3f0b10b --- /dev/null +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jSAMLConfigurationManager.java @@ -0,0 +1,13 @@ +package net.unicon.shibui.pac4j; + +import org.pac4j.saml.util.ConfigurationManager; + +import javax.annotation.Priority; + +@Priority(1) +public class Pac4jSAMLConfigurationManager implements ConfigurationManager { + @Override + public void configure() { + // do nothing. we already configuration opensaml elsewhere + } +} diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java new file mode 100644 index 000000000..e3ff9d4b6 --- /dev/null +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java @@ -0,0 +1,68 @@ +package net.unicon.shibui.pac4j; + +import org.pac4j.core.config.Config; +import org.pac4j.springframework.security.web.CallbackFilter; +import org.pac4j.springframework.security.web.SecurityFilter; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.firewall.StrictHttpFirewall; + +@Configuration +@AutoConfigureOrder(-1) +public class WebSecurity { + @Bean("webSecurityConfig") + public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter(final Config config) { + return new Pac4jWebSecurityConfigurerAdapter(config); + } + + @Configuration + @Order(0) + public static class FaviconSecurityConfiguration extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http.antMatcher("/favicon.ico").authorizeRequests().antMatchers("/favicon.ico").permitAll(); + } + } + + @Order(1) + public static class Pac4jWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { + private final Config config; + + public Pac4jWebSecurityConfigurerAdapter(final Config config) { + this.config = config; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + final SecurityFilter securityFilter = new SecurityFilter(this.config, "Saml2Client"); + + final CallbackFilter callbackFilter = new CallbackFilter(this.config); + // http.regexMatcher("/callback").addFilterBefore(callbackFilter, BasicAuthenticationFilter.class); + http.antMatcher("/**").addFilterBefore(callbackFilter, BasicAuthenticationFilter.class); + http.authorizeRequests().anyRequest().fullyAuthenticated(); + + http.addFilterBefore(securityFilter, BasicAuthenticationFilter.class); + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS); + + // http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); + + http.csrf().disable(); + http.headers().frameOptions().disable(); + } + + @Override + public void configure(org.springframework.security.config.annotation.web.builders.WebSecurity web) throws Exception { + super.configure(web); + + StrictHttpFirewall firewall = new StrictHttpFirewall(); + firewall.setAllowUrlEncodedSlash(true); + web.httpFirewall(firewall); + } + } +} diff --git a/pac4j-module/src/main/resources/META-INF/services/org.pac4j.saml.util.ConfigurationManager b/pac4j-module/src/main/resources/META-INF/services/org.pac4j.saml.util.ConfigurationManager new file mode 100644 index 000000000..5289f3cb2 --- /dev/null +++ b/pac4j-module/src/main/resources/META-INF/services/org.pac4j.saml.util.ConfigurationManager @@ -0,0 +1 @@ +net.unicon.shibui.pac4j.Pac4jSAMLConfigurationManager \ No newline at end of file diff --git a/pac4j-module/src/main/resources/META-INF/spring.factories b/pac4j-module/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..90f792d84 --- /dev/null +++ b/pac4j-module/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + net.unicon.shibui.pac4j.Pac4jConfiguration,\ + net.unicon.shibui.pac4j.WebSecurity,\ + net.unicon.shibui.pac4j.Pac4jConfigurationProperties \ No newline at end of file diff --git a/pac4j-module/src/test/docker/conf/application.yml b/pac4j-module/src/test/docker/conf/application.yml new file mode 100644 index 000000000..b850afb54 --- /dev/null +++ b/pac4j-module/src/test/docker/conf/application.yml @@ -0,0 +1,22 @@ +server: + port: 8443 + ssl: + key-store: "/conf/keystore.p12" + key-store-password: "changeit" + keyStoreType: "PKCS12" + keyAlias: "tomcat" +shibui: + pac4j: + keystorePath: "/conf/samlKeystore.jks" + keystorePassword: "changeit" + privateKeyPassword: "changeit" + serviceProviderEntityId: "https://unicon.net/shibui" + serviceProviderMetadataPath: "/conf/sp-metadata.xml" + identityProviderMetadataPath: "/conf/idp-metadata.xml" + forceServiceProviderMetadataGeneration: true + callbackUrl: "https://localhost:8443/callback" + maximumAuthenticationLifetime: 3600000 +logging: + level: + org.pac4j: "TRACE" + org.opensaml: "INFO" \ No newline at end of file diff --git a/pac4j-module/src/test/docker/conf/idp-metadata.xml b/pac4j-module/src/test/docker/conf/idp-metadata.xml new file mode 100644 index 000000000..eddc5010c --- /dev/null +++ b/pac4j-module/src/test/docker/conf/idp-metadata.xml @@ -0,0 +1,30 @@ + + + + + + + MIIDdDCCAlygAwIBAgIGAVWm+BpSMA0GCSqGSIb3DQEBCwUAMHsxFDASBgNVBAoTC0dvb2dsZSBJ +bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MQ8wDQYDVQQDEwZHb29nbGUxGDAWBgNVBAsTD0dv +b2dsZSBGb3IgV29yazELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEwHhcNMTYwNzAx +MTQ1ODQ0WhcNMjEwNjMwMTQ1ODQ0WjB7MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEWMBQGA1UEBxMN +TW91bnRhaW4gVmlldzEPMA0GA1UEAxMGR29vZ2xlMRgwFgYDVQQLEw9Hb29nbGUgRm9yIFdvcmsx +CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAirwyeCS6SZpnYxprfXhpTNXwVfQC+J9OvBlJp8/7ngA627yER1bvfUkBMQxo0CXe +H6HX6Vw1DgalZJeEGDZSErlAY7lWkXkHdsejlMoYayQSZz2b/EfeRetwxh3Ek0hMDScOgDlsdfAn +AiZ4//n3IlypCi4ZMnLPs308FYunvp+R0Wd8Yqj8ctKhiYs6fCSHksDd+JKPe2FC1Zqw9GCGhi32 +DBNRTHfE3tX3rTRs1pT0qbrQmpPfeBYfX00astGa3Dq/XWVO62IlqM7nVjglIPdi0tCIx+5RVZrY +uvULMipA+131TMxTpcGjUFxNwzPdogdpNhtL8+erfhG26C6b8wIDAQABMA0GCSqGSIb3DQEBCwUA +A4IBAQCIOe/bW+mdE9PuarSz60HPGe9ROibyEOTyAWGxvSFfqoNFzaH3oOiEHMNG+ZkHHGtGEeWc +KYQ72V1OKO4aNqy2XaT3onOkd2oh4N8Q5pWrgMRkAB2HvBhBcQeO6yojVamTd43Kbtc+Hly3o+Or +XXOR9cgfxX/0Dbb+xwzTcwcMoJ1CPd3T4zxByKMHNflWrgrmZ9DmDOya4Aqs+xvrvPJB2VHaXoJ6 +r/N+xtG8zO8wNRuxQxNUvtcFKKX2sZAqQRASGi1z8Y1FhU6rWBdBRtaiASAIgkNwOmS603Mm08Yr +0Yq7x6h3XlG8HO0bAOto6pr6q85pLqqv7v7/x7mfdjV3 + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + + + + diff --git a/pac4j-module/src/test/docker/conf/keystore.p12 b/pac4j-module/src/test/docker/conf/keystore.p12 new file mode 100644 index 000000000..57f9c162a Binary files /dev/null and b/pac4j-module/src/test/docker/conf/keystore.p12 differ diff --git a/pac4j-module/src/test/docker/docker-compose.yml b/pac4j-module/src/test/docker/docker-compose.yml new file mode 100644 index 000000000..e6b2f6e70 --- /dev/null +++ b/pac4j-module/src/test/docker/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3.7" + +services: + shibui: + image: unicon/shibui-pac4j + entrypoint: ["/usr/bin/java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005", "-jar", "app.jar"] + ports: + - 8080:8080 + - 5005:5005 + - 8443:8443 + volumes: + - ./conf:/conf + - ./conf/application.yml:/application.yml + networks: + - front +networks: + front: + driver: bridge \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index adb85a37b..8fae26617 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include 'backend', 'ui' \ No newline at end of file +include 'backend', 'ui', 'pac4j-module' \ No newline at end of file