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.