Problem
I'm experiencing a StackOverflowError when trying to authenticate users in my Spring Boot application. When I call the login endpoint, I get a stack trace showing an infinite recursion loop related to Spring's AOP proxies and the authenticate method.
Here's the error stack trace:
java.lang.StackOverflowError: null
at java.base/java.lang.Exception.<init>(Exception.java:103) ~[na:na]
at java.base/java.lang.ReflectiveOperationException.<init>(ReflectiveOperationException.java:90) ~[na:na]
at java.base/java.lang.reflect.InvocationTargetException.<init>(InvocationTargetException.java:68) ~[na:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:118) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
at .springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-6.2.1.jar:6.2.1]
at .springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:216) ~[spring-aop-6.2.1.jar:6.2.1]
at jdk.proxy3/jdk.proxy3.$Proxy205.authenticate(Unknown Source) ~[na:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
at .springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-6.2.1.jar:6.2.1]
at .springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:216) ~[spring-aop-6.2.1.jar:6.2.1]
at jdk.proxy3/jdk.proxy3.$Proxy205.authenticate(Unknown Source) ~[na:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
at .springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-6.2.1.jar:6.2.1]
at .springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:216) ~[spring-aop-6.2.1.jar:6.2.1]
at jdk.proxy3/jdk.proxy3.$Proxy205.authenticate(Unknown Source) ~[na:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
at .springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-6.2.1.jar:6.2.1]
at .springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:216) ~[spring-aop-6.2.1.jar:6.2.1]
at jdk.proxy3/jdk.proxy3.$Proxy205.authenticate(Unknown Source) ~[na:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
at .springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-6.2.1.jar:6.2.1]
at .springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:216) ~[spring-aop-6.2.1.jar:6.2.1]
Code
Here are the relevant parts of my implementation:
AuthController.java
@CrossOrigin("*")
@RestController
@AllArgsConstructor
@RequestMapping("auth")
public class AuthController {
final AuthService authService;
@PostMapping("login")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest loginRequest) throws AuthenticationFailedException {
LoginResponse response = authService.login(loginRequest);
return ResponseEntity.status(HttpStatus.OK).body(response);
}
}
AuthService.java
@Service
@AllArgsConstructor
public class AuthService {
final AuthenticationManager authenticationManager;
final UserRepository userRepository;
final JwtTokenProvider tokenProvider;
final Logger logger = LoggerFactory.getLogger(AuthService.class);
final PasswordEncoder passwordEncoder;
@Transactional
public LoginResponse login(LoginRequest loginRequest) throws AuthenticationFailedException {
logger.info("Attempting login for user: {}", loginRequest.getEmail());
try {
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
loginRequest.getEmail(), loginRequest.getPassword()
));
SecurityContextHolder.getContext().setAuthentication(authentication);
FlyWellUserPrincipal userPrincipal = (FlyWellUserPrincipal) authentication.getPrincipal();
User user = userRepository.findByEmail(userPrincipal.getUsername()).orElseThrow(() -> new EntityNotFoundException("User not found"));
String accessToken = tokenProvider.generateToken(userPrincipal);
String refreshToken = tokenProvider.generateToken(userPrincipal);
user.setRefreshToken(refreshToken);
userRepository.save(user);
logger.info("User {} {}, Email: {} successfully logged in", user.getFirstName(), user.getLastName(), user.getEmail());
return toResponse(user.getPublicId(), refreshToken, accessToken);
}catch (Exception e) {
logger.warn("Failed login attempt for user: {}", loginRequest.getEmail());
logger.error("Login Attempt Failed:: {}", e.getMessage());
throw new AuthenticationFailedException("Invalid username or password");
}
}
}
SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
final JwtAuthenticationFilter jwtAuthFilter;
final AccessDeniedHandlerImpl accessDeniedHandler;
final AuthenticationEntryPointImpl authenticationEntryPoint;
public SecurityConfig(@Lazy JwtAuthenticationFilter jwtAuthFilter, AccessDeniedHandlerImpl accessDeniedHandler,
AuthenticationEntryPointImpl authenticationEntryPoint) {
this.jwtAuthFilter = jwtAuthFilter;
this.accessDeniedHandler = accessDeniedHandler;
this.authenticationEntryPoint = authenticationEntryPoint;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// Configuration omitted for brevity
}
@Bean
public UserDetailsService userDetailsService(UserRepository userRepository){
return new FlyWellUserDetailsService(userRepository);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
FlyWellUserDetailsService.java
@Service
@AllArgsConstructor
public class FlyWellUserDetailsService implements UserDetailsService {
final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByEmail(username)
.orElseThrow(() -> new EntityNotFoundException("User with email" + username));
List<GrantedAuthority> authorities = user.getRoles()
.stream()
.map(role -> (GrantedAuthority) new SimpleGrantedAuthority("Role_" + role.getName())).toList();
return new FlyWellUserPrincipal(user, authorities);
}
}
FlyWellUserPrincipal.java
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class FlyWellUserPrincipal implements UserDetails {
private User user;
private List<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getEmail();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
//Configuration ommitted for brevity
}
}
What I've Tried
- Added
@Lazy
annotation to theJwtAuthenticationFilter
inSecurityConfig
- Verified that my user data is correctly saved in the database
- Checked that passwords are properly encrypted with BCrypt
Questions
- What is causing this
StackOverflowError
during authentication? - Are there circular dependencies in my Spring Security configuration that I need to resolve?
- Is there an issue with how I'm implementing
UserDetails
or theUserDetailsService
? How can I fix this issue while maintaining the security requirements of my application?