Describe the bug I am using Spring MVC to create a service and Spring WebFlux to use WebClient. WebClient uses JWT to authorize a request taken from the login user. For offline tasks, I am using a technical user with is authorized manually using SecurityContextHolder. After setting and removing a user using SecurityContextHolder, ReactiveSecurityContextHolder does not have the same state as SecurityContextHolder. In the following examples, I will try to explain exactly what I did.

Is there a better way to set ReactiveSecurityContextHolder?

To Reproduce dependacies

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-webflux</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
  </dependency>
</dependencies>

Utils functions:

public class SecurityUtils {

  private SecurityUtils() {
  }

  public static final User userA = new User("UserA", "", true, true, true, true, List.of());
  public static final User userB = new User("UserB", "", true, true, true, true, List.of());

  public static final Authentication authenticationUserA =
      new UsernamePasswordAuthenticationToken(userA, "", userA.getAuthorities());
  public static final Authentication authenticationUserB =
      new UsernamePasswordAuthenticationToken(userB, "", userB.getAuthorities());

  public static User getUser() {
    return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
        .map(Authentication::getPrincipal)
        .filter(User.class::isInstance)
        .map(User.class::cast)
        .orElse(null);
  }

  public static Mono<User> getReactiveUser() {
    return ReactiveSecurityContextHolder.getContext()
        .map(SecurityContext::getAuthentication)
        .map(Authentication::getPrincipal)
        .cast(User.class);
  }

  public static String getUsername() {
    return Optional.ofNullable(getUser())
        .map(User::getUsername)
        .orElse(null);
  }

  public static String getReactiveUsername() {
    return Optional.ofNullable(SecurityUtils.getReactiveUser().block())
        .map(User::getUsername)
        .orElse(null);
  }

  public static void printLoginUser() {
    System.out.println("User Mvc " + getUsername());
    System.out.println("User reactive " + getReactiveUsername());
  }
}

Test 1:

// set UserA
SecurityContextHolder.getContext().setAuthentication(authenticationUserA);
printLoginUser();

SecurityContextHolder.clearContext();
printLoginUser();

// set UserB
SecurityContextHolder.getContext().setAuthentication(authenticationUserB);
printLoginUser();
User Mvc UserA
User reactive UserA
User Mvc null
User reactive UserA
User Mvc UserB
User reactive UserA

Test 2:

// set UserA
SecurityContextHolder.getContext().setAuthentication(authenticationUserA);
ReactiveSecurityContextHolder.getContext()
    .subscribe(v -> v.setAuthentication(authenticationUserA));
printLoginUser();

SecurityContextHolder.clearContext();
printLoginUser();

// set UserB
SecurityContextHolder.getContext().setAuthentication(authenticationUserB);
ReactiveSecurityContextHolder.getContext()
    .subscribe(v -> v.setAuthentication(authenticationUserB));
printLoginUser();
User Mvc UserA
User reactive UserA
User Mvc null
User reactive UserA
User Mvc UserB
User reactive UserB

Test 3:

// set UserA
SecurityContextHolder.getContext().setAuthentication(authenticationUserA);
printLoginUser();

SecurityContextHolder.getContext().setAuthentication(null);
SecurityContextHolder.clearContext();
printLoginUser();

// set UserB
SecurityContextHolder.getContext().setAuthentication(authenticationUserB);
ReactiveSecurityContextHolder.getContext()
    .subscribe(v -> v.setAuthentication(authenticationUserB));
printLoginUser();
User Mvc UserA
User reactive UserA
User Mvc null
User reactive null
User Mvc UserB
User reactive null

Test 4:

// set UserA
SecurityContextHolder.getContext().setAuthentication(authenticationUserA);
ReactiveSecurityContextHolder.getContext()
    .subscribe(v -> v.setAuthentication(authenticationUserA));
printLoginUser();

SecurityContextHolder.getContext().setAuthentication(null);
SecurityContextHolder.clearContext();
printLoginUser();

// set UserB
SecurityContextHolder.getContext().setAuthentication(authenticationUserB);
ReactiveSecurityContextHolder.getContext()
    .subscribe(v -> v.setAuthentication(authenticationUserB));
printLoginUser();
User Mvc UserA
User reactive UserA
User Mvc null
User reactive null
User Mvc UserB
User reactive null

Test 5:

// set UserA
SecurityContextHolder.getContext().setAuthentication(authenticationUserA);
printLoginUser();

ReactiveSecurityContextHolder.getContext()
    .subscribe(v -> v.setAuthentication(null));
SecurityContextHolder.clearContext();
printLoginUser();

// set UserB
SecurityContextHolder.getContext().setAuthentication(authenticationUserB);
ReactiveSecurityContextHolder.getContext()
    .subscribe(v -> v.setAuthentication(authenticationUserB));
printLoginUser();
User Mvc UserA
User reactive UserA
User Mvc null
User reactive null
User Mvc UserB
User reactive null

Test 6:

// set UserA
SecurityContextHolder.getContext().setAuthentication(authenticationUserA);
ReactiveSecurityContextHolder.getContext()
    .subscribe(v -> v.setAuthentication(authenticationUserA));
printLoginUser();

ReactiveSecurityContextHolder.getContext()
    .subscribe(v -> v.setAuthentication(null));
SecurityContextHolder.clearContext();
printLoginUser();

// set UserB
SecurityContextHolder.getContext().setAuthentication(authenticationUserB);
ReactiveSecurityContextHolder.getContext()
    .subscribe(v -> v.setAuthentication(authenticationUserB));
printLoginUser();
User Mvc UserA
User reactive UserA
User Mvc null
User reactive null
User Mvc UserB
User reactive null

Expected behavior If I am setting reactive security context correctly, I would expect the following behavior to work: 1. Setting a user with SecurityContextHolder should set ReactiveSecurityContextHolder. 2. Setting null with SecurityContextHolder should not break down the ReactiveSecurityContextHolder state. 3. Setting null with ReactiveSecurityContextHolder should not break down the ReactiveSecurityContextHolder state.

Comment From: zaa4wz

The behavior shown in the tests was observed when running SpringBootTest; When running the application normally, I always got the same answer.

User Mvc UserA
User reactive null
User Mvc null
User reactive null
User Mvc UserB
User reactive null

I think I did not configure the application in the right way

Comment From: zaa4wz

After looking into the OAuth 2.0 Resource Server library (SecurityReactorContextConfiguration, OAuth2ImportSelector, ServletBearerExchangeFilterFunction) I was finally able to access SecurityContext inside WebClient.

Is there an easier way to access SecurityContext inside WebClient?

Added changes:

@Configuration
@EnableWebSecurity
@Import(SecurityConfig.ReactiveSecurity.class)
public class SecurityConfig {

  static final class ReactiveSecurity implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
      return new String[]{
          "org.springframework.security.config.annotation.web.configuration.SecurityReactorContextConfiguration"
      };
    }
  }
}
public class SecurityUtils {

  static final String SECURITY_CONTEXT_ATTRIBUTES = "org.springframework.security.SECURITY_CONTEXT_ATTRIBUTES";

  public static Mono<User> getReactiveUser() {
    return Mono.deferContextual(Mono::just)
        .cast(Context.class)
        .flatMap(SecurityUtils::getAuthentication)
        .map(Authentication::getPrincipal)
        .filter(User.class::isInstance)
        .cast(User.class);
  }

  private static Mono<Authentication> getAuthentication(Context ctx) {
    return Mono.justOrEmpty(getAttribute(ctx, Authentication.class));
  }

  private static <T> T getAttribute(Context ctx, Class<T> clazz) {
    // NOTE: SecurityReactorContextConfiguration.SecurityReactorContextSubscriber adds this key
    if (!ctx.hasKey(SECURITY_CONTEXT_ATTRIBUTES)) {
      return null;
    }
    Map<Class<T>, T> attributes = ctx.get(SECURITY_CONTEXT_ATTRIBUTES);
    return attributes.get(clazz);
  }
}

Comment From: jzheaux

Because ReactiveSecurityContextHolder writes to reactor's Context object, you will typically write it as part of the reactive stack that requires it:

this.webClient.get()...
    .bodyToMono(...)
    .contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));

That said, if you are using WebClient, then Spring Security ships with ExchangeFilterFunction implementations for both context holders, meaning I'm not clear on why you need to propagate anyway.

Either way, I think it would be best at this point to move this question over to StackOverflow where we prefer to answer usage questions. Would you please post this question there and share the link here?

Comment From: zaa4wz

Link to StakOverflow question