Feature 1: Security Hardening
Purpose: Fix critical security vulnerabilities identified in code analysis.
Priority: CRITICAL (fixes production blockers) Estimated Time: 4-6 hours Dependencies: None Status: ✅ COMPLETE (2026-01-04)
Overview
This feature fixes 7 critical security vulnerabilities that prevent production deployment:
- CORS wildcard configuration
- Missing JWT token validation
- CSRF disabled globally
- No password validation
- Missing rate limiting
- Hardcoded secrets
- Missing input validation
Bundled Enhancements (to avoid revisiting files):
- Centralized exception handling
- Optional.get() safety fixes
- Transaction annotation fixes
- Production infrastructure (Actuator, Hikari, logging)
- Unit tests for all new code
Prerequisites
Before starting:
- Clean branch: Start from main/develop
- Build passing:
./gradlew buildsucceeds - Database running: PostgreSQL available
- Environment file:
.envconfigured from.env.example
Step 1: CORS Configuration
Files:
server/src/main/java/com/saas/springular/common/security/WebSecurityConfig.javaserver/src/main/resources/application.yml
1.1 Update CORS Method
Replace the corsConfigurationSource() method to:
- Use
allowedOriginPatternsinstead of wildcard (*) - Enable
allowCredentials(true)for cookies/auth - Add CSRF headers to
allowedHeaders:X-XSRF-TOKEN,X-CSRF-TOKEN - Expose rate limit headers:
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset - Set
maxAge(3600L)to cache preflight for 1 hour
1.2 Add Origins to application.yml
Add auth.authorized-redirect-origins list with localhost:4200, localhost:8081, and ${FRONTEND_CLIENT_HOST}.
Step 2: Enable CSRF Protection
Files:
server/src/main/java/com/saas/springular/common/security/WebSecurityConfig.javaclient/src/app/core/interceptor/auth.interceptor.ts
2.1 Enable CSRF in Security Config
Replace .csrf(AbstractHttpConfigurer::disable) with:
CookieCsrfTokenRepository.withHttpOnlyFalse()for cookie-based tokensignoringRequestMatchers("/auth/**", "/webhook", "/webhook/**")to exempt auth and webhooks
Note: /auth/** must be excluded because login/register requests don't have a CSRF token yet.
2.2 Add CSRF Header to Frontend Interceptor
Add helper functions to:
- Get CSRF token from cookies (
XSRF-TOKEN) - Check if request method requires CSRF (
POST,PUT,DELETE,PATCH) - Add
X-XSRF-TOKENheader for state-changing requests
Step 3: JWT Token Validation
Files:
server/src/main/java/com/saas/springular/common/security/JwtTokenProvider.javaserver/src/main/java/com/saas/springular/common/security/JwtTokenAuthenticationFilter.java
3.1 Add Validation Method to JwtTokenProvider
Add validateToken(String token) method that catches and logs:
ExpiredJwtExceptionUnsupportedJwtExceptionMalformedJwtExceptionSignatureExceptionIllegalArgumentException
3.2 Use Validation in JwtTokenAuthenticationFilter
Update doFilterInternal to use jwtTokenProvider.validateToken(jwt) instead of duplicate validation logic.
Step 4: Password Validation
New Files:
server/src/main/java/com/saas/springular/common/validation/ValidPassword.javaserver/src/main/java/com/saas/springular/common/validation/PasswordValidator.java
4.1 Create ValidPassword Annotation
Custom constraint annotation with PasswordValidator as validatedBy.
4.2 Create PasswordValidator
Implementation: Minimum 12 characters, any characters allowed (NIST-aligned).
4.3 Apply to SignupRequest
Add @ValidPassword annotation. Use @Size(max = 128) for upper limit only.
4.4 Add @Email to LoginRequest
Add @Email annotation for consistency.
Step 5: Rate Limiting
New Files:
server/src/main/java/com/saas/springular/common/security/RateLimitService.javaserver/src/main/java/com/saas/springular/common/security/RateLimitProperties.javaserver/src/main/java/com/saas/springular/common/security/RateLimitHeaders.javaserver/src/main/java/com/saas/springular/common/exception/TooManyRequestsException.java
5.1 Add Bucket4j Dependency
Add com.bucket4j:bucket4j-core:8.10.1 to build.gradle.
5.2 Create RateLimitProperties
Configuration class with capacity, refillTokens, refillDurationSeconds.
5.3 Create RateLimitService
Token bucket implementation using Bucket4j with ConcurrentHashMap cache.
5.4 Create RateLimitHeaders
Constants for header names: LIMIT, REMAINING, RESET.
5.5 Create TooManyRequestsException
Exception with @ResponseStatus(HttpStatus.TOO_MANY_REQUESTS).
5.6 Add Rate Limit Config to application.yml
Add rate-limit.* properties with defaults via environment variables.
5.7 Apply Rate Limiting to AuthenticationController
Add rate limiting to /login and /register endpoints. Include rate limit headers in responses.
Step 6: Centralized Exception Handling
New Files:
server/src/main/java/com/saas/springular/common/exception/ResourceNotFoundException.javaserver/src/main/java/com/saas/springular/common/exception/ErrorResponse.javaserver/src/main/java/com/saas/springular/common/exception/GlobalExceptionHandler.java
6.1 Create ResourceNotFoundException
Exception with constructors for simple message and resource/field/value format.
6.2 Create ErrorResponse
Record with message, status, path, timestamp.
6.3 Create GlobalExceptionHandler
@RestControllerAdvice with handlers for:
ResourceNotFoundException→ 404TooManyRequestsException→ 429BadCredentialsException→ 401MethodArgumentNotValidException→ 400IllegalArgumentException→ 400Exception→ 500
Step 7: Fix Optional.get() and Transactions
File: server/src/main/java/com/saas/springular/user/api/AuthenticationServiceImpl.java
7.1 Fix Unsafe Optional.get()
Replace .get() with .orElseThrow(() -> new ResourceNotFoundException(...)).
7.2 Fix Transaction Annotations
loginUser():@Transactional(creates refresh token)refresh():@Transactional(creates new refresh token, may delete expired)registerUser():@Transactional(writes user)
7.3 Use @RequiredArgsConstructor
Change @AllArgsConstructor to @RequiredArgsConstructor for consistency.
Step 8: Production Infrastructure
8.1 Add Dependencies (build.gradle)
com.bucket4j:bucket4j-core:8.10.1- Rate limitingorg.springframework.boot:spring-boot-starter-actuator- Monitoringcom.fasterxml.jackson.datatype:jackson-datatype-jsr310- Date/time serialization- Checkstyle plugin configuration
8.2 Update application.yml
- Hikari connection pool settings
- Actuator endpoints (health, info, metrics)
- Secure logging (hide SQL parameters)
8.3 Create application-dev.yml
Dev profile with devtools enabled and ddl-auto: update.
Step 9: Unit Tests
New Files:
server/src/test/java/com/saas/springular/common/security/RateLimitServiceTest.javaserver/src/test/java/com/saas/springular/common/validation/PasswordValidatorTest.javaserver/src/test/java/com/saas/springular/common/exception/GlobalExceptionHandlerTest.java
Tests cover:
- Rate limit bucket behavior and limits
- Password length validation
- Exception handler responses
Step 10: Fix Frontend API URL
File: client/src/environments/environment.prod.ts
Change apiUrl from http://localhost:4200 to '' (empty string) so nginx can proxy /api requests correctly.
Verification Checklist
Run all verifications before merging:
- ✅ Build passes:
./gradlew clean build - ✅ Tests pass:
./gradlew test - ✅ Checkstyle passes:
./gradlew checkstyleMain checkstyleTest - ✅ Frontend builds:
cd client && npm run build - ✅ Registration works with valid password
- ✅ Rate limiting prevents brute force (11th request fails)
- ✅ Weak passwords rejected
Summary
Security Issues Fixed:
- ✅ CORS wildcard → allowedOriginPatterns with credentials
- ✅ No JWT validation → Full validation with error handling
- ✅ CSRF disabled → Cookie-based CSRF tokens with frontend integration
- ✅ No password policy → 12+ chars minimum
- ✅ No rate limiting → Configurable rate limiting on auth endpoints
- ✅ Hardcoded secrets → Environment variables with defaults
- ✅ No input validation → @Valid on all DTOs with custom validators
Bundled Enhancements:
- ✅ Centralized exception handling with consistent responses
- ✅ Optional.get() → orElseThrow for safety
- ✅ Transaction annotations fixed for write operations
- ✅ Production infrastructure (Actuator, Hikari, logging)
- ✅ 19 unit tests for all new code