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.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* Spring Security configuration for StockEase application.
*
* Orchestrates:
* - Authentication: JWT token-based stateless security
* - Authorization: Role-based access control (ADMIN, USER)
* - CORS: Allow local dev frontend and production deployment
* - Exception handling: Custom 401/403 error responses
*
* Configuration layers:
* 1. SecurityFilterChain: HTTP security rules and filter ordering
* 2. PasswordEncoder: BCrypt hashing for credentials
* 3. AuthenticationManager: Credential validation during login
* 4. CorsConfiguration: Cross-origin request handling
*
* @author Team StockEase
* @version 1.0
* @since 2025-01-01
*/
@Configuration
@EnableMethodSecurity
@org.springframework.context.annotation.Profile("!docs")
public class SecurityConfig {
/**
* JWT filter for extracting and validating tokens in request chain.
*/
private final JwtFilter jwtFilter;
/**
* Custom entry point handler for 401 Unauthorized responses.
*/
private final AuthenticationEntryPoint customAuthenticationEntryPoint;
/**
* Constructs security config with JWT and authentication entry point.
*
* Dependencies injected by Spring for filter chain integration.
*
* @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;
}
/**
* Configures password encoder bean for credential hashing.
*
* Uses BCrypt with 10 rounds (default strength, ~0.5s per hash).
* Applied during:
* - User registration: hash plain password before storage
* - Login: hash submitted password and compare with stored hash
*
* @return BCryptPasswordEncoder instance for bean container
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* Exposes Spring's default AuthenticationManager as bean.
*
* Used by AuthController during login POST request:
* 1. Receives username/password from LoginRequest DTO
* 2. Delegates to AuthenticationManager for credential validation
* 3. Returns authentication token if credentials valid
* 4. Throws BadCredentialsException if invalid
*
* @param config Spring Security authentication configuration
* @return authenticated AuthenticationManager bean
* @throws Exception if bean creation fails
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
/**
* Configures HTTP security filter chain for request authorization.
*
* Filter chain order (Spring Security):
* 1. CorsFilter - allow cross-origin requests
* 2. JwtFilter - extract and validate token, populate SecurityContext
* 3. ExceptionHandlingFilter - convert security exceptions to HTTP responses
* 4. AuthorizationFilter - enforce endpoint authorization rules
* 5. Controller routing
*
* Authorization rules:
* - Public: /api/health, /actuator/health/**, /api/auth/login (no auth required)
* - Admin: POST /api/products, DELETE /api/products/** (ADMIN role only)
* - Admin+User: PUT /api/products/**, GET /api/products**, (ADMIN or USER role)
* - Catch-all: anyRequest().authenticated() (deny by default)
*
* Exception handling:
* - 401 Unauthorized: custom entry point returns JSON error (no token/invalid token)
* - 403 Forbidden: access denied handler returns JSON error (valid token but insufficient role)
*
* CSRF: Disabled (stateless JWT doesn't need CSRF tokens, no session cookies)
* Session: Stateless (JWT-based, no JSESSIONID, prevents session fixation attacks)
*
* @param http HttpSecurity builder for fluent configuration
* @return configured SecurityFilterChain bean
* @throws Exception if configuration fails
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// Disable CSRF protection (stateless JWT doesn't need CSRF tokens)
.csrf(csrf -> csrf.disable())
// Enable CORS with custom configuration
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // Add CORS configuration
// Configure endpoint authorization
.authorizeHttpRequests(auth -> auth
// Allow public access to health check
.requestMatchers("/api/health").permitAll()
// Allow actuator health endpoints and subpaths (GET only) for external health probes
// e.g. GET /actuator/health, /actuator/health/readiness, /actuator/health/liveness
.requestMatchers(HttpMethod.GET, "/actuator/health/**").permitAll()
// Public endpoint for login
.requestMatchers(HttpMethod.POST, "/api/auth/login").permitAll()
// Admin-specific permissions
.requestMatchers(HttpMethod.POST, "/api/products").hasRole("ADMIN")
.requestMatchers(HttpMethod.PUT, "/api/products/**").hasAnyRole("ADMIN", "USER")
.requestMatchers(HttpMethod.DELETE, "/api/products/**").hasRole("ADMIN")
// User-specific permissions
.requestMatchers(HttpMethod.GET, "/api/products/**").hasAnyRole("ADMIN", "USER")
.requestMatchers(HttpMethod.GET, "/api/products").hasAnyRole("ADMIN", "USER")
// Deny all other requests
.anyRequest().authenticated()
)
// Configure exception handling for authentication/authorization failures
.exceptionHandling(exceptions -> exceptions
// Handle 401 Unauthorized (no token or invalid token)
.authenticationEntryPoint(customAuthenticationEntryPoint)
// Handle 403 Forbidden (valid token but insufficient role)
.accessDeniedHandler((request, response, accessDeniedException) -> {
System.out.println("Access Denied Handler triggered for user: " + request.getUserPrincipal());
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json");
response.getWriter().write("{\"error\": \"You are not authorized to perform this action.\"}");
})
)
// Stateless session management (no JSESSIONID cookie, JWT-based instead)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// Add JWT filter before standard username/password filter
// Order: CorsFilter → JwtFilter → authentication checks → controller
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
/**
* Configures CORS filter bean for Spring container.
*
* Applies CORS headers to all responses matching "/**" path pattern.
* Used by browser for preflight requests (OPTIONS method).
*
* @return CorsFilter bean registered in Spring container
*/
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration());
return new CorsFilter(source);
}
/**
* Defines CORS policy (allowed origins, methods, headers, credentials).
*
* Allows:
* - Origins: http://localhost:5173 (dev), https://stockeasefrontend.vercel.app (prod)
* - Methods: GET, POST, PUT, DELETE, OPTIONS (REST operations + preflight)
* - Headers: Authorization (JWT token), Cache-Control, Content-Type
* - Credentials: true (allows cookies alongside tokens if needed)
*
* Prevents CORS errors in browser when frontend calls backend API.
*
* @return CorsConfiguration with policy rules
*/
private CorsConfiguration corsConfiguration() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList(
"http://localhost:5173", // Allow local development origin
"https://stockeasefrontend.vercel.app/" // Allow deployed frontend
));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); // Allow methods
config.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type")); // Allow headers
config.setAllowCredentials(true); // Allow cookies and credentials
return config;
}
/**
* Creates CORS configuration source for security filter chain.
*
* Wraps corsConfiguration() to be compatible with Spring Security's
* HttpSecurity.cors() method which expects CorsConfigurationSource.
*
* @return URL-based CORS configuration source for "/**" paths
*/
private UrlBasedCorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration());
return source;
}
}