How to Fix AI-Generated Spring Boot Security Bugs That Break Your App

Stop debugging Spring Security configs from ChatGPT. Fix 5 common AI bugs in 20 minutes with working code examples.

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:

  1. Wrong password encoder setup (breaks all logins)
  2. Broken CORS configuration (frontend can't connect)
  3. Deprecated security syntax (won't compile on Spring Boot 3+)
  4. Missing CSRF handling (forms fail silently)
  5. 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

  1. Start your app
  2. Go to /login
  3. Enter user / password
  4. Should redirect to /dashboard

Test 2: CORS (if you have a frontend)

  1. Start your frontend on different port
  2. Make an API call to your backend
  3. Should work without CORS errors

Test 3: CSRF (if using forms)

  1. Submit a form without JavaScript
  2. Should work without 403 errors

Test 4: Role-based Access

  1. Login as admin / admin
  2. Access /admin/ endpoints
  3. 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; }"