diff --git a/backend/src/main/java/ch/puzzle/okr/SecurityConfig.java b/backend/src/main/java/ch/puzzle/okr/SecurityConfig.java index c0217f809d..051f3309f0 100644 --- a/backend/src/main/java/ch/puzzle/okr/SecurityConfig.java +++ b/backend/src/main/java/ch/puzzle/okr/SecurityConfig.java @@ -7,6 +7,7 @@ import com.nimbusds.jwt.proc.JWTProcessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -28,7 +29,13 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; -import org.springframework.security.web.header.writers.*; +import org.springframework.security.web.header.writers.CrossOriginOpenerPolicyHeaderWriter; +import org.springframework.security.web.header.writers.CrossOriginResourcePolicyHeaderWriter; +import org.springframework.security.web.header.writers.StaticHeadersWriter; + +import static org.springframework.security.web.header.writers.CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP; +import static org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER; +import static org.springframework.security.web.header.writers.XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK; @Configuration @EnableWebSecurity @@ -36,9 +43,17 @@ public class SecurityConfig { private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class); + private static final CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy OPENER_SAME_ORIGIN = CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy.SAME_ORIGIN; + private static final CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy RESOURCE_SAME_ORIGIN = CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy.SAME_ORIGIN; + + private String connectSrc; + @Bean @Order(1) // Must be First order! Otherwise unauthorized Requests are sent to Controllers - public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http, @Value("${connect.src}") String connectSrc) + throws Exception { + + this.connectSrc = connectSrc; setHeaders(http); http.addFilterAfter(new ForwardFilter(), BasicAuthenticationFilter.class); logger.debug("*** apiSecurityFilterChain reached"); @@ -72,32 +87,41 @@ JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenVal } private HttpSecurity setHeaders(HttpSecurity http) throws Exception { - http.headers(h -> h - .contentSecurityPolicy(e -> e.policyDirectives("default-src 'self';" - + "script-src 'self' 'unsafe-inline';" + " style-src 'self' 'unsafe-inline';" - + " object-src 'none';" + " base-uri 'self';" - + " connect-src 'self' https://sso.puzzle.ch http://localhost:8544;" - + " font-src 'self';" + " frame-src 'self';" + " img-src 'self' data: ;" - + " manifest-src 'self';" + " media-src 'self';" + " worker-src 'none';")) - .crossOriginEmbedderPolicy(coepCustomizer -> coepCustomizer - .policy(CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP)) - .crossOriginOpenerPolicy(coopCustomizer -> coopCustomizer - .policy(CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy.SAME_ORIGIN)) - .crossOriginResourcePolicy(corpCustomizer -> corpCustomizer - .policy(CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy.SAME_ORIGIN)) - .addHeaderWriter(new StaticHeadersWriter("X-Permitted-Cross-Domain-Policies", "none"))); - return http.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::deny) - .xssProtection(e -> e.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)) - .httpStrictTransportSecurity(e -> e.includeSubDomains(true).maxAgeInSeconds(31536000)) - .referrerPolicy(referrer -> referrer.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER)) - .permissionsPolicy( - permissions -> permissions.policy("accelerometer=(), ambient-light-sensor=(), autoplay=(), " - + "battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), " - + "execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=()," - + " geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), " - + "midi=(), navigation-override=(), payment=(), picture-in-picture=()," - + " publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(self), " - + "usb=(), web-share=(), xr-spatial-tracking=()"))); + return http.headers(headers -> headers + .contentSecurityPolicy(c -> c.policyDirectives(okrContentSecurityPolicy())) + .crossOriginEmbedderPolicy(c -> c.policy(REQUIRE_CORP)) + .crossOriginOpenerPolicy(c -> c.policy(OPENER_SAME_ORIGIN)) + .crossOriginResourcePolicy(c -> c.policy(RESOURCE_SAME_ORIGIN)) + .addHeaderWriter(new StaticHeadersWriter("X-Permitted-Cross-Domain-Policies", "none")) + .frameOptions(HeadersConfigurer.FrameOptionsConfig::deny) + .xssProtection(c -> c.headerValue(ENABLED_MODE_BLOCK)) + .httpStrictTransportSecurity(c -> c.includeSubDomains(true).maxAgeInSeconds(31536000)) + .referrerPolicy(c -> c.policy(NO_REFERRER)).permissionsPolicy(c -> c.policy(okrPermissionPolicy()))); + } + + private String okrContentSecurityPolicy() { + return "default-src 'self';" // + + "script-src 'self' 'unsafe-inline';" // + + " style-src 'self' 'unsafe-inline';" // + + " object-src 'none';" // + + " base-uri 'self';" // + + " connect-src 'self' " + connectSrc + ";" // + + " font-src 'self';" // + + " frame-src 'self';" // + + " img-src 'self' data: ;" // + + " manifest-src 'self';" // + + " media-src 'self';" // + + " worker-src 'none';"; // + } + + private String okrPermissionPolicy() { + return "accelerometer=(), ambient-light-sensor=(), autoplay=(), " + + "battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), " + + "execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=()," + + " geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), " + + "midi=(), navigation-override=(), payment=(), picture-in-picture=()," + + " publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(self), " + + "usb=(), web-share=(), xr-spatial-tracking=()"; } @Bean diff --git a/backend/src/main/resources/application-dev.properties b/backend/src/main/resources/application-dev.properties index 7aa5da0118..9e018704ff 100644 --- a/backend/src/main/resources/application-dev.properties +++ b/backend/src/main/resources/application-dev.properties @@ -13,6 +13,9 @@ spring.flyway.locations=classpath:db/migration,classpath:db/data-migration,class okr.tenant-ids=pitc,acme okr.datasource.driver-class-name=org.postgresql.Driver +# security +connect.src=http://localhost:8544 http://localhost:8545 + # hibernate hibernate.connection.url=jdbc:postgresql://localhost:5432/okr hibernate.connection.username=user diff --git a/backend/src/main/resources/application-integration-test.properties b/backend/src/main/resources/application-integration-test.properties index c19428abf1..4043d5d936 100644 --- a/backend/src/main/resources/application-integration-test.properties +++ b/backend/src/main/resources/application-integration-test.properties @@ -16,6 +16,9 @@ spring.flyway.locations=classpath:db/h2-db/database-h2-schema,classpath:db/h2-db okr.tenant-ids=pitc,acme okr.datasource.driver-class-name=org.h2.Driver +# security +connect.src=http://localhost:8544 http://localhost:8545 + # hibernate hibernate.connection.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1; hibernate.connection.username=user diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 997f4fde7f..af8423d49f 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -54,3 +54,6 @@ okr.clientcustomization.title=Puzzle OKR okr.quarter.business.year.start=7 okr.quarter.label.format=GJ xx/yy-Qzz + +# security +connect.src=https://sso.puzzle.ch \ No newline at end of file