ChatGPT gave me Spring Security code that looked perfect. Then I spent 4 hours debugging why my login kept failing.
I've now fixed these same AI-generated bugs in over 50 code reviews. Here's how to spot and fix them in 20 minutes instead of wasting your entire afternoon.
What you'll fix: 5 critical Spring Security bugs that AI tools always mess up Time needed: 20 minutes Difficulty: Intermediate (you know Spring Boot basics)
Skip the debugging nightmare. I'll show you the exact patterns AI gets wrong and the working code that actually passes production.
Why I Built This Guide
My situation: I'm a senior developer who reviews a lot of Spring Boot apps. Since ChatGPT and GitHub Copilot became popular, I see the same 5 Spring Security bugs in almost every AI-generated project.
My setup:
- Spring Boot 3.2+ applications
- Teams using AI coding assistants daily
- Production apps that need actual security (not just demos)
What didn't work:
- Trusting AI-generated Security configs blindly
- Copying Stack Overflow answers from 2019
- Using deprecated WebSecurityConfigurerAdapter patterns
Time wasted: I've spent probably 40+ hours total helping developers fix these same bugs.
The 5 Bugs AI Always Creates
Here are the patterns I see in every AI-generated Spring Security config:
- Wrong password encoder setup (breaks all logins)
- Broken CORS configuration (frontend can't connect)
- Deprecated security syntax (won't compile on Spring Boot 3+)
- Missing CSRF handling (forms fail silently)
- Bad authentication provider chains (users can't authenticate)
Let's fix each one with working code.
Bug #1: Password Encoder Hell
The problem: AI tools love to generate this broken pattern:
// AI GENERATES THIS BROKEN CODE
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder() // This is the bug
.username("admin")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
What breaks: withDefaultPasswordEncoder() is deprecated and creates security warnings. Worse, it uses a different encoder than your BCrypt bean.
My solution: Always encode passwords manually with your chosen encoder:
// WORKING CODE - Copy this exact pattern
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("admin")
.password(passwordEncoder.encode("password")) // Explicit encoding
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
Expected output: No deprecation warnings, login works immediately
Personal tip: "I always test with a hardcoded user first, then switch to database users. Saves 30 minutes of 'is it the encoder or the database?' debugging."
Bug #2: CORS Configuration That Doesn't Work
The problem: AI generates CORS configs that look right but fail in browsers:
// AI GENERATES THIS BROKEN CORS
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors().and()
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
);
return http.build();
}
// Missing or wrong CORS configuration
}
What breaks: No actual CORS bean, or it's configured wrong. Frontend gets CORS errors.
My solution: Always provide explicit CORS configuration:
// WORKING CODE - Handles real frontend requests
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable()) // For APIs - be careful in production
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("http://localhost:*")); // Dev only
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Expected output: Frontend can make requests without CORS errors
Personal tip: "Use allowedOriginPatterns instead of allowedOrigins for localhost development. Handles random ports automatically."
Bug #3: Deprecated Spring Security 5 Syntax
The problem: AI tools trained on old tutorials generate deprecated code:
// AI GENERATES THIS DEPRECATED CODE (won't compile)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter { // Deprecated!
@Override
protected void configure(HttpSecurity http) throws Exception { // Deprecated!
http
.authorizeRequests() // Old method
.antMatchers("/api/public/**").permitAll() // Old method
.anyRequest().authenticated();
}
}
What breaks: Won't compile on Spring Boot 3+. Even if it compiles, you get deprecation warnings.
My solution: Use the modern Spring Boot 3+ bean-based configuration:
// WORKING CODE - Spring Boot 3+ style
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll() // New method
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard", true)
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
)
.build();
}
}
Expected output: Clean compilation, no deprecation warnings
Personal tip: "The new lambda-style config is actually more readable once you get used to it. Each concern is clearly separated."
Bug #4: Silent CSRF Failures
The problem: AI either forgets CSRF completely or disables it everywhere:
// AI GENERATES THESE BROKEN PATTERNS
// Pattern 1: CSRF disabled everywhere (security risk)
http.csrf().disable();
// Pattern 2: CSRF enabled but forms don't include tokens (silent failures)
http.csrf().and()
.formLogin(/* form config without CSRF token */);
What breaks: Forms submit but nothing happens, or you get 403 errors with no explanation.
My solution: Configure CSRF properly for your use case:
// WORKING CODE - Proper CSRF handling
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers("/api/**") // Disable for API endpoints only
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.build();
}
And in your Thymeleaf templates, include the CSRF token:
<!-- WORKING HTML - Include in all forms -->
<form th:action="@{/login}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<input type="text" name="username" placeholder="Username" />
<input type="password" name="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
Expected output: Forms work without 403 errors, APIs work without CSRF tokens
Personal tip: "I always use cookie-based CSRF tokens for SPAs. Way easier than managing headers manually."
Bug #5: Authentication Provider Chaos
The problem: AI creates overly complex authentication chains that don't work:
// AI GENERATES THIS OVERCOMPLICATED MESS
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config,
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) throws Exception {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
// Unnecessary complexity that often breaks
return new ProviderManager(Arrays.asList(provider, /* other providers */));
}
What breaks: Complex authentication chains often have ordering issues or conflicts.
My solution: Keep it simple and let Spring handle the defaults:
// WORKING CODE - Let Spring Boot do the heavy lifting
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.build(); // Spring Boot auto-configures AuthenticationManager
}
// Only define these if you need custom behavior
@Bean
public UserDetailsService userDetailsService() {
// Your user loading logic
return username -> loadUserFromDatabase(username);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Expected output: Authentication works without complex provider setup
Personal tip: "Spring Boot 3's auto-configuration is really good. Only override the defaults when you have a specific reason."
Complete Working Example
Here's a full Security configuration that actually works in production:
package com.yourapp.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers("/api/**")
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard", true)
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder.encode("admin"))
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("http://localhost:*"));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Testing Your Fixes
Test each fix with these specific scenarios:
Test 1: Basic Login
- Start your app
- Go to
/login - Enter
user/password - Should redirect to
/dashboard
Test 2: CORS (if you have a frontend)
- Start your frontend on different port
- Make an API call to your backend
- Should work without CORS errors
Test 3: CSRF (if using forms)
- Submit a form without JavaScript
- Should work without 403 errors
Test 4: Role-based Access
- Login as
admin/admin - Access
/admin/endpoints - Should work without 403 errors
What You Just Built
A Spring Security configuration that actually works with modern Spring Boot 3+ and handles the 5 most common AI-generated bugs.
Key Takeaways (Save These)
- Password Encoding: Always use explicit
passwordEncoder.encode()instead of deprecated helpers - CORS Configuration: Provide actual CorsConfigurationSource beans, don't rely on defaults
- Modern Syntax: Use lambda-style configuration, avoid deprecated WebSecurityConfigurerAdapter
- CSRF Handling: Configure it properly or disable it explicitly for APIs only
- Keep It Simple: Let Spring Boot auto-configure authentication when possible
Tools I Actually Use
- IntelliJ IDEA: Catches deprecated Spring Security methods immediately
- Spring Boot DevTools: Automatic restarts when testing security configs
- Postman: Testing API endpoints and CORS behavior
- Chrome DevTools: Debugging CSRF token issues in forms
Personal tip: "Set up a simple test controller that returns the current user. Makes it super easy to verify your security config is working: @GetMapping("/api/me") public Authentication me(Authentication auth) { return auth; }"