diff --git a/apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/config/WebSecurityConfig.java b/apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/config/WebSecurityConfig.java
index 5e303cf1..0358dd66 100644
--- a/apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/config/WebSecurityConfig.java
+++ b/apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/config/WebSecurityConfig.java
@@ -1,8 +1,7 @@
package com.acme.backend.springboot.users.config;
-import com.acme.backend.springboot.users.support.access.AccessController;
-import com.acme.backend.springboot.users.support.keycloak.KeycloakJwtAuthenticationConverter;
-import lombok.RequiredArgsConstructor;
+import java.util.List;
+
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -12,80 +11,82 @@
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
-import java.util.List;
+import com.acme.backend.springboot.users.support.access.AccessController;
+import com.acme.backend.springboot.users.support.keycloak.KeycloakJwtAuthenticationConverter;
+
+import lombok.RequiredArgsConstructor;
/**
- * Configuration applied on all web endpoints defined for this
- * application. Any configuration on specific resources is applied
- * in addition to these global rules.
+ * Configuration applied on all web endpoints defined for this application. Any configuration on specific resources is applied in addition to these global
+ * rules.
*/
@Configuration
@RequiredArgsConstructor
-class WebSecurityConfig {
-
- private final KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter;
-
- /**
- * Configures basic security handler per HTTP session.
- *
- *
- * - Stateless session (no session kept server-side)
- * - CORS set up
- * - Require the role "ACCESS" for all api paths
- * - JWT converted into Spring token
- *
- *
- * @param http security configuration
- * @throws Exception any error
- */
- @Bean
- public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
-
- http.sessionManagement(smc -> {
- smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
- });
- http.cors(this::configureCors);
- http.authorizeHttpRequests(ahrc -> {
- // declarative route configuration
- // .mvcMatchers("/api").hasAuthority("ROLE_ACCESS")
- ahrc.requestMatchers("/api/**").access(AccessController::checkAccess);
- // add additional routes
- ahrc.anyRequest().fullyAuthenticated(); //
- });
- http.oauth2ResourceServer(arsc -> {
- arsc.jwt().jwtAuthenticationConverter(keycloakJwtAuthenticationConverter);
- });
-
- return http.build();
- }
-
- @Bean
- AccessController accessController() {
- return new AccessController();
- }
-
- /**
- * Configures CORS to allow requests from localhost:30000
- *
- * @param cors mutable cors configuration
- */
- protected void configureCors(CorsConfigurer cors) {
-
- UrlBasedCorsConfigurationSource defaultUrlBasedCorsConfigSource = new UrlBasedCorsConfigurationSource();
- CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
- corsConfiguration.addAllowedOrigin("https://apps.acme.test:4443");
- List.of("GET", "POST", "PUT", "DELETE").forEach(corsConfiguration::addAllowedMethod);
- defaultUrlBasedCorsConfigSource.registerCorsConfiguration("/api/**", corsConfiguration);
-
- cors.configurationSource(req -> {
-
- CorsConfiguration config = new CorsConfiguration();
-
- config = config.combine(defaultUrlBasedCorsConfigSource.getCorsConfiguration(req));
-
- // check if request Header "origin" is in white-list -> dynamically generate cors config
-
- return config;
- });
- }
+public class WebSecurityConfig {
+
+ private final KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter;
+
+ /**
+ * Configures basic security handler per HTTP session.
+ *
+ *
+ * - Stateless session (no session kept server-side)
+ * - CORS set up
+ * - Require the role "ACCESS" for all api paths
+ * - JWT converted into Spring token
+ *
+ *
+ * @param http security configuration
+ * @throws Exception any error
+ */
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+
+ http.sessionManagement(smc -> {
+ smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
+ });
+ http.cors(this::configureCors);
+ http.authorizeHttpRequests(ahrc -> {
+ // declarative route configuration
+ // .mvcMatchers("/api").hasAuthority("ROLE_ACCESS")
+ ahrc.requestMatchers("/api/**").access(AccessController::checkAccess);
+ // add additional routes
+ ahrc.anyRequest().fullyAuthenticated(); //
+ });
+ http.oauth2ResourceServer(arsc -> {
+ arsc.jwt().jwtAuthenticationConverter(keycloakJwtAuthenticationConverter);
+ });
+
+ return http.build();
+ }
+
+ @Bean
+ AccessController accessController() {
+ return new AccessController();
+ }
+
+ /**
+ * Configures CORS to allow requests from localhost:30000
+ *
+ * @param cors mutable cors configuration
+ */
+ protected void configureCors(CorsConfigurer cors) {
+
+ UrlBasedCorsConfigurationSource defaultUrlBasedCorsConfigSource = new UrlBasedCorsConfigurationSource();
+ CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
+ corsConfiguration.addAllowedOrigin("https://apps.acme.test:4443");
+ List.of("GET", "POST", "PUT", "DELETE").forEach(corsConfiguration::addAllowedMethod);
+ defaultUrlBasedCorsConfigSource.registerCorsConfiguration("/api/**", corsConfiguration);
+
+ cors.configurationSource(req -> {
+
+ CorsConfiguration config = new CorsConfiguration();
+
+ config = config.combine(defaultUrlBasedCorsConfigSource.getCorsConfiguration(req));
+
+ // check if request Header "origin" is in white-list -> dynamically generate cors config
+
+ return config;
+ });
+ }
}
\ No newline at end of file
diff --git a/apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/support/access/AccessController.java b/apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/support/access/AccessController.java
index 03f15cda..79df696b 100644
--- a/apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/support/access/AccessController.java
+++ b/apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/support/access/AccessController.java
@@ -1,11 +1,13 @@
package com.acme.backend.springboot.users.support.access;
-import lombok.extern.slf4j.Slf4j;
+import java.util.function.Supplier;
+
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
-import java.util.function.Supplier;
+import lombok.extern.slf4j.Slf4j;
/**
* Example for generic custom access checks on request level.
@@ -13,14 +15,17 @@
@Slf4j
public class AccessController {
- private static final AuthorizationDecision GRANTED = new AuthorizationDecision(true);
- private static final AuthorizationDecision DENIED = new AuthorizationDecision(false);
-
- public static AuthorizationDecision checkAccess(Supplier authentication, RequestAuthorizationContext requestContext) {
+ private static final AuthorizationDecision GRANTED = new AuthorizationDecision(true);
+ private static final AuthorizationDecision DENIED = new AuthorizationDecision(false);
- var auth = authentication.get();
- log.info("Check access for username={} path={}", auth.getName(), requestContext.getRequest().getRequestURI());
+ public static AuthorizationDecision checkAccess(Supplier authentication, RequestAuthorizationContext requestContext) {
- return GRANTED;
- }
+ var auth = authentication.get();
+ if (auth == null) {
+ log.warn("Authentication provider returned null authentication");
+ return DENIED;
+ }
+ log.info("Check access for username={} path={}", auth.getName(), requestContext.getRequest().getRequestURI());
+ return auth.getAuthorities().stream().map(GrantedAuthority::getAuthority).filter("ROLE_ACCESS"::equals).count() > 0 ? GRANTED : DENIED;
+ }
}
diff --git a/apps/backend-api-springboot3/src/test/java/com/acme/backend/springboot/users/BackendApiSpringboot3AppTests.java b/apps/backend-api-springboot3/src/test/java/com/acme/backend/springboot/users/BackendApiSpringboot3AppTests.java
index 2cdd42de..31b6b096 100644
--- a/apps/backend-api-springboot3/src/test/java/com/acme/backend/springboot/users/BackendApiSpringboot3AppTests.java
+++ b/apps/backend-api-springboot3/src/test/java/com/acme/backend/springboot/users/BackendApiSpringboot3AppTests.java
@@ -1,13 +1,50 @@
package com.acme.backend.springboot.users;
+import static org.hamcrest.CoreMatchers.is;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+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.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.security.test.context.support.WithAnonymousUser;
+import org.springframework.test.web.servlet.MockMvc;
+
+import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims;
+import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockJwtAuth;
@SpringBootTest
+@AutoConfigureMockMvc
class BackendApiSpringboot3AppTests {
+ @Autowired
+ MockMvc api;
+
+ @Test
+ @WithAnonymousUser
+ void givenRequestIsAnonymous_whengetUsersMe_thenUnauthorized() throws Exception {
+ api.perform(get("/api/users/me")).andExpectAll(status().isUnauthorized());
+ }
+
+ @Test
+ @WithMockJwtAuth(claims = @OpenIdClaims(sub = "Tonton Pirate"))
+ void givenUserIsNotGrantedWithAccess_whengetUsersMe_thenForbidden() throws Exception {
+ // @formatter:off
+ api.perform(get("/api/users/me"))
+ .andExpect(status().isForbidden());
+ // @formatter:on
+ }
+
@Test
- void contextLoads() {
+ @WithMockJwtAuth(authorities = { "ROLE_ACCESS" }, claims = @OpenIdClaims(sub = "Tonton Pirate"))
+ void givenUserIsGrantedWithAccess_whengetUsersMe_thenOk() throws Exception {
+ // @formatter:off
+ api.perform(get("/api/users/me"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.message", is("Hello Tonton Pirate")));
+ // @formatter:on
}
}
diff --git a/apps/backend-api-springboot3/src/test/java/com/acme/backend/springboot/users/web/UsersControllerTest.java b/apps/backend-api-springboot3/src/test/java/com/acme/backend/springboot/users/web/UsersControllerTest.java
index df418944..2cec6ae8 100644
--- a/apps/backend-api-springboot3/src/test/java/com/acme/backend/springboot/users/web/UsersControllerTest.java
+++ b/apps/backend-api-springboot3/src/test/java/com/acme/backend/springboot/users/web/UsersControllerTest.java
@@ -8,17 +8,24 @@
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.WithAnonymousUser;
import org.springframework.test.web.servlet.MockMvc;
+import com.acme.backend.springboot.users.config.WebSecurityConfig;
+import com.acme.backend.springboot.users.support.keycloak.KeycloakJwtAuthenticationConverter;
import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims;
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockJwtAuth;
@WebMvcTest(controllers = UsersController.class)
+@Import({ WebSecurityConfig.class, KeycloakJwtAuthenticationConverter.class })
class UsersControllerTest {
+
@Autowired
MockMvc api;
@Test
+ @WithAnonymousUser
void givenRequestIsAnonymous_whengetUsersMe_thenUnauthorized() throws Exception {
api.perform(get("/api/users/me")).andExpectAll(status().isUnauthorized());
}
@@ -28,8 +35,7 @@ void givenRequestIsAnonymous_whengetUsersMe_thenUnauthorized() throws Exception
void givenUserIsNotGrantedWithAccess_whengetUsersMe_thenForbidden() throws Exception {
// @formatter:off
api.perform(get("/api/users/me"))
- // should be forbidden as user does is not granted with "ROLE_ACCESS"
- .andExpect(status().isOk());
+ .andExpect(status().isForbidden());
// @formatter:on
}