Search code examples
spring-bootrestasynchronousspring-security

Spring boot 3.2.2 With Spring security - Problem with Security context in async actions


Hello :) I have some issues with spring boot update to newest stable version and my async endpoints...

Recently i tasked myself with bumping spring boot from 2.7.18 to 3.2.2 and I thought that it is over and it's working - nothing near !

I have an issue where when I return Mono or Completable Future my response losses the security context and I don't have idea how to handle it ;/ I think that's related to configurati

my spring boot dependencies :

spring-boot-starter-web
spring-boot-starter-web - it is used on some of my endpoints and according to the documentation it's working on spring-mvc basis. - worked on 2.7.18
spring-boot-starter-websocket
spring-boot-starter-security

my example endpoints where in both cases I have same issue:

    @ResponseStatus(HttpStatus.OK)
    @GetMapping(path = "/barka", produces = APPLICATION_JSON_VALUE)
    @PreAuthorize(SecurityRuleSet.ALL_USERS)
    @Operation(summary = SWAG_ALL_USERS + "Get logics from specified application")
    public CompletableFuture<String> method() {
        return retrieverService.getBarka();
    }
      @ResponseStatus(HttpStatus.OK)
    @GetMapping(path = "/logics", produces = {APPLICATION_JSON_VALUE})
    @PreAuthorize(SecurityRuleSet.ALL_USERS)
    @Operation(summary = SWAG_ALL_USERS + "Get logics from specified application")
    public Mono<List<LogicInfoMMResponseDTO>> getLogics(@NotBlank @RequestParam String applicationName) {
        return retrieverService.getLogicsForFront(MMFilterableCommand.builder()
                .applicationName(applicationName).build());
    }

my config class:

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true, mode = PROXY)
public class WebSecurityConfig {

    private final AbstractCookieManager abstractCookieManager;
    private final FilterChainHandler filterChainHandler;
    private final SpringJwtTokenProvider springJwtTokenProvider;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws  {
        http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .csrf(AbstractHttpConfigurer::disable)
                .cors(AbstractHttpConfigurer::disable)

                .authorizeHttpRequests(auth -> {
                    auth

                            .anyRequest().authenticated();
                })
                .formLogin(AbstractHttpConfigurer::disable)
                .logout(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .addFilterBefore(filterChainHandler, UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new CookieJwtTokenFilter(abstractCookieManager), UsernamePasswordAuthenticationFilter.class);

        return http.build();

    }

    @Bean
    public WebSecurityCustomizer configure() {
        return web -> web.ignoring().requestMatchers("/api/login","/api/logout", "/api/refresh-token",
                "/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**"
                , "/api/version", "/api/monitoring/log", "/api/jenkins-job-update"
                , "/ws/public/**"
        );
    }


    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http)
            throws  {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .build();
    }
    @Bean
    @RequestScope
    public UserContext requestScopeRequestData() {
        return new UserContext(springJwtTokenProvider);
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        final CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("*"));
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE"));
        configuration.setAllowCredentials(true);
        configuration.setAllowedHeaders(List.of("Authorization", "Cache-Control", "Content-Type"));

        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }
}

Custom JWT extraction filter from JWT - I know that is not the best and spring has it's own way to handle it - it might be the issue but not and expert here and projects runs for approx 2+ years right now so there is some technical debt by now :/

@RequiredArgsConstructor
public class CookieJwtTokenFilter extends OncePerRequestFilter {

    private final AbstractCookieManager abstractCookieManager;

    @Override
    protected void doFilterInternal(@NotNull HttpServletRequest httpServletRequest, @NotNull HttpServletResponse httpServletResponse, FilterChain filterChain) throws Servlet, IO {
        String token = abstractCookieManager.resolveCookieToken(httpServletRequest);
        try {
            if (token != null && abstractCookieManager.validateToken(token)) {
               var authentication =  abstractCookieManager.getAuthentication(token);
                SecurityContext sc = SecurityContextHolder.getContext();
                sc.setAuthentication(authentication);
                HttpSession session = httpServletRequest.getSession(true);
                SecurityContextHolder.getContext().setAuthentication(authentication);
                SecurityContextHolder.setContext(sc);
                session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, sc);

            }
        } catch (MMGui ex) {
            SecurityContextHolder.clearContext();
            httpServletResponse.send(ex.getHttpStatus().value(), ex.getMessage());
            return;
        }
        catch ( ex) {
            SecurityContextHolder.clearContext();
            httpServletResponse.send(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
            return;
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

}

and the log that informs on the issue :

10:51:15.471 DEBUG DefaultPooledConnectionProvider - [9892e210, L:/10.9.248.21:56812 - R:/10.9.27.29:12750] onStateChange(GET{uri=/logic, connection=PooledConnection{channel=[id: 0x9892e210, L:/10.9.248.21:56812 - R:/10.9.27.29:12750]}}, [disconnecting])
10:51:15.471 DEBUG DefaultPooledConnectionProvider - [9892e210, L:/10.9.248.21:56812 - R:/10.9.27.29:12750] Releasing channel
10:51:15.472 DEBUG PooledConnectionProvider - [9892e210, L:/10.9.248.21:56812 - R:/10.9.27.29:12750] Channel cleaned, now: 0 active connections, 1 inactive connections and 0 pending acquire requests.
10:51:15.473 DEBUG FilterChainProxy - Securing GET /api/market-maker-data/logics?applicationName=mm_pancakeswap_flokibnb
10:51:15.473 DEBUG AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext
10:51:15.476 DEBUG Http403ForbiddenEntryPoint - Pre-authenticated entry point called. Rejecting access
10:51:15.477 DEBUG FilterChainProxy - Securing GET /?applicationName=mm_pancakeswap_flokibnb
10:51:15.477 DEBUG AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext
10:51:15.477 DEBUG Http403ForbiddenEntryPoint - Pre-authenticated entry point called. Rejecting access

I found out that if I mess in this place in my securinty config

.authorizeHttpRequests(auth -> {
 auth

  .anyRequest().authenticated();
  })

It started to work some maybe here is an issue ? I'm not the best with upgrading with versions and I spent to much time to figure it to this place...

I would be grateful for any hint or clue what might the issue be and how to solve it <3 B.


Solution

  • After consulting with Spring-security team on github i have my working answer. I will put it here as a hint for anyone to have as a potential solution.

    It is required to have following bean

        @Bean
        public SecurityContextRepository securityContextRepository() {
            return new DelegatingSecurityContextRepository(
                    new RequestAttributeSecurityContextRepository(),
                    new HttpSessionSecurityContextRepository()
            );
        }
    
    
    

    which should be injected as follows:

        private final SecurityContextRepository repository;
    
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                    .csrf(AbstractHttpConfigurer::disable)
                    .cors(AbstractHttpConfigurer::disable)
                    .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                    .formLogin(AbstractHttpConfigurer::disable)
                    .logout(AbstractHttpConfigurer::disable)
                    .httpBasic(AbstractHttpConfigurer::disable)
                    .securityContext(request -> request.securityContextRepository(repository))  // insert here 
                    .addFilterBefore(filterChainExceptionHandler, UsernamePasswordAuthenticationFilter.class)
                    .addFilterBefore(new CookieJwtTokenFilter(abstractCookieManager, repository),
                            UsernamePasswordAuthenticationFilter.class); // and in my case - into my filter
    
            return http.build();
    
        }
    

    and example how to use it in my filter

    
    @RequiredArgsConstructor
    public class CookieJwtTokenFilter extends OncePerRequestFilter {
    
        private final AbstractCookieManager abstractCookieManager;
        private final SecurityContextRepository repository;
    
    
        @Override
        protected void doFilterInternal(@NotNull HttpServletRequest httpServletRequest, @NotNull HttpServletResponse httpServletResponse, @NotNull FilterChain filterChain) throws ServletException, IOException {
            String token = abstractCookieManager.resolveCookieToken(httpServletRequest);
            try {
                if (token != null && abstractCookieManager.validateToken(token)) {
                   var authentication =  abstractCookieManager.getAuthentication(token);
                    SecurityContext sc = SecurityContextHolder.getContext();
                    HttpSession session = httpServletRequest.getSession(true);
                    SecurityContextHolder.setContext(sc);
                    this.repository.saveContext(sc, httpServletRequest, httpServletResponse);
    
                    sc.setAuthentication(authentication);
                    session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, sc);
                }
            } catch (MMGuiException ex) {
                SecurityContextHolder.clearContext();
                httpServletResponse.sendError(ex.getHttpStatus().value(), ex.getMessage());
                return;
            }
            catch (Exception ex) {
                SecurityContextHolder.clearContext();
                httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
                return;
            }
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        }
    
    }@RequiredArgsConstructor
    public class CookieJwtTokenFilter extends OncePerRequestFilter {
    
        private final AbstractCookieManager abstractCookieManager;
        private final SecurityContextRepository repository;
    
    
        @Override
        protected void doFilterInternal(@NotNull HttpServletRequest httpServletRequest, @NotNull HttpServletResponse httpServletResponse, @NotNull FilterChain filterChain) throws ServletException, IOException {
            String token = abstractCookieManager.resolveCookieToken(httpServletRequest);
            try {
                if (token != null && abstractCookieManager.validateToken(token)) {
                   var authentication =  abstractCookieManager.getAuthentication(token);
                    SecurityContext sc = SecurityContextHolder.getContext();
                    HttpSession session = httpServletRequest.getSession(true);
                    SecurityContextHolder.setContext(sc);
                    this.repository.saveContext(sc, httpServletRequest, httpServletResponse);
    
                    sc.setAuthentication(authentication);
                    session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, sc);
                }
            } catch (MMGuiException ex) {
                SecurityContextHolder.clearContext();
                httpServletResponse.sendError(ex.getHttpStatus().value(), ex.getMessage());
                return;
            }
            catch (Exception ex) {
                SecurityContextHolder.clearContext();
                httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
                return;
            }
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        }
    
    }
    
    

    Places to read further on the topic for some proper explanations:

    https://github.com/spring-projects/spring-security/issues/11962#issuecomment-1320346945

    https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html