Within the Boot 4 migration guide, we can read:

We recommend that you eventually migrate your application away from using the classic starters.

However, I couldn't migrate the following:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.7</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>truc</groupId>
    <artifactId>test-security-context-reproducer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>test-security-context-reproducer</name>
    <description>Reproduce test security context regression</description>

    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
spring.application.name=truc
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://what.ever
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
public class TrucApplication {

  static final String TEST_ENDPOINT = "/greet";

  public static void main(String[] args) {
    SpringApplication.run(TrucApplication.class, args);
  }

  @Configuration
  @EnableWebSecurity
  @EnableMethodSecurity
  static class SecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

      http.oauth2ResourceServer(rs -> rs.jwt(
          customizer -> customizer.jwtAuthenticationConverter(jwt -> new JwtAuthenticationToken(jwt,
              jwt.getClaimAsStringList("roles").stream().map(SimpleGrantedAuthority::new).toList(),
              jwt.getClaimAsString(JwtClaimNames.SUB)))));

      http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
      http.csrf(csrf -> csrf.disable());

      http.authorizeHttpRequests(requests -> requests.anyRequest().authenticated());

      return http.build();
    }
  }

  @Service
  public static class MessageService {
    @PreAuthorize("hasAuthority('GREATABLE')")
    public String greet(Object auth) {
      return String.format("Hello %s! You are granted with %s.", "user", "authorities");
    }
  }

  @RestController
  public static class GreetingController {
    private final MessageService messageService;

    public GreetingController(MessageService messageService) {
      this.messageService = messageService;
    }

    @PreAuthorize("hasAuthority('GREATABLE')")
    @GetMapping(TEST_ENDPOINT)
    public String greet(JwtAuthenticationToken auth) {
      return messageService.greet(auth);
    }
  }
}
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.test.context.TestSecurityContextHolder;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest
@AutoConfigureMockMvc
class TrucApplicationTests {

  @Autowired
  MockMvc api;

  @Test
  void contextLoads() {}

  @Test
  void givenTestSecurityContextIsNotSet_whenCallSecuredEndpoint_thenUnauthorized()
      throws Exception {
    TestSecurityContextHolder.clearContext();

    api.perform(get(TrucApplication.TEST_ENDPOINT)).andExpect(status().isUnauthorized());
  }

  @Test
  void givenMockedAuthenticationInTestSecurityContext_whenCallSecuredEndpoint_thenOk()
      throws Exception {
    var auth = mock(JwtAuthenticationToken.class);
    when(auth.isAuthenticated()).thenReturn(true);
    when(auth.getAuthorities()).thenReturn(List.of(new SimpleGrantedAuthority("GREATABLE")));
    TestSecurityContextHolder.setAuthentication(auth);

    api.perform(get(TrucApplication.TEST_ENDPOINT)).andExpect(status().isOk());
  }
}

Describe the bug With Boot 4.0.0-RC1, MockMvc requests test security context is anonymous, whatever we set via TestSecurityContextHolder.

Note that the unit tests for the secured MessageService are removed from the code above because they work the same with Boot 3.x and 4.0.0-RC1 (no regression).

Also note that when using spring-boot-starter-test-classic instead of spring-boot-starter-test, things work as before. But I couldn't find any documentation about what additional dependency to include or configuration to set for the TestSecurityContextHolder to work with @AutoConfigureMockMvc.

To Reproduce Change the dependencies to:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.0.0-RC1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>truc</groupId>
    <artifactId>test-security-context-reproducer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>test-security-context-reproducer</name>
    <description>Reproduce test security context regression</description>

    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security-oauth2-resource-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-webmvc-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Also, organize test imports to adapt to the new @AutoConfigureMockMvc package.

Expected behavior The way to configure the test security context should be homogeneous across component types (what works when unit testing a @Service should work the same when testing a @Controller) and servlet / reactive apps.