spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.45k stars 5.76k forks source link

Inconsistent state between SecurityContextHolder and ReactiveSecurityContextHolder #14966

Closed zaa4wz closed 3 weeks ago

zaa4wz commented 3 weeks ago

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.
zaa4wz commented 3 weeks ago

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

zaa4wz commented 3 weeks ago

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);
  }
}
jzheaux commented 3 weeks ago

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?

zaa4wz commented 2 weeks ago

Link to StakOverflow question