SecurityConfig.java
package com.stocks.stockease.security;
import java.util.Arrays;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.jspecify.annotations.NonNull;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
/**
* Spring Security configuration defining authentication, authorization, CORS, and session policies.
* Uses stateless JWT-based authentication with role-based access control.
*/
@Configuration
@EnableMethodSecurity
@org.springframework.context.annotation.Profile("!docs")
public class SecurityConfig {
private final JwtFilter jwtFilter;
private final AuthenticationEntryPoint customAuthenticationEntryPoint;
/**
* Constructs security config with the JWT filter and custom authentication entry point.
*
* @param jwtFilter validates JWT tokens in request headers
* @param customAuthenticationEntryPoint sends custom 401 error responses
*/
public SecurityConfig(JwtFilter jwtFilter, AuthenticationEntryPoint customAuthenticationEntryPoint) {
this.jwtFilter = jwtFilter;
this.customAuthenticationEntryPoint = customAuthenticationEntryPoint;
}
/**
* Returns a BCrypt password encoder bean for credential hashing.
*
* @return BCryptPasswordEncoder instance
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* Exposes Spring's default AuthenticationManager as a bean for login credential validation.
*
* @param config Spring Security authentication configuration
* @return AuthenticationManager bean
* @throws Exception if bean creation fails
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
/**
* Configures the HTTP security filter chain with authorization rules, CORS, and exception handling.
*
* @param http HttpSecurity builder
* @return configured SecurityFilterChain bean
* @throws Exception if configuration fails
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// Disabled: stateless JWT authentication does not require CSRF tokens
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/health").permitAll()
.requestMatchers("/health", "/health/").permitAll()
.requestMatchers(HttpMethod.GET, "/actuator/health/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/auth/login").permitAll()
.requestMatchers(HttpMethod.POST, "/api/products").hasRole("ADMIN")
.requestMatchers(HttpMethod.PUT, "/api/products/**").hasAnyRole("ADMIN", "USER")
.requestMatchers(HttpMethod.DELETE, "/api/products/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/products/**").hasAnyRole("ADMIN", "USER")
.requestMatchers(HttpMethod.GET, "/api/products").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated()
)
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json");
response.getWriter().write("{\"error\": \"You are not authorized to perform this action.\"}");
})
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
/**
* Defines CORS policy with allowed origins, methods, headers, and credentials.
*
* @return CorsConfiguration with policy rules
*/
private @NonNull CorsConfiguration corsConfiguration() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList(
"http://localhost:5173",
"https://stockeasefrontend.vercel.app/"
));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type"));
config.setAllowCredentials(true);
return config;
}
/**
* Returns a CORS configuration source wrapping the CORS policy for use by the security filter chain.
*
* @return URL-based CORS configuration source applied to all paths
*/
private UrlBasedCorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration());
return source;
}
}