spring-projects / spring-projects/spring-security

SecurityReactorContextSubscriber#LoadingMap fails to retrieve Authentication

Open
#11,973 3 comments 0 reactions 1 assignee View on GitHub

@jzheaux is already working on this.

Since Oct 10, 2022.

status: feedback-provided type: bug
Dominant language
Java
Stars
9.6k
Forks
6.3k
Avg merge
2d 11h
Merged PRs (30d)
52

Description

While upgrading Spring Boot from 2.6 to 2.7, one of our tests started failing.
The test verifies thread switching with WebClient for OAuth2 client in the servlet environment.

This happens when WebClient uses subscribeOn.(uses a different thread than the caller thread)
The SecurityReactorContextSubscriber/LoadingMap resolves null for Authentication.

I have created a minimum repro here.

This is the test case:

@SpringJUnitConfig
public class MyTest {

	// To run this test, it needs to have "spring-boot-starter-oauth2-resource-server" and "spring-boot-starter-oauth2-client"
	// dependencies which triggers "OAuth2ImportSelector" to import "SecurityReactorContextConfiguration".
	//

	// from SecurityReactorContextConfiguration.SecurityReactorContextSubscriberRegistrar#SECURITY_REACTOR_CONTEXT_OPERATOR_KEY
	// also used by ServletOAuth2AuthorizedClientExchangeFilterFunction
	static final String SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY = "org.springframework.security.SECURITY_CONTEXT_ATTRIBUTES";

	@Test
	@WithMockUser("foo")
	void demoTest() {
		Authentication authInMainThread = TestSecurityContextHolder.getContext().getAuthentication();

		AtomicReference<Authentication> authInFilter = new AtomicReference<>();
		WebClient webClient = WebClient.builder()
//				.filter(new ServletOAuth2AuthorizedClientExchangeFilterFunction())
				.filter((request, next) -> {
					return Mono.deferContextual(context -> {
						Map<Object, Object> contextAttributes = context.get(SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY);
						Authentication auth = (Authentication) contextAttributes.get(Authentication.class);
						authInFilter.set(auth);
						return next.exchange(request);
					});
				}).build();

		webClient.get().uri("https://vmware.com")
				.retrieve()
				.bodyToMono(String.class)
				.subscribeOn(Schedulers.boundedElastic())  // <-- use different thread to make a call
				.block();

		assertThat(authInFilter.get()).isSameAs(authInMainThread);
	}

	@Configuration(proxyBeanMethods = false)
	@EnableWebSecurity
	static class MyConfig {

	}
}
Root cause

The issue is introduced by this commit which added the LoadingMap to lazily retrieve the servlet request/response/auth.
Prior to this change, the request/response/auth were resolved on the caller's thread when the SecurityReactorContextSubscriber is created by the lifter.
However, with LoadingMap the callback is deferred until the webclient operations are executed.
When the thread is not on the caller's thread(by subscribeOn), it cannot retrieve any threadlocal values and they become null.

I haven't tested but the ServletOAuth2AuthorizedClientExchangeFilterFunction, which uses SecurityReactorContextSubscriber mechanism, should fail to resolve Authentication in this scenario.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.