Skip to content

Commit 7ca2ec9

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 7ca2ec9

4 files changed

Lines changed: 154 additions & 23 deletions

File tree

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

Lines changed: 43 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;
@@ -13,13 +15,19 @@
1315
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
1416
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
1517
import org.springframework.security.config.http.SessionCreationPolicy;
18+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
1619
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
1720
import org.springframework.security.web.SecurityFilterChain;
21+
import org.springframework.security.web.context.NullSecurityContextRepository;
1822

1923
@Configuration
2024
@EnableWebSecurity
2125
public class SecurityConfig {
2226

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

@@ -39,35 +47,47 @@ public class SecurityConfig {
3947
private String testPassword;
4048

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

6686
@Bean
6787
UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
6888
UserDetails admin = User.withUsername(adminUsername)
6989
.password(passwordEncoder.encode(adminPassword))
70-
.roles("ADMIN")
90+
.roles(ADMIN_ROLE)
7191
.build();
7292

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

0 commit comments

Comments
 (0)