Affects: \5.2.9 Affects: \security: 5.3.4
I have the following code, where testBWithAuth
throws a null pointer exception.
@SpringBootTest
@ContextConfiguration(classes = Configuration.class)
public class ApplicationTest {
private static String signingKey = "signingKey";
@TestConfiguration
@EnableResourceServer
public static class Configuration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.cors();
http.authorizeRequests().anyRequest().authenticated();
http.sessionManagement().sessionCreationPolicy(STATELESS);
}
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
@Bean
TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
JwtAccessTokenConverter accessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(signingKey);
return converter;
}
@Bean
RouterFunction<ServerResponse> routerFunction() {
final Builder routerBuilder = route();
routerBuilder
.GET("a", x -> ServerResponse.ok().body("A"))
.GET("b", x -> ServerResponse.ok().body("B"))
;
return routerBuilder.build();
}
}
MockMvc mockMvc;
@Autowired
WebApplicationContext context;
public static String createToken() {
final JwtBuilder jwtBuilder = Jwts.builder();
String token = jwtBuilder
.claim("user_name", "123")
.signWith(
SignatureAlgorithm.HS256, Base64.getEncoder().encodeToString(signingKey.getBytes()))
.compact();
return String.format("Bearer %1$s", token);
}
@Test
void testAWithAuth() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(springSecurity()).build();
mockMvc.perform(get("/a")
.header("Authorization", createToken())
).andExpect(status().isOk());
}
@Test
void testBWithAuth() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(springSecurity())
.build();
mockMvc.perform(get("/b")
.header("Authorization", createToken())
)
.andExpect(status().isOk());
}
@Test
void testAWithoutAuth() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
mockMvc.perform(get("/a")).andExpect(status().isOk());
}
@Test
void testBWithoutAuth() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
mockMvc.perform(get("/b")).andExpect(status().isOk());
}
}
When adding more endpoints, they will also fail with a null pointer exception, only the first route can succeed.
I could trigger the NPE with the resource server only and not with "normal" JWT authentication. Also when disabling springSecurity (see tests), the code seems to work. However, when tracking down this issue I stumbled mainly over spring-framework classes and not over spring-security classes. This is why I am posting the issue here.
My current analysis is the following:
org.springframework.test.web.servlet.MockMvc.MVC_RESULT_ATTRIBUTE
gets deleted before the handler method is called. Thus, after the execution of the handler function, when writing back the result, the search for this attribute results in null
. Thus setting the value results in a NullPointerExceptino.
In fact, during execution of the springSecurity filter chain several attributes on the request are deleted since the restoreAttributes
methods of RequestPredicates
are unable to restore attributes in the map implementation of HandlerMappingIntrospector.RequestAttributeChangeIgnoringWrapper
.
Currently, as a workaround I disable the CorsFilter in the FilterChainProxy.
Further Observations
The same setup worked for a standard setup with mappings via annotations only. However, when using WebMVC.fn those fail on tests as well. The issue does not seem to occur when starting the server itself (At least the mappings via annotations still seem to work).
Pointers to source code:
https://github.com/spring-projects/spring-framework/blob/fee8abfa5f54b9fa45f6f5a491bbbec1ac53b0a0/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java#L156-L164
getHandler
calls eventually route
of RouterFunctionBuilder
https://github.com/spring-projects/spring-framework/blob/fee8abfa5f54b9fa45f6f5a491bbbec1ac53b0a0/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java#L359-L368
Which in turn will test
a route and after the test restoreAttributes
https://github.com/spring-projects/spring-framework/blob/fee8abfa5f54b9fa45f6f5a491bbbec1ac53b0a0/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java#L294-L299
putAll
is implemented via put
which uses setAttribute
https://github.com/spring-projects/spring-framework/blob/fee8abfa5f54b9fa45f6f5a491bbbec1ac53b0a0/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java#L233-L249
Comment From: snicoll
Thanks for the detailed analysis but we'd need that code in text to move in an actual project that we can run ourselves. Copying the code in a new project takes time and we can overlook something that you haven't shown in the description.
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: 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.