Hi,

I initiated an upgrade from Spring Boot 2.7.X to 3.1.0. I followed all the steps.

Everything seems to be working, but since this upgrade, some unit tests of controllers with @WebMvcTest and @WithMockUser are not working. I am getting a 403 response. I believe something has changed with Spring Security 6.1 regarding mocked user authentication, but I can't find the correct configuration.

Do you have any links or examples for OAuth2 authentication and mocked tests?

Thanks.

Regards.

Comment From: philwebb

We have a few tests that make use of @WithMockUser (for example MockMvcSecurityIntegrationTests). I'm not sure we have any that use OAuth2.

If you think you've found a bug, please could you provide a minimal sample application that replicates the problem.

Comment From: marouj

Ok there the files to reproduce.

WebSecurityConfig.java :

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

  @Value("${spring.profiles.active}")
  private String activeProfile;

  @Value("${app.security.okta.oauth.enabled}")
  private boolean oktaEnabled;

  private final CustomFilter customFilter;

  @Autowired
  public WebSecurityConfig(CustomFilter customFilter) {
    this.customFilter = customFilter;
  }

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    if (activeProfile.equals("prod") || (activeProfile.equals("dev") && oktaEnabled)) {
      http
              .oauth2Login(Customizer.withDefaults())
              .addFilterBefore(customFilter,UsernamePasswordAuthenticationFilter.class)
              .cors(Customizer.withDefaults())
              .csrf(csrf -> csrf.disable())
              .authorizeRequests(authorize -> authorize
                      .requestMatchers(new AntPathRequestMatcher("/**/*.{js,html,css}")).permitAll()
                      .requestMatchers(new AntPathRequestMatcher("/"),
                              new AntPathRequestMatcher("/api/logout"),
                              new AntPathRequestMatcher("/ping"),
                              new AntPathRequestMatcher("/absoluteURL"),
                              new AntPathRequestMatcher("/absoluteURLWithFilter")).permitAll()
                      .requestMatchers(new AntPathRequestMatcher("/swagger-ui/**"),
                              new AntPathRequestMatcher("/api-docs/**")).permitAll()
                      .requestMatchers(new AntPathRequestMatcher("/s3")).permitAll()
                      .anyRequest().authenticated());
    } else {
      http
              .oauth2Login(Customizer.withDefaults())
              .addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class)
              .cors(Customizer.withDefaults())
              .csrf(csrf -> csrf.disable())
              .authorizeRequests().anyRequest().permitAll();
      http.headers(h -> h
              .frameOptions(f -> f
                      .sameOrigin()));
    }
    return http.build();
  }

  @Bean
  public CorsConfigurationSource corsConfigurationSource() {
    final CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOriginPatterns(List.of("sampleUri"));
    configuration.setAllowedMethods(List.of("*"));

    configuration.setAllowCredentials(true);

    configuration.setAllowedHeaders(Arrays.asList("*"));


    configuration.setAllowedHeaders(List.of("*"));

    final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
  }

  @Bean
  public RequestCache refererRequestCache() {
    return new HttpSessionRequestCache() {
      @Override
      public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
        String referrer = request.getHeader("referer");
        if (referrer != null) {
          request.getSession().setAttribute("SPRING_SECURITY_SAVED_REQUEST",
                  new SimpleSavedRequest(referrer));
        }
      }
    };
  }

} 

CustomFilter.java

@Component
public class CustomFilter extends OncePerRequestFilter {
  private static final Logger LOGGER = LoggerFactory.getLogger(CustomFilter.class);


  @Override
  public void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse,
                               FilterChain filterChain)
          throws IOException, ServletException {


    ContentCachingRequestWrapper req = new ContentCachingRequestWrapper(servletRequest);

    LOGGER.info("---> {} from:{}", req.getRequestURI(), servletRequest.getHeader("referer"));

    getHeadersInfo(req);
    ContentCachingResponseWrapper resp = new ContentCachingResponseWrapper(servletResponse);

    if (isFromAriane(servletRequest.getHeader("referer"))) {
      SecurityContext context = SecurityContextHolder.createEmptyContext();
      context.setAuthentication(new ArianeAuthentication());
      SecurityContextHolder.setContext(context);
    }

    filterChain.doFilter(req, resp);

    // Get Cache
    byte[] responseBody = resp.getContentAsByteArray();
    LOGGER.info("<--- {} {}", resp.getStatus(), new String(responseBody, StandardCharsets.UTF_8));

    // Finally remember to respond to the client with the cached data.
    resp.copyBodyToResponse();
  }

  private Map<String, String> getHeadersInfo(HttpServletRequest request) {

    Map<String, String> map = new HashMap<>();

    Enumeration<String> headerNames = request.getHeaderNames();
    while (headerNames.hasMoreElements()) {
      String key = headerNames.nextElement();
      String value = request.getHeader(key);
      map.put(key, value);
    }

    return map;
  }

  public boolean isFromAriane(String from) {
    return from != null && from.contains("arianeUri");
  }


}

And here my controller tests class FamilyTest.java

@WebMvcTest(FamilyRestController.class)
class FamilyTest {

  protected org.springframework.security.core.userdetails.User loggedUser;
  @Autowired
  protected WebApplicationContext context;
  @MockBean
  OidcUser oAuth2User;
  @MockBean
  OktaService oktaService;
  @MockBean
  HttpSession session;
  @Autowired
  private MockMvc mvc;
  @MockBean
  private FamilyService familyService;

  @BeforeEach
  void setUp() {
    mvc = MockMvcBuilders
            .webAppContextSetup(context)
            .apply(springSecurity())
            .build();

    loggedUser = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
  }

  @Test
  @WithMockUser("testAuthenticatedUser")
  void should_post_family_with_status_201() throws Exception {

    // GIVEN
    FamilyCreateRequest family = new FamilyCreateRequest("family1", "data", "fam1");
    FamilyCrudResponse familyCreateResponse = new FamilyCrudResponse(1L, "family1", "data", "fam1");

    // WHEN
    when(familyService.save(any(), any(), any())).thenReturn(familyCreateResponse);

    // THEN
    this.mvc.perform(post("/family")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(new Gson().toJson(family))
                    .with(oidcLogin().oidcUser(oAuth2User).idToken(token -> token.claim("sub", "email@exemple.com"))
                            .authorities(new SimpleGrantedAuthority("read"))))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.familyName").value("family1"))
            .andExpect(jsonPath("$.scope").value("qae"))
            .andExpect(jsonPath("$.baseRef").value("fam1"));
  }

}

Comment From: wilkinsona

@marouj Thanks. Can you please turn those code snippets into a complete sample that reproduces the problem? We'd prefer not to have to guess the exact details of your application's dependencies, configuration properties, and so on.

Comment From: spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: marouj

Hi, yes I’m currently trying to reproduce the issue in a poc to share it with you.

Thanks you

Comment From: spring-projects-issues

Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.