diff --git a/gradle/libraries.gradle b/gradle/libraries.gradle
index 37d5570466..abd276388a 100644
--- a/gradle/libraries.gradle
+++ b/gradle/libraries.gradle
@@ -123,7 +123,7 @@ ext {
springboot_starter_mail: "org.springframework.boot:spring-boot-starter-mail",
springboot_starter_validation: "org.springframework.boot:spring-boot-starter-validation",
springboot_starter_webflux: "org.springframework.boot:spring-boot-starter-webflux",
-
+ springboot_starter_oauth2_client: "org.springframework.boot:spring-boot-starter-oauth2-client",
springframework_restdocs: "org.springframework.restdocs:spring-restdocs-mockmvc",
springframework_security_test: "org.springframework.security:spring-security-test",
springframework_web: "org.springframework:spring-web",
diff --git a/sechub-webui-solution/helm/sechub-webui/values.yaml b/sechub-webui-solution/helm/sechub-webui/values.yaml
index c3e52e3692..7771934866 100644
--- a/sechub-webui-solution/helm/sechub-webui/values.yaml
+++ b/sechub-webui-solution/helm/sechub-webui/values.yaml
@@ -39,9 +39,9 @@ webui:
appenderName: "LOGSTASH_JSON"
spring:
# Spring profiles (comma-separated list):
- # - Server mode: 'webui_localserver' (self-signed cert) or 'webui_server' (provide SSL certificate)
- # - SecHub connection: 'webui_mocked' or leave empty for connect to configured SecHub server
- profiles: "webui_localserver"
+ # - Server mode: 'ssl-cert-provided' (self-signed SSL cert included) or 'ssl-cert-required' (SSL certificate required by external config)
+ # - SecHub connection: 'basic-auth-mocked' or leave empty for connect to configured SecHub server
+ profiles: "ssl-cert-provided"
# Configure Spring Boot's embedded Tomcat
embeddedTomcat:
logging:
diff --git a/sechub-webui/.gitignore b/sechub-webui/.gitignore
new file mode 100644
index 0000000000..0fc15402e5
--- /dev/null
+++ b/sechub-webui/.gitignore
@@ -0,0 +1,2 @@
+application-local.*.yaml
+application-local.*.yml
\ No newline at end of file
diff --git a/sechub-webui/README.md b/sechub-webui/README.md
new file mode 100644
index 0000000000..22c659cadc
--- /dev/null
+++ b/sechub-webui/README.md
@@ -0,0 +1,61 @@
+
+
+# SecHub WebUI
+
+## Overview
+
+SecHub WebUI is a web-based user interface for managing and interacting with the SecHub application.
+
+## Profiles
+
+To start the application locally use the `webui_local` profile.
+
+This will include the following profiles:
+
+- `ssl-cert-provided`: a default ssl certificate will be used by the WebUI server
+- `basic-auth-mocked`: mock the SecHub Server & enable login with preconfigured credentials at `/login/classic`)
+- `local`: includes any local configurations matching `application-local.${USER}.yml`
+
+If you want to provide local configurations, create a file named `application-local.${USER}.yml` in the `src/main/resources` directory.
+Make sure that the ${USER} part matches your system username.
+
+This will enable configurations suitable for local development and testing.
+
+## Running the application in OAuth2 Mode
+
+To run the application in OAuth2 mode, include the `oauth2-enabled` profile.
+
+Note: The `webui_prod` profile includes the `oauth2-enabled` profile.
+
+Make sure that you either provide a valid `application-oauth2-enabled.yml` file in the `src/main/resources` directory or set the required environment variables.
+
+Example `application-oauth2-enabled.yml`:
+
+```yaml
+sechub:
+ security:
+ oauth2:
+ client-id: example-client-id
+ client-secret: example-client-secret
+ provider: example-provider
+ redirect-uri: {baseUrl}/login/oauth2/code/{provider}
+ issuer-uri: https://sso.example-provider.com
+ authorization-uri: https://sso.example-provider.com/as/authorization.oauth2
+ token-uri: https://sso.example-provider.com/as/token.oauth2
+ user-info-uri: https://sso.example-provider.com/idp/userinfo.openid
+ jwk-set-uri: https://sso.example-provider.com/pf/JWKS
+```
+
+Alternatively, you can provide the following environment variables:
+
+```bash
+SECHUB_SECURITY_OAUTH2_CLIENT_ID=example-client-id
+SECHUB_SECURITY_OAUTH2_CLIENT_SECRET=example-client-secret
+SECHUB_SECURITY_OAUTH2_PROVIDER=example-provider
+SECHUB_SECURITY_OAUTH2_REDIRECT_URI={baseUrl}/login/oauth2/code/{provider}
+SECHUB_SECURITY_OAUTH2_ISSUER_URI=https://sso.example-provider.com
+SECHUB_SECURITY_OAUTH2_AUTHORIZATION_URI=https://sso.example-provider.com/as/authorization.oauth2
+SECHUB_SECURITY_OAUTH2_TOKEN_URI=https://sso.example-provider.com/as/token.oauth2
+SECHUB_SECURITY_OAUTH2_USER_INFO_URI=https://sso.example-provider.com/idp/userinfo.openid
+SECHUB_SECURITY_OAUTH2_JWK_SET_URI=https://sso.example-provider.com/pf/JWKS
+```
diff --git a/sechub-webui/build.gradle b/sechub-webui/build.gradle
index f5707a45db..f0dd829017 100644
--- a/sechub-webui/build.gradle
+++ b/sechub-webui/build.gradle
@@ -13,11 +13,16 @@ plugins {
dependencies {
implementation project(':sechub-commons-core')
implementation project(':sechub-api-java')
+ implementation library.springboot_starter_web
implementation library.springboot_starter_security
implementation library.springboot_starter_thymeleaf
implementation library.logstashLogbackEncoder
- implementation library.springboot_starter_webflux
implementation library.thymeleaf_extras_springsecurity5
+ implementation library.springboot_starter_oauth2_client
+ implementation library.findbugs
+
+ testImplementation library.springboot_starter_test
+ testImplementation library.springframework_security_test
developmentOnly library.springboot_devtoolssf
}
diff --git a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/ApplicationProfiles.java b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/ApplicationProfiles.java
new file mode 100644
index 0000000000..3026e44662
--- /dev/null
+++ b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/ApplicationProfiles.java
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: MIT
+package com.mercedesbenz.sechub.webui;
+
+public final class ApplicationProfiles {
+
+ public static final String BASIC_AUTH_MOCKED = "basic-auth-mocked";
+ public static final String INTEGRATION_TEST_DATA = "integrationtest-data";
+ public static final String LOCAL = "local";
+ public static final String OAUTH2_ENABLED = "oauth2-enabled";
+ public static final String SSL_CERT_PROVIDED = "ssl-cert-provided";
+ public static final String SSL_CERT_REQUIRED = "ssl-cert-required";
+ public static final String TEST = "test";
+
+ private ApplicationProfiles() {
+ // Prevent instantiation
+ }
+}
\ No newline at end of file
diff --git a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/RequestConstants.java b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/RequestConstants.java
index 696d225824..189879c726 100644
--- a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/RequestConstants.java
+++ b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/RequestConstants.java
@@ -1,14 +1,17 @@
// SPDX-License-Identifier: MIT
package com.mercedesbenz.sechub.webui;
-public class RequestConstants {
+public final class RequestConstants {
public static final String ROOT = "/";
public static final String PROJECTS = "/projects";
public static final String PROJECT_SCANS = "/projects/{projectId}/scans";
public static final String STATUS = "/status";
- public static final String LOGIN = "/login";
+ public static final String LOGIN_CLASSIC = "/login/classic";
+ public static final String LOGIN_OAUTH2 = "/login/oauth2";
+ public static final String HOME = "/home";
+ public static final String LOGOUT = "/logout";
public static final String REQUEST_NEW_APITOKEN = "/request-new-apitoken";
}
diff --git a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/credentials/NewApiTokenController.java b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/credentials/NewApiTokenController.java
similarity index 56%
rename from sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/credentials/NewApiTokenController.java
rename to sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/credentials/NewApiTokenController.java
index 11c7506e6b..094accdf3b 100644
--- a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/credentials/NewApiTokenController.java
+++ b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/credentials/NewApiTokenController.java
@@ -1,26 +1,26 @@
// SPDX-License-Identifier: MIT
-package com.mercedesbenz.sechub.webui.page.credentials;
+package com.mercedesbenz.sechub.webui.credentials;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import com.mercedesbenz.sechub.webui.RequestConstants;
-import com.mercedesbenz.sechub.webui.page.user.UserInfoService;
import com.mercedesbenz.sechub.webui.sechubaccess.SecHubAccessService;
+import com.mercedesbenz.sechub.webui.user.UserInfoService;
@Controller
-public class NewApiTokenController {
+class NewApiTokenController {
- @Autowired
- NewApiTokenService newApiTokenService;
+ private final NewApiTokenService newApiTokenService;
+ private final SecHubAccessService accessService;
+ private final UserInfoService userInfoService;
- @Autowired
- SecHubAccessService accessService;
-
- @Autowired
- UserInfoService userInfoService;
+ NewApiTokenController(NewApiTokenService newApiTokenService, SecHubAccessService accessService, UserInfoService userInfoService) {
+ this.newApiTokenService = newApiTokenService;
+ this.accessService = accessService;
+ this.userInfoService = userInfoService;
+ }
@GetMapping(RequestConstants.REQUEST_NEW_APITOKEN)
String requestNewApiToken(Model model) {
diff --git a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/credentials/NewApiTokenService.java b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/credentials/NewApiTokenService.java
similarity index 64%
rename from sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/credentials/NewApiTokenService.java
rename to sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/credentials/NewApiTokenService.java
index 6d5b4be480..cf9bb15bdf 100644
--- a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/credentials/NewApiTokenService.java
+++ b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/credentials/NewApiTokenService.java
@@ -1,21 +1,21 @@
// SPDX-License-Identifier: MIT
-package com.mercedesbenz.sechub.webui.page.credentials;
+package com.mercedesbenz.sechub.webui.credentials;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.mercedesbenz.sechub.webui.sechubaccess.SecHubAccessService;
@Service
-public class NewApiTokenService {
+class NewApiTokenService {
- @Autowired
- SecHubAccessService accessService;
+ private final SecHubAccessService accessService;
+
+ NewApiTokenService(SecHubAccessService accessService) {
+ this.accessService = accessService;
+ }
/**
- * Request new API token as described in
- *
- * documentation
*
@@ -25,9 +25,10 @@ public class NewApiTokenService {
* com' -i -X POST -H 'Content-Type: application/json;charset=UTF-8'
*
*/
- public boolean requestNewApiToken(String emailAddress) {
+ boolean requestNewApiToken(String emailAddress) {
/* @formatter:off */
- Boolean succesfulSendNewApiToken = accessService.createExecutorForResult(Boolean.class).
+
+ return accessService.createExecutorForResult(Boolean.class).
whenDoing("request a new api token").
callAndReturn(client -> {
client.requestNewApiToken(emailAddress);
@@ -36,8 +37,6 @@ public boolean requestNewApiToken(String emailAddress) {
onErrorReturn(exception -> Boolean.FALSE).
execute();
- return succesfulSendNewApiToken;
-
/* @formatter:on */
}
}
diff --git a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/HomeController.java b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/HomeController.java
new file mode 100644
index 0000000000..e041d4149e
--- /dev/null
+++ b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/HomeController.java
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: MIT
+package com.mercedesbenz.sechub.webui.page;
+
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+
+import com.mercedesbenz.sechub.webui.RequestConstants;
+
+@Controller
+public class HomeController {
+
+ @GetMapping({ RequestConstants.ROOT, RequestConstants.HOME })
+ public String home(@AuthenticationPrincipal OidcUser principal, Model model) {
+ if (principal != null) {
+ model.addAttribute("principal", principal.getAttribute("name"));
+ }
+ return "home";
+ }
+}
diff --git a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/LoginController.java b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/LoginClassicController.java
similarity index 70%
rename from sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/LoginController.java
rename to sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/LoginClassicController.java
index 6a9a2c0fa2..1a22f65737 100644
--- a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/LoginController.java
+++ b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/LoginClassicController.java
@@ -7,10 +7,10 @@
import com.mercedesbenz.sechub.webui.RequestConstants;
@Controller
-public class LoginController {
+public class LoginClassicController {
- @GetMapping(RequestConstants.LOGIN)
+ @GetMapping(RequestConstants.LOGIN_CLASSIC)
String login() {
- return "login";
+ return "login-classic";
}
}
\ No newline at end of file
diff --git a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/LoginOAuth2Controller.java b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/LoginOAuth2Controller.java
new file mode 100644
index 0000000000..659574f2b9
--- /dev/null
+++ b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/LoginOAuth2Controller.java
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: MIT
+package com.mercedesbenz.sechub.webui.page;
+
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+
+import com.mercedesbenz.sechub.webui.ApplicationProfiles;
+import com.mercedesbenz.sechub.webui.RequestConstants;
+
+@Controller
+@Profile(ApplicationProfiles.OAUTH2_ENABLED)
+class LoginOAuth2Controller {
+
+ @GetMapping(RequestConstants.LOGIN_OAUTH2)
+ String login(Model model) {
+ // TODO: make this configurable later for multiple client registrations
+ String registrationId = "mercedes-benz";
+ model.addAttribute("registrationId", registrationId);
+ return "login-oauth2";
+ }
+}
\ No newline at end of file
diff --git a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/project/ProjectInfoService.java b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/project/ProjectInfoService.java
deleted file mode 100644
index 8e329eca2c..0000000000
--- a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/project/ProjectInfoService.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// SPDX-License-Identifier: MIT
-package com.mercedesbenz.sechub.webui.page.project;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
-import com.mercedesbenz.sechub.webui.page.user.UserInfoService;
-
-@Service
-public class ProjectInfoService {
-
- @Autowired
- UserInfoService userInfoService;
-
- public List
+ * Custom implementation of {@link OAuth2AccessTokenResponseClient} for
+ * retrieving a JWT token response from a configured Identity Provider (IDP)
+ * after a successful OAuth2 authorization code grant request.
+ *
+ * This class handles the exchange of the authorization code for an access
+ * token, refresh token, and ID token by making a POST request to the token
+ * endpoint of the IDP. The client credentials (client ID and client secret) are
+ * encoded in Base64 and included in the Authorization header of the request.
+ *
+ * The response from the IDP is expected to be a {@link JwtResponse}, which
+ * includes the access token, refresh token, ID token, and the expiration time
+ * of the access token.
+ *
+ * The {@code JwtResponse} object is constructed from JSON using Jackson,
+ * mapping the expected token fields from the authentication response.
+ *
+ * Fields:
+ *
+ *
+ *
+ * For more information on JSON Web Tokens (JWT), please refer to the + * official JWT documentation. + *
+ * + * @author hamidonos + */ +class JwtResponse { + + private static final String JSON_PROPERTY_ACCESS_TOKEN = "access_token"; + private static final String JSON_PROPERTY_TOKEN_TYPE = "token_type"; + private static final String JSON_PROPERTY_ID_TOKEN = "id_token"; + private static final String JSON_PROPERTY_EXPIRES_IN = "expires_in"; + private static final String JSON_PROPERTY_REFRESH_TOKEN = "refresh_token"; + + private final String accessToken; + private final String tokenType; + private final String idToken; + private final Long expiresIn; + private final String refreshToken; + + @JsonCreator + JwtResponse(@JsonProperty(JSON_PROPERTY_ACCESS_TOKEN) String accessToken, @JsonProperty(JSON_PROPERTY_TOKEN_TYPE) String tokenType, + @JsonProperty(JSON_PROPERTY_ID_TOKEN) String idToken, @JsonProperty(JSON_PROPERTY_EXPIRES_IN) Long expiresIn, + @JsonProperty(JSON_PROPERTY_REFRESH_TOKEN) String refreshToken) { + this.accessToken = requireNonNull(accessToken, JSON_PROPERTY_ACCESS_TOKEN + " must not be null"); + this.tokenType = requireNonNull(tokenType, JSON_PROPERTY_TOKEN_TYPE + " must not be null"); + this.idToken = requireNonNull(idToken, JSON_PROPERTY_ID_TOKEN + " must not be null"); + this.expiresIn = requireNonNull(expiresIn, JSON_PROPERTY_EXPIRES_IN + " must not be null"); + this.refreshToken = refreshToken; + } + + String getAccessToken() { + return accessToken; + } + + String getTokenType() { + return tokenType; + } + + String getIdToken() { + return idToken; + } + + Long getExpiresIn() { + return expiresIn; + } + + String getRefreshToken() { + return refreshToken; + } +} diff --git a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/security/LoginAuthenticationSuccessHandler.java b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/security/LoginAuthenticationSuccessHandler.java new file mode 100644 index 0000000000..78e639be53 --- /dev/null +++ b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/security/LoginAuthenticationSuccessHandler.java @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.webui.security; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import com.mercedesbenz.sechub.webui.RequestConstants; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * {@code OAuth2SuccessHandler} implements {@link AuthenticationSuccessHandler} + * to provide custom behavior upon successful authentication. This handler + * redirects the user to the /home page specified in {@link RequestConstants}. + * + * @see SecurityConfiguration + * @see RequestConstants + * + * @author hamidonos + */ +class LoginAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + private static final Logger LOG = LoggerFactory.getLogger(LoginAuthenticationSuccessHandler.class); + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + LOG.atDebug().log("Redirecting to %s".formatted(RequestConstants.HOME)); + response.sendRedirect(RequestConstants.HOME); + } +} \ No newline at end of file diff --git a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/security/OAuth2Properties.java b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/security/OAuth2Properties.java new file mode 100644 index 0000000000..589a6deaeb --- /dev/null +++ b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/security/OAuth2Properties.java @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.webui.security; + +import static java.util.Objects.requireNonNull; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.ConstructorBinding; + +@ConfigurationProperties(prefix = OAuth2Properties.PREFIX) +final class OAuth2Properties { + + static final String PREFIX = "sechub.security.oauth2"; + private static final String ERR_MSG_FORMAT = "The property '%s.%s' must not be null"; + + private final String clientId; + private final String clientSecret; + private final String provider; + private final String redirectUri; + private final String issuerUri; + private final String authorizationUri; + private final String tokenUri; + private final String userInfoUri; + private final String jwkSetUri; + + /* @formatter:off */ + @ConstructorBinding + OAuth2Properties(String clientId, + String clientSecret, + String provider, + String redirectUri, + String issuerUri, + String authorizationUri, + String tokenUri, + String userInfoUri, + String jwkSetUri) { + this.clientId = requireNonNull(clientId, ERR_MSG_FORMAT.formatted(PREFIX, "client-id")); + this.clientSecret = requireNonNull(clientSecret, ERR_MSG_FORMAT.formatted(PREFIX, "client-secret"));; + this.provider = requireNonNull(provider, ERR_MSG_FORMAT.formatted(PREFIX, "provider")); + this.redirectUri = requireNonNull(redirectUri, ERR_MSG_FORMAT.formatted(PREFIX, "redirect-uri")); + this.issuerUri = requireNonNull(issuerUri, ERR_MSG_FORMAT.formatted(PREFIX, "issuer-uri")); + this.authorizationUri = requireNonNull(authorizationUri, ERR_MSG_FORMAT.formatted(PREFIX, "authorization-uri")); + this.tokenUri = requireNonNull(tokenUri, ERR_MSG_FORMAT.formatted(PREFIX, "token-uri")); + this.userInfoUri = requireNonNull(userInfoUri, ERR_MSG_FORMAT.formatted(PREFIX, "user-info-uri")); + this.jwkSetUri = requireNonNull(jwkSetUri, ERR_MSG_FORMAT.formatted(PREFIX, "jwk-set-uri")); + } + /* @formatter:on */ + + String getClientId() { + return clientId; + } + + String getClientSecret() { + return clientSecret; + } + + String getProvider() { + return provider; + } + + String getRedirectUri() { + return redirectUri; + } + + String getIssuerUri() { + return issuerUri; + } + + String getAuthorizationUri() { + return authorizationUri; + } + + String getTokenUri() { + return tokenUri; + } + + String getUserInfoUri() { + return userInfoUri; + } + + String getJwkSetUri() { + return jwkSetUri; + } +} \ No newline at end of file diff --git a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/security/OAuth2PropertiesConfig.java b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/security/OAuth2PropertiesConfig.java new file mode 100644 index 0000000000..db08b4f4c9 --- /dev/null +++ b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/security/OAuth2PropertiesConfig.java @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.webui.security; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +import com.mercedesbenz.sechub.webui.ApplicationProfiles; + +/** + * The + * {@link org.springframework.boot.context.properties.ConfigurationProperties} + * annotation does not support the {@link Profile} annotation. To ensure that + * the properties are only loaded when the + * {@link ApplicationProfiles#OAUTH2_ENABLED} profile is active, this separate + * configuration class is created with the {@link Profile} annotation. + * + * @author hamidonos + */ +@Configuration +@Profile(ApplicationProfiles.OAUTH2_ENABLED) +@EnableConfigurationProperties(OAuth2Properties.class) +class OAuth2PropertiesConfig { +} diff --git a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/security/SecurityConfiguration.java b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/security/SecurityConfiguration.java index fc7d814296..a5b0ed4c22 100644 --- a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/security/SecurityConfiguration.java +++ b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/security/SecurityConfiguration.java @@ -1,49 +1,117 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.webui.security; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; -import org.springframework.security.config.web.server.ServerHttpSecurity; -import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; -import org.springframework.security.web.server.SecurityWebFilterChain; -import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; +import org.springframework.context.annotation.Profile; +import org.springframework.core.env.Environment; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +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.web.SecurityFilterChain; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.web.client.RestTemplate; -import com.mercedesbenz.sechub.webui.page.user.UserDetailInformationService; +import com.mercedesbenz.sechub.webui.ApplicationProfiles; +import com.mercedesbenz.sechub.webui.RequestConstants; @Configuration -@EnableWebFluxSecurity -public class SecurityConfiguration { - @Autowired - UserDetailInformationService userDetailInformationService; +@EnableWebSecurity +@EnableMethodSecurity +class SecurityConfiguration { + private static final String[] PUBLIC_PATHS = { RequestConstants.LOGIN_CLASSIC, RequestConstants.LOGIN_OAUTH2, "/css/**", "/js/**", "/images/**" }; + private static final String SCOPE = "openid"; + private static final String USER_NAME_ATTRIBUTE_NAME = "sub"; + + private final Environment environment; + private final OAuth2Properties oAuth2Properties; + + SecurityConfiguration(@Autowired Environment environment, @Autowired(required = false) OAuth2Properties oAuth2Properties) { + this.environment = environment; + if (isOAuth2Enabled() && oAuth2Properties == null) { + throw new NoSuchBeanDefinitionException( + "No qualifying bean of type 'OAuth2Properties' available: expected at least 1 bean which qualifies as autowire candidate."); + } + this.oAuth2Properties = oAuth2Properties; + } + + @Bean + @Profile(ApplicationProfiles.OAUTH2_ENABLED) + ClientRegistrationRepository clientRegistrationRepository() { + /* @formatter:off */ + ClientRegistration clientRegistration = ClientRegistration + .withRegistrationId(oAuth2Properties.getProvider()) + .clientId(oAuth2Properties.getClientId()) + .clientSecret(oAuth2Properties.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()) + .userNameAttributeName(USER_NAME_ATTRIBUTE_NAME) + .build(); + /* @formatter:on */ + + return new InMemoryClientRegistrationRepository(clientRegistration); + } @Bean - public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity httpSecurity) { + SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + AuthenticationSuccessHandler authenticationSuccessHandler = new LoginAuthenticationSuccessHandler(); /* @formatter:off */ - httpSecurity. - authorizeExchange(exchanges -> exchanges. - pathMatchers("/css/**", "/js/**", "/images/**").permitAll(). - pathMatchers("/login").permitAll(). - anyExchange().authenticated() - ). - formLogin(formLogin -> formLogin. - loginPage("/login") - ). - logout(logout -> logout. - logoutUrl("/logout"). - requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout")) - ). - csrf((csrf) -> csrf.disable() // CSRF protection disabled. The CookieServerCsrfTokenRepository does not work, since Spring Boot 3 - ); - /* @formatter:on */ + + httpSecurity + /* Disable CSRF */ + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(exchanges -> exchanges + /* Allow access to public paths */ + .requestMatchers(PUBLIC_PATHS).permitAll() + /* Protect all other paths */ + .anyRequest().authenticated() + ) + /* Enable stateful sessions */ + .sessionManagement(httpSecuritySessionManagementConfigurer -> httpSecuritySessionManagementConfigurer + .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)); + + if (isOAuth2Enabled()) { + RestTemplate restTemplate = new RestTemplate(); + Base64EncodedClientIdAndSecretOAuth2AccessTokenClient base64EncodedClientIdAndSecretOAuth2AccessTokenClient = new Base64EncodedClientIdAndSecretOAuth2AccessTokenClient(restTemplate); + /* Enable OAuth2 */ + httpSecurity.oauth2Login(oauth2 -> oauth2 + .loginPage(RequestConstants.LOGIN_OAUTH2) + .tokenEndpoint(token -> token.accessTokenResponseClient(base64EncodedClientIdAndSecretOAuth2AccessTokenClient)) + .successHandler(authenticationSuccessHandler)); + } + + /* + Enable Form Login + 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 + */ + httpSecurity + .formLogin(form -> form + .loginPage(RequestConstants.LOGIN_CLASSIC) + .permitAll() + .successHandler(authenticationSuccessHandler)); + + /* @formatter:on */ + return httpSecurity.build(); } - @Bean - public MapReactiveUserDetailsService userDetailsService() { - return new MapReactiveUserDetailsService(userDetailInformationService.getUser(), userDetailInformationService.getUser()); + private boolean isOAuth2Enabled() { + return environment.matchesProfiles(ApplicationProfiles.OAUTH2_ENABLED); } + } diff --git a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/user/UserDetailInformationService.java b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/user/UserDetailInformationService.java similarity index 93% rename from sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/user/UserDetailInformationService.java rename to sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/user/UserDetailInformationService.java index 3400bbc5f5..42afd54caf 100644 --- a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/user/UserDetailInformationService.java +++ b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/user/UserDetailInformationService.java @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.webui.page.user; +package com.mercedesbenz.sechub.webui.user; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; diff --git a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/user/UserInfoService.java b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/user/UserInfoService.java similarity index 95% rename from sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/user/UserInfoService.java rename to sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/user/UserInfoService.java index 766ed0f020..12318d36e0 100644 --- a/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/page/user/UserInfoService.java +++ b/sechub-webui/src/main/java/com/mercedesbenz/sechub/webui/user/UserInfoService.java @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -package com.mercedesbenz.sechub.webui.page.user; +package com.mercedesbenz.sechub.webui.user; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; diff --git a/sechub-webui/src/main/resources/application-webui_mocked.yml b/sechub-webui/src/main/resources/application-basic-auth-mocked.yml similarity index 74% rename from sechub-webui/src/main/resources/application-webui_mocked.yml rename to sechub-webui/src/main/resources/application-basic-auth-mocked.yml index 95e631b816..983a955dc7 100644 --- a/sechub-webui/src/main/resources/application-webui_mocked.yml +++ b/sechub-webui/src/main/resources/application-basic-auth-mocked.yml @@ -6,3 +6,9 @@ webui: ## Mocked sechub user (necessary for credentialinjection, we have no defaults...) userid: "mocked-user" apitoken: "mocked-apitoken" + +spring: + security: + user: + name: sechub + password: password \ No newline at end of file diff --git a/sechub-webui/src/main/resources/application-webui_integrationtest-data.yml b/sechub-webui/src/main/resources/application-integrationtest-data.yml similarity index 100% rename from sechub-webui/src/main/resources/application-webui_integrationtest-data.yml rename to sechub-webui/src/main/resources/application-integrationtest-data.yml diff --git a/sechub-webui/src/main/resources/application-local.yml b/sechub-webui/src/main/resources/application-local.yml new file mode 100644 index 0000000000..4a0591d0a0 --- /dev/null +++ b/sechub-webui/src/main/resources/application-local.yml @@ -0,0 +1,9 @@ +# Utility file to load user specific configuration for local development +# Define a profile for your local system user by creating a file named application-local.${USER}.yml (if needed) +# ${USER} is the value of your system username (e.g. application-local.JOHNDOE.yml) +# Note that all application-local.${USER}.yml files are ignored by git + +spring: + config: + import: + - optional:classpath:application-local.${USER}.yml \ No newline at end of file diff --git a/sechub-webui/src/main/resources/application-webui_localserver.yml b/sechub-webui/src/main/resources/application-ssl-cert-provided.yml similarity index 79% rename from sechub-webui/src/main/resources/application-webui_localserver.yml rename to sechub-webui/src/main/resources/application-ssl-cert-provided.yml index 1c2f4cc59c..958242c602 100644 --- a/sechub-webui/src/main/resources/application-webui_localserver.yml +++ b/sechub-webui/src/main/resources/application-ssl-cert-provided.yml @@ -1,4 +1,6 @@ # SPDX-License-Identifier: MIT +# This configuration is used for development and local testing. It uses a self-signed certificate provided by the build. + server: ssl: key-store-type: 'PKCS12' diff --git a/sechub-webui/src/main/resources/application-webui_server.yml b/sechub-webui/src/main/resources/application-ssl-cert-required.yml similarity index 69% rename from sechub-webui/src/main/resources/application-webui_server.yml rename to sechub-webui/src/main/resources/application-ssl-cert-required.yml index 70caeaaba8..a0ce3c19ef 100644 --- a/sechub-webui/src/main/resources/application-webui_server.yml +++ b/sechub-webui/src/main/resources/application-ssl-cert-required.yml @@ -1,4 +1,6 @@ # SPDX-License-Identifier: MIT +# This configuration is used for prod and int. It requires a valid certificate to be provided through the environment variables. + server: ssl: keyStoreType: diff --git a/sechub-webui/src/main/resources/application-webui_test.yml b/sechub-webui/src/main/resources/application-webui_test.yml deleted file mode 100644 index 05d1a2afbe..0000000000 --- a/sechub-webui/src/main/resources/application-webui_test.yml +++ /dev/null @@ -1,4 +0,0 @@ -# SPDX-License-Identifier: MIT -server: - ssl: - enabled: false \ No newline at end of file diff --git a/sechub-webui/src/main/resources/application.yml b/sechub-webui/src/main/resources/application.yml index 8a0261ee24..c1dc9e877c 100644 --- a/sechub-webui/src/main/resources/application.yml +++ b/sechub-webui/src/main/resources/application.yml @@ -9,12 +9,24 @@ server: protocol: TLS enabled-protocols: TLSv1.2,TLSv1.3 + spring: messages: basename: "i18n/messages" profiles: group: - webui_prod: "webui_server" - webui_dev: "webui_localserver,webui_mocked" - webui_test: "webui_test" - webui_integrationtest: "webui_localserver,webui_integrationtest-data" + webui_prod: "ssl-cert-required,oauth2-enabled" + webui_int: "ssl-cert-required,oauth2-enabled,basic-auth-mocked" + webui_dev: "ssl-cert-provided,basic-auth-mocked" + webui_local: "ssl-cert-provided,basic-auth-mocked,local" + webui_test: "test" + webui_integrationtest: "ssl-cert-provided,integrationtest-data" + web: + resources: + static-locations: classpath:/static + +logging: + level: + org: + springframework: + security: DEBUG diff --git a/sechub-webui/src/main/resources/templates/home.html b/sechub-webui/src/main/resources/templates/home.html new file mode 100644 index 0000000000..d57369166d --- /dev/null +++ b/sechub-webui/src/main/resources/templates/home.html @@ -0,0 +1,47 @@ + + + + + +This site is under construction. Please check back later.
+Invalid username or password.
+Date | -Job UUID | -Download | -
---|---|---|
23.04.2022 | -b9738d0e-a913-11ec-bd6c-c3a099311e89 | -Download | -
22.04.2022 | -f79ba56c-a913-11ec-bb97-93283fb5d88b | -Download | -
04.04.2022 | -12c32504-a914-11ec-91ea-6b1199e06e4e | -Download | -
URL | -Server URL | -|
Status | -unavailable | -|
Scheduler | -disabled | -ERROR: No SecHub status available! | -
Version | -0.0.0 | -
Running | -99 | -
Waiting | -99 | -
Initializating | -99 | -
Cancel requested | -9 | -
Canceled | -9 | -
Ended | -9 | -
Jobs all | -999 | -
@LocalServerPort annotation.
- */
-@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
-@AutoConfigureWebTestClient
-@ActiveProfiles("webui_test")
-class SecHubWebUiApplicationSpringBootTest {
-
- @Autowired
- private WebTestClient webTestClient;
-
- @MockBean
- private SecHubAccessService mockAccessService;
-
- @Test
- void contextLoads(ApplicationContext context) {
- assertNotNull(context);
- }
-
- @Test
- void index() throws Exception {
- webTestClient.get().uri("/").exchange().expectStatus().isFound();
- }
-}
diff --git a/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/YamlPropertyLoaderFactory.java b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/YamlPropertyLoaderFactory.java
new file mode 100644
index 0000000000..d9f0899f02
--- /dev/null
+++ b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/YamlPropertyLoaderFactory.java
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: MIT
+package com.mercedesbenz.sechub.webui;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.springframework.boot.env.YamlPropertySourceLoader;
+import org.springframework.core.env.PropertySource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.DefaultPropertySourceFactory;
+import org.springframework.core.io.support.EncodedResource;
+import org.springframework.lang.Nullable;
+
+/**
+ * Spring Boot's default property source factory does not support YAML files
+ * directly in `@TestPropertySource`. By using this custom factory, we can load
+ * properties from YAML files, which is often more convenient and readable for
+ * complex configurations.
+ *
+ * @author hamidonos
+ */
+public class YamlPropertyLoaderFactory extends DefaultPropertySourceFactory {
+
+ @Override
+ public PropertySource> createPropertySource(@Nullable String name, EncodedResource resource) throws IOException {
+ Resource actualResource = resource.getResource();
+ if (actualResource.exists()) {
+ YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
+ List> propertySources = loader.load(name != null ? name : actualResource.getFilename(), actualResource);
+ return propertySources.isEmpty() ? null : propertySources.get(0);
+ }
+ return super.createPropertySource(name, resource);
+ }
+}
\ No newline at end of file
diff --git a/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/page/HomeControllerTest.java b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/page/HomeControllerTest.java
new file mode 100644
index 0000000000..016a8099ec
--- /dev/null
+++ b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/page/HomeControllerTest.java
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: MIT
+package com.mercedesbenz.sechub.webui.page;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.web.servlet.MockMvc;
+
+import com.mercedesbenz.sechub.webui.YamlPropertyLoaderFactory;
+import com.mercedesbenz.sechub.webui.security.SecurityTestConfiguration;
+
+@WebMvcTest(HomeController.class)
+@Import(SecurityTestConfiguration.class)
+@TestPropertySource(locations = "classpath:application-test.yml", factory = YamlPropertyLoaderFactory.class)
+class HomeControllerTest {
+
+ private final MockMvc mockMvc;
+
+ @Autowired
+ public HomeControllerTest(MockMvc mockMvc) {
+ this.mockMvc = mockMvc;
+ }
+
+ @Test
+ void home_page_is_not_accessible_anonymously() throws Exception {
+ /* @formatter:off */
+ mockMvc
+ .perform(get("/home"))
+ .andExpect(status().is3xxRedirection());
+ /* @formatter:on */
+ }
+
+ @Test
+ @WithMockUser
+ void home_page_is_accessible_with_authenticated_user() throws Exception {
+ /* @formatter:off */
+ mockMvc
+ .perform(get("/home"))
+ .andExpect(status().isOk());
+ /* @formatter:on */
+ }
+}
diff --git a/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/page/LoginClassicControllerTest.java b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/page/LoginClassicControllerTest.java
new file mode 100644
index 0000000000..68e692bbfa
--- /dev/null
+++ b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/page/LoginClassicControllerTest.java
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: MIT
+package com.mercedesbenz.sechub.webui.page;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.web.servlet.MockMvc;
+
+import com.mercedesbenz.sechub.webui.YamlPropertyLoaderFactory;
+import com.mercedesbenz.sechub.webui.security.SecurityTestConfiguration;
+
+@WebMvcTest(LoginClassicController.class)
+@Import(SecurityTestConfiguration.class)
+@TestPropertySource(locations = "classpath:application-test.yml", factory = YamlPropertyLoaderFactory.class)
+class LoginClassicControllerTest {
+
+ private final MockMvc mockMvc;
+
+ @Autowired
+ LoginClassicControllerTest(MockMvc mockMvc) {
+ this.mockMvc = mockMvc;
+ }
+
+ @Test
+ void login_classic_page_is_accessible_anonymously() throws Exception {
+ /* @formatter:off */
+ mockMvc
+ .perform(get("/login/classic"))
+ .andExpect(status().isOk());
+ /* @formatter:on */
+ }
+}
diff --git a/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/page/LoginControllerTest.java b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/page/LoginControllerTest.java
deleted file mode 100644
index f55d78025f..0000000000
--- a/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/page/LoginControllerTest.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// SPDX-License-Identifier: MIT
-package com.mercedesbenz.sechub.webui.page;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-import org.springframework.test.web.reactive.server.WebTestClient;
-
-import com.mercedesbenz.sechub.webui.sechubaccess.SecHubAccessService;
-
-/*
- * No HTTP Server will be started for this test
- * for more details see: https://docs.spring.io/spring-framework/docs/current/reference/html/testing.html#mock-objects-web-reactive
- */
-@WebFluxTest(controllers = LoginController.class)
-public class LoginControllerTest {
-
- @Autowired
- private WebTestClient webTestClient;
-
- @MockBean
- private SecHubAccessService mockAccessService;
-
- @Test
- void login() throws Exception {
- webTestClient.get().uri("/login").exchange().expectStatus().isOk();
- }
-}
diff --git a/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/page/LoginOAuth2ControllerTest.java b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/page/LoginOAuth2ControllerTest.java
new file mode 100644
index 0000000000..0cc8ea140c
--- /dev/null
+++ b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/page/LoginOAuth2ControllerTest.java
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: MIT
+package com.mercedesbenz.sechub.webui.page;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.web.servlet.MockMvc;
+
+import com.mercedesbenz.sechub.webui.YamlPropertyLoaderFactory;
+import com.mercedesbenz.sechub.webui.security.SecurityTestConfiguration;
+
+@WebMvcTest(LoginOAuth2Controller.class)
+@Import(SecurityTestConfiguration.class)
+@ActiveProfiles("oauth2-enabled")
+@TestPropertySource(locations = "classpath:application-test.yml", factory = YamlPropertyLoaderFactory.class)
+class LoginOAuth2ControllerTest {
+
+ private final MockMvc mockMvc;
+
+ @Autowired
+ LoginOAuth2ControllerTest(MockMvc mockMvc) {
+ this.mockMvc = mockMvc;
+ }
+
+ @Test
+ void login_o_auth_2_page_is_accessible_anonymously() throws Exception {
+ /* @formatter:off */
+ mockMvc
+ .perform(get("/login/oauth2"))
+ .andExpect(status().isOk());
+ /* @formatter:on */
+ }
+}
diff --git a/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/page/project/ProjectsControllerTest.java b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/page/project/ProjectsControllerTest.java
deleted file mode 100644
index 74b5dc6fe4..0000000000
--- a/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/page/project/ProjectsControllerTest.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// SPDX-License-Identifier: MIT
-package com.mercedesbenz.sechub.webui.page.project;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-import org.springframework.test.web.reactive.server.WebTestClient;
-
-import com.mercedesbenz.sechub.webui.sechubaccess.SecHubAccessService;
-
-/*
- * No HTTP Server will be started for this test
- * for more details see: https://docs.spring.io/spring-framework/docs/current/reference/html/testing.html#mock-objects-web-reactive
- */
-@WebFluxTest(controllers = ProjectsController.class)
-public class ProjectsControllerTest {
- @Autowired
- private WebTestClient webTestClient;
-
- @MockBean
- private SecHubAccessService mockAccessService;
-
- @MockBean
- private ProjectInfoService projectInfoService;
-
- @Test
- void index() throws Exception {
- webTestClient.get().uri("/").exchange().expectStatus().isUnauthorized();
- }
-
- @Test
- void projects() throws Exception {
- webTestClient.get().uri("/projects").exchange().expectStatus().isUnauthorized();
- }
-}
diff --git a/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/security/Base64EncodedClientIdAndSecretOAuth2AccessTokenClientTest.java b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/security/Base64EncodedClientIdAndSecretOAuth2AccessTokenClientTest.java
new file mode 100644
index 0000000000..2602f88448
--- /dev/null
+++ b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/security/Base64EncodedClientIdAndSecretOAuth2AccessTokenClientTest.java
@@ -0,0 +1,187 @@
+// SPDX-License-Identifier: MIT
+package com.mercedesbenz.sechub.webui.security;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.content;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.Set;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opentest4j.TestAbortedException;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.test.web.client.MockRestServiceServer;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.server.ResponseStatusException;
+
+import com.jayway.jsonpath.JsonPath;
+
+class Base64EncodedClientIdAndSecretOAuth2AccessTokenClientTest {
+
+ // @formatter:off
+ private static final RestTemplate restTemplate = new RestTemplate();
+ private static final MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate);
+ private static final Base64EncodedClientIdAndSecretOAuth2AccessTokenClient client = new Base64EncodedClientIdAndSecretOAuth2AccessTokenClient(restTemplate);
+ private static final ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(Constants.REGISTRATION_ID)
+ .clientId(Constants.CLIENT_ID)
+ .clientSecret(Constants.CLIENT_SECRET)
+ .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+ .redirectUri(Constants.REDIRECT_URI)
+ .tokenUri(Constants.TOKEN_URI)
+ .authorizationUri(Constants.AUTHORIZATION_URI)
+ .build();
+ private static final OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
+ .authorizationUri(Constants.AUTHORIZATION_URI)
+ .clientId(Constants.CLIENT_ID)
+ .redirectUri(Constants.REDIRECT_URI)
+ .scopes(Set.of(Constants.OPENID))
+ .state(Constants.STATE)
+ .build();
+ private static final OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponse.success(Constants.CODE)
+ .redirectUri(Constants.REDIRECT_URI)
+ .state(Constants.STATE)
+ .build();
+ // @formatter:on
+ private static final OAuth2AuthorizationExchange authorizationExchange = new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse);
+ private static final OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest = new OAuth2AuthorizationCodeGrantRequest(clientRegistration,
+ authorizationExchange);
+ private static final String jwtResponseJson;
+
+ static {
+ try {
+ jwtResponseJson = Files.readString(Paths.get("src/test/resources/jwt-response.json"));
+ } catch (IOException e) {
+ throw new TestAbortedException("Failed to prepare test", e);
+ }
+ }
+
+ @BeforeEach
+ void beforeEach() {
+ mockServer.reset();
+ }
+
+ @Test
+ void get_token_response_executes_correctly_formatted_http_request() {
+ // prepare
+ String tokenUri = clientRegistration.getProviderDetails().getTokenUri();
+ String authorizationHeaderValue = getBasicAuthHeaderValue(clientRegistration.getClientId(), clientRegistration.getClientSecret());
+ // @formatter:off
+ mockServer.expect(requestTo(tokenUri))
+ .andExpect(method(HttpMethod.POST))
+ .andExpect(header(HttpHeaders.CONTENT_TYPE, Constants.APPLICATION_FORM_URLENCODED_VALUE))
+ .andExpect(header(HttpHeaders.AUTHORIZATION, authorizationHeaderValue))
+ .andExpect(content().formData(getMultiValueMap(authorizationCodeGrantRequest)))
+ .andRespond(withSuccess(jwtResponseJson, MediaType.APPLICATION_JSON));
+ // @formatter:on
+
+ // execute
+ client.getTokenResponse(authorizationCodeGrantRequest);
+
+ // verify
+ mockServer.verify();
+ }
+
+ @Test
+ void get_token_response_returns_o_auth_access_token_as_expected() {
+ // prepare
+ String tokenUri = clientRegistration.getProviderDetails().getTokenUri();
+ // @formatter:off
+ mockServer.expect(requestTo(tokenUri))
+ .andExpect(method(HttpMethod.POST))
+ .andRespond(withSuccess(jwtResponseJson, MediaType.APPLICATION_JSON));
+ // @formatter:on
+
+ // execute
+ OAuth2AccessTokenResponse oAuth2AccessTokenResponse = client.getTokenResponse(authorizationCodeGrantRequest);
+
+ // verify
+ mockServer.verify();
+ assertThat(oAuth2AccessTokenResponse.getAccessToken().getTokenValue()).isEqualTo(JsonPath.read(jwtResponseJson, "$.access_token"));
+ assertThat(oAuth2AccessTokenResponse.getAccessToken().getTokenType().getValue()).isEqualTo(JsonPath.read(jwtResponseJson, "$.token_type"));
+ assertThat(oAuth2AccessTokenResponse.getAdditionalParameters().get(Constants.ID_TOKEN)).isEqualTo(JsonPath.read(jwtResponseJson, "$.id_token"));
+ assertThat(oAuth2AccessTokenResponse.getRefreshToken()).isNotNull();
+ assertThat(oAuth2AccessTokenResponse.getRefreshToken().getTokenValue()).isEqualTo(JsonPath.read(jwtResponseJson, "$.refresh_token"));
+ int expiresIn = JsonPath.read(jwtResponseJson, "$.expires_in");
+ long expiresInLong = Long.parseLong(String.valueOf(expiresIn));
+ assertThat(oAuth2AccessTokenResponse.getAccessToken().getExpiresAt())
+ .isAfterOrEqualTo(Instant.now().minus(expiresInLong, java.time.temporal.ChronoUnit.SECONDS));
+ }
+
+ @Test
+ void get_token_response_handles_rest_client_exception_well() {
+ // prepare
+ String tokenUri = clientRegistration.getProviderDetails().getTokenUri();
+ // @formatter:off
+ mockServer.expect(requestTo(tokenUri))
+ .andExpect(method(HttpMethod.POST))
+ .andRespond(withSuccess("", MediaType.APPLICATION_JSON));
+
+ // execute & assert
+
+ assertThatThrownBy(() -> client.getTokenResponse(authorizationCodeGrantRequest))
+ .isExactlyInstanceOf(ResponseStatusException.class)
+ .hasMessageContaining("Failed to get JWT token response");
+ // @formatter:on
+ }
+
+ private static MultiValueMap getMultiValueMap(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) {
+ MultiValueMap formParameters = new LinkedMultiValueMap<>();
+ OAuth2AuthorizationExchange authorizationExchange = authorizationGrantRequest.getAuthorizationExchange();
+ String code = authorizationExchange.getAuthorizationResponse().getCode();
+ String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri();
+
+ formParameters.add(OAuth2ParameterNames.GRANT_TYPE, Constants.GRANT_TYPE_VALUE);
+ formParameters.add(OAuth2ParameterNames.CODE, code);
+ formParameters.add(OAuth2ParameterNames.REDIRECT_URI, redirectUri);
+
+ return formParameters;
+ }
+
+ private static String getBasicAuthHeaderValue(String clientId, String clientSecret) {
+ String clientIdClientSecret = Constants.CLIENT_ID_CLIENT_SECRET_FORMAT.formatted(clientId, clientSecret);
+ String clientIdClientSecretB64Encoded = Base64.getEncoder().encodeToString(clientIdClientSecret.getBytes());
+ return String.format(Constants.BASIC_AUTHORIZATION_HEADER_VALUE_FORMAT, clientIdClientSecretB64Encoded);
+ }
+
+ private static final class Constants {
+ private static final String REGISTRATION_ID = "registration-id";
+ private static final String CLIENT_ID = "client-id";
+ private static final String CLIENT_SECRET = "client-secret";
+ private static final String GRANT_TYPE_VALUE = "authorization_code";
+ private static final String BASIC_AUTHORIZATION_HEADER_VALUE_FORMAT = "Basic %s";
+ private static final String CLIENT_ID_CLIENT_SECRET_FORMAT = "%s:%s";
+ private static final String APPLICATION_FORM_URLENCODED_VALUE = "%s;charset=%s".formatted(MediaType.APPLICATION_FORM_URLENCODED_VALUE,
+ StandardCharsets.UTF_8);
+ private static final String REDIRECT_URI = "http://localhost:8080/login/oauth2/code/registration-id";
+ private static final String TOKEN_URI = "https://localhost:8080/oauth2/token";
+ private static final String AUTHORIZATION_URI = "https://localhost:8080/oauth2/authorize";
+ private static final String STATE = "state";
+ private static final String CODE = "code";
+ private static final String OPENID = "openid";
+ private static final String ID_TOKEN = "id_token";
+
+ }
+}
\ No newline at end of file
diff --git a/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/security/JwtResponseTest.java b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/security/JwtResponseTest.java
new file mode 100644
index 0000000000..828baf7cc0
--- /dev/null
+++ b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/security/JwtResponseTest.java
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: MIT
+package com.mercedesbenz.sechub.webui.security;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.ArgumentsProvider;
+import org.junit.jupiter.params.provider.ArgumentsSource;
+import org.opentest4j.TestAbortedException;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.exc.ValueInstantiationException;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.jayway.jsonpath.JsonPath;
+
+class JwtResponseTest {
+
+ private static final String jwtResponseJson;
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ static {
+ try {
+ jwtResponseJson = Files.readString(Paths.get("src/test/resources/jwt-response.json"));
+ } catch (IOException e) {
+ throw new TestAbortedException("Failed to prepare test", e);
+ }
+ }
+
+ @Test
+ void construct_jwt_response_from_valid_json_is_successful() throws JsonProcessingException {
+ // execute
+ JwtResponse jwtResponse = objectMapper.readValue(jwtResponseJson, JwtResponse.class);
+
+ // assert
+ assertThat(jwtResponse).isNotNull();
+ assertThat(jwtResponse.getAccessToken()).isEqualTo(JsonPath.read(jwtResponseJson, "$.access_token"));
+ assertThat(jwtResponse.getRefreshToken()).isEqualTo(JsonPath.read(jwtResponseJson, "$.refresh_token"));
+ assertThat(jwtResponse.getIdToken()).isEqualTo(JsonPath.read(jwtResponseJson, "$.id_token"));
+ int expiresIn = JsonPath.read(jwtResponseJson, "$.expires_in");
+ long expiresInLong = Long.parseLong(String.valueOf(expiresIn));
+ assertThat(jwtResponse.getExpiresIn()).isEqualTo(expiresInLong);
+ assertThat(jwtResponse.getTokenType()).isEqualTo(JsonPath.read(jwtResponseJson, "$.token_type"));
+ }
+
+ @Test
+ void construct_jwt_response_from_valid_json_with_no_refresh_token_is_successful() throws JsonProcessingException {
+ // prepare
+ String jwtResponseJsonWithoutRefreshToken = removeJsonKeyAndValue("refresh_token");
+
+ // execute & assert
+
+ // @formatter:off
+ assertDoesNotThrow(() -> {
+ JwtResponse jwtResponse = objectMapper.readValue(jwtResponseJsonWithoutRefreshToken, JwtResponse.class);
+ assertThat(jwtResponse.getRefreshToken()).isNull();
+ });
+ // @formatter:on
+ }
+
+ @ParameterizedTest
+ @ArgumentsSource(InvalidJwtResponseJsonProvider.class)
+ void construct_jwt_response_from_invalid_json_fails(String invalidJwtResponseJson, String expectedErrMsg) {
+ // execute & assert
+
+ // @formatter:off
+ assertThatThrownBy(() -> objectMapper.readValue(invalidJwtResponseJson, JwtResponse.class))
+ .isInstanceOf(ValueInstantiationException.class)
+ .hasMessageContaining(expectedErrMsg);
+ // @formatter:on
+ }
+
+ private static String removeJsonKeyAndValue(String key) throws JsonProcessingException {
+ ObjectMapper objectMapper = new ObjectMapper();
+ JsonNode rootNode = objectMapper.readTree(jwtResponseJson);
+
+ if (rootNode instanceof ObjectNode) {
+ ((ObjectNode) rootNode).remove(key);
+ } else {
+ throw new IllegalArgumentException("Invalid JSON");
+ }
+
+ return objectMapper.writeValueAsString(rootNode);
+ }
+
+ private static class InvalidJwtResponseJsonProvider implements ArgumentsProvider {
+ @Override
+ public Stream extends Arguments> provideArguments(ExtensionContext extensionContext) throws Exception {
+ return Stream.of(Arguments.of(removeJsonKeyAndValue("access_token"), "access_token must not be null"),
+ Arguments.of(removeJsonKeyAndValue("token_type"), "token_type must not be null"),
+ Arguments.of(removeJsonKeyAndValue("id_token"), "id_token must not be null"),
+ Arguments.of(removeJsonKeyAndValue("expires_in"), "expires_in must not be null"));
+ }
+ }
+}
diff --git a/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/security/OAuth2PropertiesTest.java b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/security/OAuth2PropertiesTest.java
new file mode 100644
index 0000000000..1a748186ee
--- /dev/null
+++ b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/security/OAuth2PropertiesTest.java
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: MIT
+package com.mercedesbenz.sechub.webui.security;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.ArgumentsProvider;
+import org.junit.jupiter.params.provider.ArgumentsSource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.TestPropertySource;
+
+import com.mercedesbenz.sechub.webui.YamlPropertyLoaderFactory;
+
+@SpringBootTest
+@ActiveProfiles("oauth2-enabled")
+@TestPropertySource(locations = "classpath:application-test.yml", factory = YamlPropertyLoaderFactory.class)
+class OAuth2PropertiesTest {
+
+ private static final String ERR_MSG_FORMAT = "The property 'sechub.security.oauth2.%s' must not be null";
+
+ private final OAuth2Properties properties;
+
+ @Autowired
+ OAuth2PropertiesTest(OAuth2Properties properties) {
+ this.properties = properties;
+ }
+
+ @Test
+ void construct_o_auth_2_properties_with_valid_properties_file_succeeds() {
+ assertThat(properties.getClientId()).isEqualTo("client-id");
+ assertThat(properties.getClientSecret()).isEqualTo("client-secret");
+ assertThat(properties.getProvider()).isEqualTo("provider");
+ assertThat(properties.getRedirectUri()).isEqualTo("redirect-uri");
+ assertThat(properties.getIssuerUri()).isEqualTo("issuer-uri");
+ assertThat(properties.getAuthorizationUri()).isEqualTo("authorization-uri");
+ assertThat(properties.getTokenUri()).isEqualTo("token-uri");
+ assertThat(properties.getUserInfoUri()).isEqualTo("user-info-uri");
+ assertThat(properties.getJwkSetUri()).isEqualTo("jwk-set-uri");
+ }
+
+ /* @formatter:off */
+ @ParameterizedTest
+ @ArgumentsSource(InvalidOAuth2PropertiesProvider.class)
+ void construct_o_auth_2_properties_with_null_property_fails(String clientId,
+ String clientSecret,
+ String provider,
+ String redirectUri,
+ String issuerUri,
+ String authorizationUri,
+ String tokenUri,
+ String userInfoUri,
+ String jwkSetUri,
+ String errMsg) {
+ assertThatThrownBy(() -> new OAuth2Properties(clientId, clientSecret, provider, redirectUri, issuerUri, authorizationUri, tokenUri, userInfoUri, jwkSetUri))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessageContaining(errMsg);
+ }
+ /* @formatter:on */
+
+ @Configuration
+ @Import(OAuth2PropertiesConfig.class)
+ static class TestConfig {
+ }
+
+ private static class InvalidOAuth2PropertiesProvider implements ArgumentsProvider {
+ @Override
+ public Stream extends Arguments> provideArguments(ExtensionContext extensionContext) throws Exception {
+ /* @formatter:off */
+ return Stream.of(
+ Arguments.of(null, "client-secret", "provider", "redirect-uri", "issuer-uri", "authorization-uri", "token-uri", "user-info-uri", "jwk-set-uri", ERR_MSG_FORMAT.formatted("client-id")),
+ Arguments.of("client-id", null, "provider", "redirect-uri", "issuer-uri", "authorization-uri", "token-uri", "user-info-uri", "jwk-set-uri", ERR_MSG_FORMAT.formatted("client-secret")),
+ Arguments.of("client-id", "client-secret", null, "redirect-uri", "issuer-uri", "authorization-uri", "token-uri", "user-info-uri", "jwk-set-uri", ERR_MSG_FORMAT.formatted("provider")),
+ Arguments.of("client-id", "client-secret", "provider", null, "issuer-uri", "authorization-uri", "token-uri", "user-info-uri", "jwk-set-uri", ERR_MSG_FORMAT.formatted("redirect-uri")),
+ Arguments.of("client-id", "client-secret", "provider", "redirect-uri", null, "authorization-uri", "token-uri", "user-info-uri", "jwk-set-uri", ERR_MSG_FORMAT.formatted("issuer-uri")),
+ Arguments.of("client-id", "client-secret", "provider", "redirect-uri", "issuer-uri", null, "token-uri", "user-info-uri", "jwk-set-uri", ERR_MSG_FORMAT.formatted("authorization-uri")),
+ Arguments.of("client-id", "client-secret", "provider", "redirect-uri", "issuer-uri", "authorization-uri", null, "user-info-uri", "jwk-set-uri", ERR_MSG_FORMAT.formatted("token-uri")),
+ Arguments.of("client-id", "client-secret", "provider", "redirect-uri", "issuer-uri", "authorization-uri", "token-uri", null, "jwk-set-uri", ERR_MSG_FORMAT.formatted("user-info-uri")),
+ Arguments.of("client-id", "client-secret", "provider", "redirect-uri", "issuer-uri", "authorization-uri", "token-uri", "user-info-uri", null, ERR_MSG_FORMAT.formatted("jwk-set-uri")));
+ /* @formatter:on */
+ }
+ }
+}
diff --git a/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/security/SecurityTestConfiguration.java b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/security/SecurityTestConfiguration.java
new file mode 100644
index 0000000000..5306eaecd7
--- /dev/null
+++ b/sechub-webui/src/test/java/com/mercedesbenz/sechub/webui/security/SecurityTestConfiguration.java
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: MIT
+package com.mercedesbenz.sechub.webui.security;
+
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Import;
+
+@TestConfiguration
+@Import({ SecurityConfiguration.class, OAuth2PropertiesConfig.class })
+public class SecurityTestConfiguration {
+}
diff --git a/sechub-webui/src/test/resources/application-test.yml b/sechub-webui/src/test/resources/application-test.yml
new file mode 100644
index 0000000000..211423ae47
--- /dev/null
+++ b/sechub-webui/src/test/resources/application-test.yml
@@ -0,0 +1,18 @@
+# SPDX-License-Identifier: MIT
+
+sechub:
+ security:
+ oauth2:
+ client-id: client-id
+ client-secret: client-secret
+ provider: provider
+ redirect-uri: redirect-uri
+ issuer-uri: issuer-uri
+ authorization-uri: authorization-uri
+ token-uri: token-uri
+ user-info-uri: user-info-uri
+ jwk-set-uri: jwk-set-uri
+
+server:
+ ssl:
+ enabled: false
\ No newline at end of file
diff --git a/sechub-webui/src/test/resources/jwt-response.json b/sechub-webui/src/test/resources/jwt-response.json
new file mode 100644
index 0000000000..2bec6853e9
--- /dev/null
+++ b/sechub-webui/src/test/resources/jwt-response.json
@@ -0,0 +1,7 @@
+{
+ "access_token":"access_token",
+ "token_type":"Bearer",
+ "expires_in":3600,
+ "refresh_token": "refresh_token",
+ "id_token":"id_token"
+}
\ No newline at end of file