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.
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