Skip to content

Commit 708523e

Browse files
Theo-lbgCopilot
andcommitted
feat(security): enhance security filter chain and add custom exception handling
Co-authored-by: Copilot <copilot@github.com>
1 parent 3ca0012 commit 708523e

4 files changed

Lines changed: 156 additions & 23 deletions

File tree

src/main/java/com/xpeho/spring_boot_java_random_user/config/SecurityConfig.java

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.xpeho.spring_boot_java_random_user.config;
22

3+
import jakarta.servlet.http.HttpServletRequest;
34
import org.springframework.context.annotation.Bean;
45
import org.springframework.context.annotation.Configuration;
56
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.http.HttpHeaders;
68
import org.springframework.http.HttpMethod;
79
import org.springframework.security.core.userdetails.User;
810
import org.springframework.security.core.userdetails.UserDetails;
@@ -15,11 +17,16 @@
1517
import org.springframework.security.config.http.SessionCreationPolicy;
1618
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
1719
import org.springframework.security.web.SecurityFilterChain;
20+
import org.springframework.security.web.context.NullSecurityContextRepository;
1821

1922
@Configuration
2023
@EnableWebSecurity
2124
public class SecurityConfig {
2225

26+
private static final String RANDOM_USERS_PATH = "/random-users/**";
27+
private static final String RANDOM_USERS_PREFIX = "/random-users";
28+
private static final String ADMIN_ROLE = "ADMIN";
29+
2330
@Value("${app.security.admin.username}")
2431
private String adminUsername;
2532

@@ -39,35 +46,50 @@ public class SecurityConfig {
3946
private String testPassword;
4047

4148
@Bean
42-
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
43-
http
44-
.csrf(csrf -> csrf.ignoringRequestMatchers("/random-users/**"))
45-
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
46-
.httpBasic(Customizer.withDefaults())
47-
.authorizeHttpRequests(auth -> auth
48-
.requestMatchers(
49-
"/api/**",
50-
"/swagger-ui/**",
51-
"/swagger-ui.html",
52-
"/v3/api-docs/**",
53-
"/actuator/health"
54-
).permitAll()
55-
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
56-
.requestMatchers(HttpMethod.GET, "/random-users/**").hasAnyRole("ADMIN", "USER", "TEST")
57-
.requestMatchers(HttpMethod.POST, "/random-users/**").hasRole("ADMIN")
58-
.requestMatchers(HttpMethod.PUT, "/random-users/**").hasRole("ADMIN")
59-
.requestMatchers(HttpMethod.DELETE, "/random-users/**").hasRole("ADMIN")
60-
.anyRequest().authenticated()
61-
);
62-
63-
return http.build();
49+
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
50+
try {
51+
return http
52+
.csrf(csrf -> csrf.ignoringRequestMatchers(this::isBasicAuthRequest))
53+
.securityContext(context -> context.securityContextRepository(new NullSecurityContextRepository()))
54+
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
55+
.httpBasic(Customizer.withDefaults())
56+
.authorizeHttpRequests(auth -> auth
57+
.requestMatchers(getPublicEndpoints()).permitAll()
58+
.requestMatchers(HttpMethod.GET, RANDOM_USERS_PATH).hasAnyRole(ADMIN_ROLE, "USER", "TEST")
59+
.requestMatchers(HttpMethod.POST, RANDOM_USERS_PATH).hasRole(ADMIN_ROLE)
60+
.requestMatchers(HttpMethod.PUT, RANDOM_USERS_PATH).hasRole(ADMIN_ROLE)
61+
.requestMatchers(HttpMethod.DELETE, RANDOM_USERS_PATH).hasRole(ADMIN_ROLE)
62+
.anyRequest().authenticated()
63+
)
64+
.build();
65+
} catch (Exception e) {
66+
throw new SecurityConfigurationException("Failed to build Spring Security filter chain", e);
67+
}
68+
}
69+
70+
71+
private boolean isBasicAuthRequest(HttpServletRequest request) {
72+
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
73+
String servletPath = request.getServletPath();
74+
boolean isRandomUsersPath = servletPath != null && servletPath.startsWith(RANDOM_USERS_PREFIX);
75+
return isRandomUsersPath && authHeader != null && authHeader.startsWith("Basic ");
76+
}
77+
78+
private String[] getPublicEndpoints() {
79+
return new String[]{
80+
"/api/**",
81+
"/swagger-ui/**",
82+
"/swagger-ui.html",
83+
"/v3/api-docs/**",
84+
"/actuator/health"
85+
};
6486
}
6587

6688
@Bean
6789
UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
6890
UserDetails admin = User.withUsername(adminUsername)
6991
.password(passwordEncoder.encode(adminPassword))
70-
.roles("ADMIN")
92+
.roles(ADMIN_ROLE)
7193
.build();
7294

7395
UserDetails user = User.withUsername(userUsername)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.xpeho.spring_boot_java_random_user.config;
2+
3+
public class SecurityConfigurationException extends RuntimeException {
4+
5+
public SecurityConfigurationException(String message, Throwable cause) {
6+
super(message, cause);
7+
}
8+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.xpeho.spring_boot_java_random_user.config;
2+
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.Test;
5+
import org.springframework.mock.web.MockHttpServletRequest;
6+
import org.springframework.security.core.userdetails.UserDetails;
7+
import org.springframework.security.core.userdetails.UserDetailsService;
8+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
9+
import org.springframework.security.crypto.password.PasswordEncoder;
10+
import org.springframework.test.util.ReflectionTestUtils;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
14+
class SecurityConfigTest {
15+
16+
private final SecurityConfig securityConfig = new SecurityConfig();
17+
18+
@BeforeEach
19+
void setUp() {
20+
ReflectionTestUtils.setField(securityConfig, "adminUsername", "admin");
21+
ReflectionTestUtils.setField(securityConfig, "adminPassword", "admin123");
22+
ReflectionTestUtils.setField(securityConfig, "userUsername", "apiuser");
23+
ReflectionTestUtils.setField(securityConfig, "userPassword", "changeit");
24+
ReflectionTestUtils.setField(securityConfig, "testUsername", "testuser");
25+
ReflectionTestUtils.setField(securityConfig, "testPassword", "testpass");
26+
}
27+
28+
@Test
29+
void shouldEncodePasswordsWithBcrypt() {
30+
PasswordEncoder passwordEncoder = securityConfig.passwordEncoder();
31+
32+
assertThat(passwordEncoder).isInstanceOf(BCryptPasswordEncoder.class);
33+
assertThat(passwordEncoder.matches("admin123", passwordEncoder.encode("admin123"))).isTrue();
34+
}
35+
36+
@Test
37+
void shouldCreateInMemoryUsersWithExpectedRoles() {
38+
PasswordEncoder passwordEncoder = securityConfig.passwordEncoder();
39+
40+
UserDetailsService userDetailsService = securityConfig.userDetailsService(passwordEncoder);
41+
42+
UserDetails admin = userDetailsService.loadUserByUsername("admin");
43+
UserDetails user = userDetailsService.loadUserByUsername("apiuser");
44+
UserDetails test = userDetailsService.loadUserByUsername("testuser");
45+
46+
assertThat(admin.getAuthorities()).extracting("authority").containsExactly("ROLE_ADMIN");
47+
assertThat(user.getAuthorities()).extracting("authority").containsExactly("ROLE_USER");
48+
assertThat(test.getAuthorities()).extracting("authority").containsExactly("ROLE_TEST");
49+
}
50+
51+
@Test
52+
void shouldRecognizeBasicAuthRequestsOnRandomUsersPath() {
53+
MockHttpServletRequest request = new MockHttpServletRequest();
54+
request.setServletPath("/random-users/123");
55+
request.addHeader("Authorization", "Basic dGVzdDp0ZXN0");
56+
57+
boolean result = ReflectionTestUtils.invokeMethod(securityConfig, "isBasicAuthRequest", request);
58+
59+
assertThat(result).isTrue();
60+
}
61+
62+
@Test
63+
void shouldRejectNonBasicAuthOrNonRandomUsersRequests() {
64+
MockHttpServletRequest request = new MockHttpServletRequest();
65+
request.setServletPath("/health");
66+
request.addHeader("Authorization", "Bearer token");
67+
68+
boolean result = ReflectionTestUtils.invokeMethod(securityConfig, "isBasicAuthRequest", request);
69+
70+
assertThat(result).isFalse();
71+
}
72+
73+
@Test
74+
void shouldExposePublicEndpoints() {
75+
String[] endpoints = ReflectionTestUtils.invokeMethod(securityConfig, "getPublicEndpoints");
76+
77+
assertThat(endpoints).contains(
78+
"/api/**",
79+
"/swagger-ui/**",
80+
"/swagger-ui.html",
81+
"/v3/api-docs/**",
82+
"/actuator/health"
83+
);
84+
}
85+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.xpeho.spring_boot_java_random_user.config;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.assertj.core.api.Assertions.assertThat;
6+
7+
class SecurityConfigurationExceptionTest {
8+
9+
@Test
10+
void shouldExposeMessageAndCause() {
11+
IllegalStateException cause = new IllegalStateException("boom");
12+
SecurityConfigurationException exception = new SecurityConfigurationException("Failed to build Spring Security filter chain", cause);
13+
14+
assertThat(exception)
15+
.hasMessage("Failed to build Spring Security filter chain")
16+
.hasCause(cause);
17+
}
18+
}

0 commit comments

Comments
 (0)