javaspringspring-bootauthenticationspring-security

Spring 3/6 Migration Security Config with Custom Filter/Token/Provider - Issues Persisting Principal after successful Provider authenticate()


I've had some issues after a Spring 5 -> 6 (Spring Boot 3.3.4) migration. I can sign in to my site, but calls requesting the Provider afterwards do not work (the user returns null in say the following code below from our AuthenticationController):

@RequestMapping(value = "/user", method = RequestMethod.GET)
public UserResponse user(Principal user) {
    UserResponse response = new UserResponse();
    response.setCode(CODE_SUCCESS);
    response.setStatus(SUCCESS_STATUS);
    response.setData(user);
    return response;
}

I suspect the culprit is the SecurityConfig which has seen multiple changes with Spring 5 to Spring 6, most importantly, I think the code around .addFilterBefore(authenticationFilter(authenticationManager(http)), UsernamePasswordAuthenticationFilter.class) and authenticationFilter and authenticationManager beans are most important here and seen the most changes since Spring 5 configure overrides:

@Configuration
@EnableWebSecurity(debug = true)
public class XYZLibrarySecurityConfig {

    private final XYZLibraryAuthenticationProvider authProvider;
    private final XYZLibraryConfig properties;

    public XYZLibrarySecurityConfig(@Lazy XYZLibraryAuthenticationProvider authProvider, XYZLibraryConfig properties) {
        this.authProvider = authProvider;
        this.properties = properties;
    }
    
    @Bean
    public XYZLibraryAuthenticationFilter authenticationFilter(AuthenticationManager authenticationManager) {
        XYZLibraryAuthenticationFilter filter = new XYZLibraryAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManager);
        filter.setAuthenticationSuccessHandler(authenticationSuccessHandler());
        filter.setAuthenticationFailureHandler(authenticationFailureHandler());
        filter.setSessionAuthenticationStrategy(authStrategy());

        return filter;
    }
    
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
        authenticationManagerBuilder.authenticationProvider(authProvider);
        return authenticationManagerBuilder.build();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authenticationProvider(authProvider)
            .addFilterBefore(authenticationFilter(authenticationManager(http)), UsernamePasswordAuthenticationFilter.class)
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .exceptionHandling(exceptionHandling -> exceptionHandling.authenticationEntryPoint(authenticationEntryPoint()))
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/rest/authentication/authenticate").permitAll()
                .requestMatchers("/rest/authentication/user").permitAll()
                .requestMatchers("/rest/authentication/forgotPassword").permitAll()
                .requestMatchers("/rest/authentication/changePassword").permitAll()
                .requestMatchers("/rest/authentication/passwordRules").permitAll()
                .requestMatchers("/rest/authentication/helpContact").permitAll()
                .requestMatchers("/rest/authentication/user-message").denyAll()
                .requestMatchers("/rest/system-check/check").permitAll()
                .requestMatchers("/rest/admin/**").hasAnyRole("Administrator", "XYZ Complete Editor")
                .requestMatchers("/rest/**").hasAnyRole("User", "Administrator", "XYZ Limited User", "XYZ Limited Submitter", "XYZ Complete User", "XYZ Complete Editor")
                .anyRequest().authenticated()
            )
            .formLogin(formLogin -> formLogin.permitAll())
            .logout(logout -> logout
                .deleteCookies("JSESSIONID")
                .invalidateHttpSession(true)
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))
            )
            .headers(headers -> headers
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives("default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; frame-ancestors 'self'; form-action 'self';")
                )
                .httpStrictTransportSecurity(hsts -> hsts
                    .includeSubDomains(true)
                    .maxAgeInSeconds(31536000)
                )
            )
            .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(csrf -> {
                if (properties.getCsrf().getEnabled().booleanValue()) {
                    if (properties.getCsrf().getUseCookie().booleanValue()) {
                        CookieCsrfTokenRepository trep = (CookieCsrfTokenRepository) XYZLibraryCookieRepository.withHttpOnlyFalse();
                        trep.setCookiePath("/");
                        csrf.csrfTokenRepository(trep);
                    }
                } else {
                    csrf.disable();
                }
            });

        http.addFilterBefore(expiredSessionFilter(), SessionManagementFilter.class);
        //http.addFilterBefore(authenticationFilter(authenticationManager(http)), UsernamePasswordAuthenticationFilter.class);
        http.addFilterAfter(new RequestAuditingFilter(), BasicAuthenticationFilter.class);

        return http.build();
    }

In short, a /login request which authenticates the user appears to authenticate fine (returns 200 and a valid response, has user details, and the returning new UsernamePasswordAuthenticationToken(p, password, authorities) from the custom provider seems fine. However, /user (the first code block above) and other API calls that rely on the user principal coming back have null user parameters and thus don't work despite supposedly having successful authentication. I have heard of needing a supports override there, which we also have and looks like:

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(XYZLibraryAuthenticationToken.class);
    }

I can provide other code snippets of say the custom filter/token/providers but this post is already somewhat lengthy - while we do custom filter/token/providers there's nothing too out of the ordinary here I think and it used to work fine before migrating to Spring 3/6 so I think it's something to do with how our security config is set up now due to how many changes it needed (we also did some linter suggestions while we were in here like swapping to lambdas, etc..). I've tried moving orders around in the security chain and countless other trial and error tweaks but nothing seems to work, so I'm asking here to see if anyone has any ideas.


Solution

  • I fixed this today, and found the following out that helped resolve this in my custom filter code, a new override we did not once need:

    @Override
    protected void successfulAuthentication(HttpServletRequest request, 
                                            HttpServletResponse response, 
                                            FilterChain chain, 
                                            Authentication authResult) 
                                            throws IOException, ServletException {
        // Set the Authentication in the SecurityContextHolder
        SecurityContextHolder.getContext().setAuthentication(authResult);
    
        // Optionally create a session if needed (session-based auth)
        if (getAllowSessionCreation()) {
            request.getSession().setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
        }
    
        // Trigger the authentication success handler (which you already set)
        getSuccessHandler().onAuthenticationSuccess(request, response, authResult);
    
        // Optionally proceed with the filter chain if needed (for JWT-based applications)
        // chain.doFilter(request, response); // Uncomment if continuing the filter chain is required
    }