Skip to content

Commit

Permalink
migrate web server to sechub server #3648
Browse files Browse the repository at this point in the history
  • Loading branch information
hamidonos committed Nov 29, 2024
1 parent afa4f7d commit 0771c7b
Show file tree
Hide file tree
Showing 71 changed files with 848 additions and 1,131 deletions.
2 changes: 2 additions & 0 deletions gradle/projects.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ projectType = [
project(':sechub-developertools'),
project(':sechub-test'),
project(':sechub-commons-security-spring'),
project(':sechub-commons-security-login-spring'),
project(':sechub-testframework-spring'),
project(':sechub-storage-sharedvolume-spring'),

Expand Down Expand Up @@ -85,6 +86,7 @@ projectType = [
project(':sechub-notification'),
project(':sechub-sereco'),
project(':sechub-shared-kernel'),
project(':sechub-web'),

project(':sechub-scan'),
project(':sechub-scan-product-pds'),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// SPDX-License-Identifier: MIT
package com.mercedesbenz.sechub.webserver.security;
package com.mercedesbenz.sechub.commons.security.login.spring;

import java.util.Arrays;

import com.mercedesbenz.sechub.spring.security.AES256Encryption;
import com.mercedesbenz.sechub.spring.security.LoginOAuth2Properties;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -41,8 +43,10 @@
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class WebServerSecurityConfiguration {
class LoginSecurityConfiguration {

static final String ACCESS_TOKEN = "access_token";
static final String LOGIN_PROPERTIES_PREFIX = "sechub.security.login";

private static final String ACTUATOR_PATH = "/actuator/**";
/* @formatter:off */
Expand All @@ -60,22 +64,22 @@ class WebServerSecurityConfiguration {
private static final String USER_NAME_ATTRIBUTE_NAME = "sub";

private final Environment environment;
private final OAuth2Properties oAuth2Properties;
private final LoginOAuth2Properties loginOAuth2Properties;
private final AES256Encryption aes256Encryption;

/* @formatter:off */
WebServerSecurityConfiguration(@Autowired Environment environment,
@Autowired(required = false) OAuth2Properties oAuth2Properties,
@Autowired AES256Encryption aes256Encryption) {
LoginSecurityConfiguration(@Autowired Environment environment,
@Autowired(required = false) LoginOAuth2Properties loginOAuth2Properties,
@Autowired AES256Encryption aes256Encryption) {
/* @formatter:on */
this.environment = environment;
if (isOAuth2Enabled() && oAuth2Properties == null) {
throw new NoSuchBeanDefinitionException(OAuth2Properties.class);
if (isOAuth2Enabled() && loginOAuth2Properties == null) {
throw new NoSuchBeanDefinitionException(LoginOAuth2Properties.class);
}
if (!isOAuth2Enabled() && !isClassicAuthEnabled()) {
throw new IllegalStateException("At least one authentication method must be enabled");
}
this.oAuth2Properties = oAuth2Properties;
this.loginOAuth2Properties = loginOAuth2Properties;
this.aes256Encryption = aes256Encryption;
}

Expand All @@ -84,16 +88,15 @@ class WebServerSecurityConfiguration {
ClientRegistrationRepository clientRegistrationRepository() {
/* @formatter:off */
ClientRegistration clientRegistration = ClientRegistration
.withRegistrationId(oAuth2Properties.getProvider())
.clientId(oAuth2Properties.getClientId())
.clientSecret(oAuth2Properties.getClientSecret())
.withRegistrationId(loginOAuth2Properties.getProvider())
.clientId(loginOAuth2Properties.getClientId())
.clientSecret(loginOAuth2Properties.getClientSecret())
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri(oAuth2Properties.getRedirectUri())
.issuerUri(oAuth2Properties.getIssuerUri()).scope(SCOPE)
.authorizationUri(oAuth2Properties.getAuthorizationUri())
.tokenUri(oAuth2Properties.getTokenUri())
.userInfoUri(oAuth2Properties.getUserInfoUri())
.jwkSetUri(oAuth2Properties.getJwkSetUri())
.redirectUri(loginOAuth2Properties.getRedirectUri())
.issuerUri(loginOAuth2Properties.getIssuerUri()).scope(SCOPE)
.authorizationUri(loginOAuth2Properties.getAuthorizationUri())
.tokenUri(loginOAuth2Properties.getTokenUri())
.userInfoUri(loginOAuth2Properties.getUserInfoUri())
.userNameAttributeName(USER_NAME_ATTRIBUTE_NAME)
.build();
/* @formatter:on */
Expand Down Expand Up @@ -140,7 +143,7 @@ SecurityFilterChain securityFilterChainProtectedPaths(HttpSecurity httpSecurity,
.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer
.authenticationEntryPoint(authenticationEntryPoint)
.bearerTokenResolver(bearerTokenResolver)
.jwt(jwt -> jwt.jwkSetUri(oAuth2Properties.getJwkSetUri()))
.jwt(jwt -> jwt.jwkSetUri(loginOAuth2Properties.getJwkSetUri()))
);
/* @formatter:on */

Expand Down Expand Up @@ -180,7 +183,7 @@ SecurityFilterChain securityFilterChainPublicPaths(HttpSecurity httpSecurity,
throw new NoSuchBeanDefinitionException(
"No qualifying bean of type 'OAuth2AuthorizedClientService' available: expected at least 1 bean which qualifies as autowire candidate.");
}
AuthenticationSuccessHandler authenticationSuccessHandler = new OAuth2LoginSuccessHandler(oAuth2Properties, oAuth2AuthorizedClientService,
AuthenticationSuccessHandler authenticationSuccessHandler = new OAuth2LoginSuccessHandler(loginOAuth2Properties, oAuth2AuthorizedClientService,
aes256Encryption);
/* Enable OAuth2 */
httpSecurity.oauth2Login(oauth2 -> oauth2.loginPage(RequestConstants.LOGIN)
Expand Down
2 changes: 2 additions & 0 deletions sechub-commons-security-spring/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ dependencies {
implementation project(':sechub-commons-core')
implementation project(':sechub-testframework-spring')
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 @@ -15,6 +15,7 @@

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

// TODO: move to a central place
@Component
@EnableConfigurationProperties(AES256EncryptionProperties.class)
public class AES256Encryption {
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,5 +1,5 @@
// SPDX-License-Identifier: MIT
package com.mercedesbenz.sechub.webserver.encryption;
package com.mercedesbenz.sechub.spring.security;

import static java.util.Objects.requireNonNull;

Expand All @@ -23,15 +23,10 @@ class AES256EncryptionProperties {

@ConstructorBinding
AES256EncryptionProperties(String secretKey) {
try {
requireNonNull(secretKey, ERR_MSG_FORMAT.formatted(PREFIX, "secret-key"));
this.secretKeySealed = CryptoAccess.CRYPTO_STRING.seal(secretKey);
if (!is256BitString(secretKey)) {
throw new IllegalArgumentException("The property %s.%s must be a 256-bit string".formatted(PREFIX, "secret-key"));
}
} finally {
/* Ensure that the secret key is cleared */
secretKey = null;
requireNonNull(secretKey, ERR_MSG_FORMAT.formatted(PREFIX, "secret-key"));
this.secretKeySealed = CryptoAccess.CRYPTO_STRING.seal(secretKey);
if (!is256BitString(secretKey)) {
throw new IllegalArgumentException("The property %s.%s must be a 256-bit string".formatted(PREFIX, "secret-key"));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,17 @@
*/
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 LOGIN_PROPERTIES_PREFIX = "sechub.security.login";
static final String SERVER_OAUTH2_PROPERTIES_PREFIX = "sechub.security.server.oauth2";;
static final String MODE = "mode";

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

Expand All @@ -83,18 +85,18 @@ SecurityFilterChain filterChain(HttpSecurity httpSecurity,
throw new NoSuchBeanDefinitionException(UserDetailsService.class);
}

if ((oAuth2JwtProperties == null && oAuth2OpaqueTokenProperties == null) || (oAuth2JwtProperties != null && oAuth2OpaqueTokenProperties != null)) {
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,
SERVER_OAUTH2_PROPERTIES_PREFIX,
MODE,
OAuth2JwtPropertiesConfiguration.MODE,
OAuth2OpaqueTokenPropertiesConfiguration.MODE
);

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

if (oAuth2JwtProperties != null) {
if (OAuth2JwtProperties != null) {
if (jwtDecoder == null) {
throw new NoSuchBeanDefinitionException(JwtDecoder.class);
}
Expand All @@ -109,7 +111,7 @@ SecurityFilterChain filterChain(HttpSecurity httpSecurity,
}

if (oAuth2OpaqueTokenProperties != null) {
OpaqueTokenIntrospector opaqueTokenIntrospector = new Base64OAuth2OpaqueTokenIntrospector(
OpaqueTokenIntrospector opaqueTokenIntrospector = new OAuth2OpaqueTokenIntrospector(
restTemplate,
oAuth2OpaqueTokenProperties.getIntrospectionUri(),
oAuth2OpaqueTokenProperties.getClientId(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: MIT
package com.mercedesbenz.sechub.spring.security;

import java.util.Arrays;
import java.util.Base64;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;

/**
* {@code CookieTokenResolver} implements {@link BearerTokenResolver} to provide
* custom Bearer Token resolution. The encrypted access token is read from the
* cookies and decrypted using {@link AES256Encryption}. Note that the access
* token is expected in {@link Base64} encoded format.
*
* @see BearerTokenResolver
* @see AES256Encryption
*
* @author hamidonos
*/
class CookieAccessTokenResolver implements BearerTokenResolver {

private static final Logger LOG = LoggerFactory.getLogger(CookieAccessTokenResolver.class);
private static final String MISSING_ACCESS_TOKEN_VALUE = "missing-access-token";
private static final Base64.Decoder DECODER = Base64.getDecoder();

private final AES256Encryption aes256Encryption;

CookieAccessTokenResolver(AES256Encryption aes256Encryption) {
this.aes256Encryption = aes256Encryption;
}

@Override
public String resolve(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();

if (cookies == null) {
LOG.debug("No cookies found in the request");

/*
* If the access token cookie is not found, we return a constant string to
* indicate that the access token is missing. We do this because we want to pass
* exception handling further down the chain. Spring does not provide a way to
* wrap exceptions around custom BearerTokenResolver classes effectively. This
* is a good practice because it allows us to handle the missing access token in
* a more controlled manner.
*/
return MISSING_ACCESS_TOKEN_VALUE;
}

/* @formatter:off */
String accessToken = Arrays
.stream(cookies)
.filter(cookie -> AbstractSecurityConfiguration.ACCESS_TOKEN.equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
/* @formatter:on */

if (accessToken == null) {
LOG.debug("Request is missing the 'access_token' cookie");
/* same here */
return MISSING_ACCESS_TOKEN_VALUE;
}

try {
byte[] jwtBytes = DECODER.decode(accessToken);
return aes256Encryption.decrypt(jwtBytes);
} catch (Exception e) {
LOG.debug("Failed to decrypt the access token cookie", e);
/* same here */
return MISSING_ACCESS_TOKEN_VALUE;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT
package com.mercedesbenz.sechub.spring.security;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = LoginClassicProperties.PREFIX)
public class LoginClassicProperties {

static final String PREFIX = "sechub.security.login.classic";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
package com.mercedesbenz.sechub.spring.security;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(LoginClassicProperties.class)
@ConditionalOnProperty(prefix = AbstractSecurityConfiguration.LOGIN_PROPERTIES_PREFIX, name = "enabled", havingValue = "true")
class LoginClassicPropertiesConfiguration {
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
package com.mercedesbenz.sechub.webserver.security;
package com.mercedesbenz.sechub.spring.security;

import java.io.IOException;

Expand All @@ -8,29 +8,32 @@
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import com.mercedesbenz.sechub.webserver.RequestConstants;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
* {@code ClassicLoginSuccessHandler} implements
* {@link AuthenticationSuccessHandler} to provide custom behavior upon
* successful authentication. This handler redirects the user to the /home page
* specified in {@link RequestConstants}.
* successful authentication. This handler redirects the user to the specified
* <code>redirectUri</code>.
*
* @see WebServerSecurityConfiguration
* @see RequestConstants
* @see LoginSecurityConfiguration
*
* @author hamidonos
*/
class ClassicLoginSuccessHandler implements AuthenticationSuccessHandler {
class LoginClassicSuccessHandler implements AuthenticationSuccessHandler {

private static final Logger LOG = LoggerFactory.getLogger(LoginClassicSuccessHandler.class);

private static final Logger LOG = LoggerFactory.getLogger(ClassicLoginSuccessHandler.class);
private final String redirectUri;

LoginClassicSuccessHandler(String redirectUri) {
this.redirectUri = redirectUri;
}

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
LOG.debug("Redirecting to {}", RequestConstants.HOME);
response.sendRedirect(RequestConstants.HOME);
LOG.debug("Redirecting to {}", redirectUri);
response.sendRedirect(redirectUri);
}
}
Loading

0 comments on commit 0771c7b

Please sign in to comment.