Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature migrate web server to sechub server #3648 #3775

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions gradle/projects.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,10 @@ projectType = [
project(':sechub-pds'),
project(':sechub-wrapper-checkmarx'),
project(':sechub-wrapper-prepare'),
project(':sechub-web-server'),
project(':sechub-wrapper-secretvalidation'),
],

springBootWebApplicationProjects:[
project('sechub-web-server')
],

/* documentation projects */
Expand Down
3 changes: 3 additions & 0 deletions sechub-commons-security-spring/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ dependencies {

implementation project(':sechub-commons-core')
implementation project(':sechub-testframework-spring')
implementation library.springboot_starter_web
implementation library.springboot_starter_security
implementation library.springboot_starter_oauth2_client
implementation library.springboot_starter_oauth2_resource_server
implementation library.jakarta_servlet_api

testImplementation library.springframework_web
testImplementation library.springframework_webmvc
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
package com.mercedesbenz.sechub.webserver.encryption;
package com.mercedesbenz.sechub.spring.security;

import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
Expand All @@ -10,13 +10,13 @@
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

import com.mercedesbenz.sechub.commons.core.security.CryptoAccess;

@Component
@EnableConfigurationProperties(AES256EncryptionProperties.class)
@ConditionalOnProperty(name = "sechub.security.encryption.secret-key")
public class AES256Encryption {

private static final String TRANSFORMATION = "AES";
Expand All @@ -26,8 +26,8 @@ public class AES256Encryption {
private final Cipher decrypt;
private final SealedObject sealedSecretKey;

AES256Encryption(AES256EncryptionProperties properties) throws GeneralSecurityException {
SecretKey secretKey = new SecretKeySpec(properties.getSecretKeyBytes(), TRANSFORMATION);
AES256Encryption(SecurityProperties securityProperties) throws GeneralSecurityException {
SecretKey secretKey = new SecretKeySpec(securityProperties.getEncryption().getSecretKey().getBytes(StandardCharsets.UTF_8), TRANSFORMATION);
this.sealedSecretKey = secretKeyCryptoAccess.seal(secretKey);

this.encrypt = Cipher.getInstance(TRANSFORMATION);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
package com.mercedesbenz.sechub.webserver.encryption;
package com.mercedesbenz.sechub.spring.security;

public class AES256EncryptionException extends RuntimeException {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
// SPDX-License-Identifier: MIT
package com.mercedesbenz.sechub.spring.security;

import java.util.HashSet;
import java.util.Set;

import org.springframework.beans.BeanInstantiationException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.client.RestTemplate;

/**
Expand All @@ -34,20 +49,6 @@
* authorization.
* </p>
*
* <p>
* To enable OAuth2 authentication in JWT mode, you must set the following
* property {@code sechub.security.oauth2.jwt.enabled=true} in the application
* properties. For opaque token mode, set
* {@code sechub.security.oauth2.opaque-token.enabled=true}. <b>Note:</b> This
* configuration requires <b>exactly one</b> of the two modes to be enabled.
* </p>
*
* <p>
* Subclasses must implement the {@link #isOAuth2Enabled()} method to indicate
* whether OAuth2 is enabled and the {@link #authorizeHttpRequests()} method to
* configure API access permissions.
* </p>
*
* @see org.springframework.security.config.annotation.web.builders.HttpSecurity
* @see org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer
* @see org.springframework.security.oauth2.jwt.JwtDecoder
Expand All @@ -57,76 +58,201 @@
*
* @author hamidonos
*/
@EnableConfigurationProperties(SecurityProperties.class)
public abstract class AbstractSecurityConfiguration {

static final String OAUTH2_PROPERTIES_PREFIX = "sechub.security.oauth2";;
static final String OAUTH2_PROPERTIES_MODE = "mode";
static final String ACCESS_TOKEN = "access_token";
static final String SERVER_OAUTH2_PROPERTIES_PREFIX = "sechub.security.server.oauth2";;
static final String MODE = "mode";

private static final String SCOPE = "openid";
private static final String SUBJECT = "sub";
/* @formatter:off */
private static final Set<String> DEFAULT_PUBLIC_PATHS = Set.of(
"/css/**",
"/js/**",
"/images/**",
"/login/oauth2/**",
"/oauth2/**",
"/favicon.ico",
"/sechub-logo.svg"
);
/* @formatter:on */

/* @formatter:off */
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity,
@Autowired(required = false) UserDetailsService userDetailsService,
RestTemplate restTemplate,
@Autowired(required = false) OAuth2JwtProperties oAuth2JwtProperties,
@Autowired(required = false) OAuth2OpaqueTokenProperties oAuth2OpaqueTokenProperties,
@Autowired(required = false) JwtDecoder jwtDecoder) throws Exception {

httpSecurity.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
@Order(1)
SecurityFilterChain securityFilterChainResourceServer(HttpSecurity httpSecurity,
SecurityProperties securityProperties,
@Autowired(required = false) UserDetailsService userDetailsService,
RestTemplate restTemplate,
@Autowired(required = false) AES256Encryption aes256Encryption,
@Autowired(required = false) JwtDecoder jwtDecoder) throws Exception {
SecurityProperties.Login login = securityProperties.getLogin();

if (login != null && login.isEnabled()) {
Set<String> publicPaths = new HashSet<>(DEFAULT_PUBLIC_PATHS);
publicPaths.add(login.getLoginPage());

RequestMatcher publicPathsMatcher = new OrRequestMatcher(
publicPaths.stream()
.map(AntPathRequestMatcher::new)
.toArray(AntPathRequestMatcher[]::new));

RequestMatcher protectedPathsMatcher = new NegatedRequestMatcher(publicPathsMatcher);

httpSecurity.securityMatcher(protectedPathsMatcher);
}

httpSecurity
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorizeHttpRequests())
.csrf(AbstractHttpConfigurer::disable) // CSRF protection disabled. The CookieServerCsrfTokenRepository does
// not work since Spring Boot 3
.httpBasic(Customizer.withDefaults()).headers((headers) -> headers
.contentSecurityPolicy((csp) -> csp.policyDirectives("default-src 'none'; style-src 'unsafe-inline'")));

if (isOAuth2Enabled()) {
SecurityProperties.Server server = securityProperties.getServer();

if (server != null && server.isOAuth2ModeEnabled()) {
if (userDetailsService == null) {
throw new NoSuchBeanDefinitionException(UserDetailsService.class);
}

if ((oAuth2JwtProperties == null && oAuth2OpaqueTokenProperties == null) || (oAuth2JwtProperties != null && oAuth2OpaqueTokenProperties != null)) {
String exMsg = "Either JWT or opaque token mode must be enabled by setting the '%s.%s' property to either '%s' or '%s'".formatted(
OAUTH2_PROPERTIES_PREFIX,
OAUTH2_PROPERTIES_MODE,
OAuth2JwtPropertiesConfiguration.MODE,
OAuth2OpaqueTokenPropertiesConfiguration.MODE
SecurityProperties.Server.OAuth2 oAuth2 = server.getOAuth2();

if (oAuth2.isJwtModeEnabled() == oAuth2.isOpaqueTokenModeEnabled()) {
String exMsg = "Either 'jwt' or opaque token mode must be enabled by setting the '%s.%s' property to either '%s' or '%s'".formatted(
SERVER_OAUTH2_PROPERTIES_PREFIX,
MODE,
SecurityProperties.Server.OAuth2.OAUTH2_JWT_MODE,
SecurityProperties.Server.OAuth2.OAUTH2_OPAQUE_TOKEN_MODE
);

throw new BeanInstantiationException(SecurityFilterChain.class, exMsg);
}

if (oAuth2JwtProperties != null) {
if (aes256Encryption == null) {
throw new NoSuchBeanDefinitionException(AES256Encryption.class);
}

BearerTokenResolver bearerTokenResolver = new DynamicBearerTokenResolver(aes256Encryption);

if (oAuth2.isJwtModeEnabled()) {
if (jwtDecoder == null) {
throw new NoSuchBeanDefinitionException(JwtDecoder.class);
}
BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
AuthenticationProvider authenticationProvider = new OAuth2JwtAuthenticationProvider(userDetailsService, jwtDecoder);

httpSecurity
.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer
.jwt(jwt -> jwt.decoder(jwtDecoder))
.bearerTokenResolver(bearerTokenResolver))
.jwt(jwt -> jwt.decoder(jwtDecoder))
.bearerTokenResolver(bearerTokenResolver)
)
.authenticationProvider(authenticationProvider);
}

if (oAuth2OpaqueTokenProperties != null) {
OpaqueTokenIntrospector opaqueTokenIntrospector = new Base64OAuth2OpaqueTokenIntrospector(
if (oAuth2.isOpaqueTokenModeEnabled()) {
SecurityProperties.Server.OAuth2.OpaqueToken opaqueToken = oAuth2.getOpaqueToken();
OpaqueTokenIntrospector opaqueTokenIntrospector = new OAuth2OpaqueTokenIntrospector(
restTemplate,
oAuth2OpaqueTokenProperties.getIntrospectionUri(),
oAuth2OpaqueTokenProperties.getClientId(),
oAuth2OpaqueTokenProperties.getClientSecret(),
opaqueToken.getIntrospectionUri(),
opaqueToken.getClientId(),
opaqueToken.getClientSecret(),
userDetailsService);

httpSecurity
.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer
.opaqueToken(opaqueToken -> opaqueToken.introspector(opaqueTokenIntrospector)));
.opaqueToken(opaqueTokenConfigurer -> opaqueTokenConfigurer.introspector(opaqueTokenIntrospector))
.bearerTokenResolver(bearerTokenResolver)
);
}
}
/* @formatter:on */

return httpSecurity.build();
}

protected abstract boolean isOAuth2Enabled();
@Bean
@Conditional(LoginEnabledCondition.class)
ClientRegistrationRepository clientRegistrationRepository(SecurityProperties securityProperties) {
SecurityProperties.Login login = securityProperties.getLogin();
SecurityProperties.Login.OAuth2 oAuth2 = login.getOAuth2();

/* @formatter:off */
ClientRegistration clientRegistration = ClientRegistration
.withRegistrationId(oAuth2.getProvider())
.clientId(oAuth2.getClientId())
.clientSecret(oAuth2.getClientSecret())
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri(oAuth2.getRedirectUri())
.issuerUri(oAuth2.getIssuerUri())
.scope(SCOPE)
.authorizationUri(oAuth2.getAuthorizationUri())
.tokenUri(oAuth2.getTokenUri())
.userInfoUri(oAuth2.getUserInfoUri())
.userNameAttributeName(SUBJECT)
.jwkSetUri(oAuth2.getJwkSetUri())
.build();
/* @formatter:on */

return new InMemoryClientRegistrationRepository(clientRegistration);
}

@Bean
@Conditional(LoginEnabledCondition.class)
@Order(2)
/* @formatter:off */
SecurityFilterChain securityFilterChainLogin(HttpSecurity httpSecurity,
@Autowired(required = false) SecurityProperties securityProperties,
@Autowired(required = false) AES256Encryption aes256Encryption,
@Autowired(required = false) OAuth2AuthorizedClientService oAuth2AuthorizedClientService) throws Exception {
SecurityProperties.Login login = securityProperties.getLogin();

Set<String> publicPaths = new HashSet<>(DEFAULT_PUBLIC_PATHS);
publicPaths.add(login.getLoginPage());

httpSecurity.securityMatcher(publicPaths.toArray(new String[0]))
/* Disable CSRF */
.csrf(AbstractHttpConfigurer::disable)
/* Make the application stateless */
.sessionManagement(httpSecuritySessionManagementConfigurer -> httpSecuritySessionManagementConfigurer
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

if (login.isOAuth2ModeEnabled()) {
SecurityProperties.Login.OAuth2 loginOAuth2 = login.getOAuth2();
RestTemplate restTemplate = new RestTemplate();
LoginOAuth2AccessTokenClient loginOAuth2AccessTokenClient = new LoginOAuth2AccessTokenClient(restTemplate);
if (oAuth2AuthorizedClientService == null) {
throw new NoSuchBeanDefinitionException(OAuth2AuthorizedClientService.class);
}
if (aes256Encryption == null) {
throw new NoSuchBeanDefinitionException(AES256Encryption.class);
}
AuthenticationSuccessHandler authenticationSuccessHandler = new LoginOAuth2SuccessHandler(loginOAuth2.getProvider(), oAuth2AuthorizedClientService,
aes256Encryption, login.getRedirectUri());
/* Enable OAuth2 */
httpSecurity.oauth2Login(oauth2 -> oauth2.loginPage(login.getLoginPage())
.tokenEndpoint(token -> token.accessTokenResponseClient(loginOAuth2AccessTokenClient))
.successHandler(authenticationSuccessHandler));
}

if (login.isClassicModeEnabled()) {
/*
* Enable Classic Authentication
*
* Note: This must be the last configuration in
* order to set the default 'loginPage' to oAuth2 because spring uses the
* 'loginPage' from the first authentication method configured
*/
AuthenticationSuccessHandler authenticationSuccessHandler = new LoginClassicSuccessHandler(login.getRedirectUri());
httpSecurity.formLogin(form -> form.loginPage(login.getLoginPage()).successHandler(authenticationSuccessHandler));
}

/* @formatter:on */

return httpSecurity.build();
}

protected abstract Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeHttpRequests();

Expand Down
Loading
Loading