Reputation: 3147
We are building REST API and adding AuthenticationPrincipal as method argument
@Override
public ResponseEntity<MappingJacksonValue> listProduct(
@Valid @RequestParam(value = "fields", required = false) final String fields,
@Valid @RequestParam(value = "offset", required = false) final Integer offset,
@Valid @RequestParam(value = "limit", required = false) final Integer limit,
@AuthenticationPrincipal final Jwt jwt) throws Exception {
//Extract roles from jwt and do advanced role checking.
}
While testing using mockmvc
@Autowired
private WebApplicationContext context;
@BeforeEach
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
@Test
void testGetListProductsWithFields() throws Exception {
Map<String, Object> attributeMap = new HashMap<>();
attributeMap.put(null, "");
//Code
final var result = mvc
.perform(get("/api" + requestFields)
.with(jwt().authorities(new OAuth2UserAuthority("ROLE_my_roll", attributeMap)))
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andReturn();
}
JWT token is getting initialised using constructor (org.springframework.security.oauth2.jwt.Jwt
) at ModelAttributeMethodProcessor
and since no token is being sent assertion errors are thrown. Idea is not to connect to auth provider to generate tokens
Call trace
Jwt(AbstractOAuth2Token).<init>(String, Instant, Instant) line: 55
Jwt.<init>(String, Instant, Instant, Map<String,Object>, Map<String,Object>) line: 69
NativeConstructorAccessorImpl.newInstance0(Constructor<?>, Object[]) line: not available [native method]
NativeConstructorAccessorImpl.newInstance(Object[]) line: 62
DelegatingConstructorAccessorImpl.newInstance(Object[]) line: 45
Constructor<T>.newInstance(Object...) line: 490
BeanUtils.instantiateClass(Constructor<T>, Object...) line: 204
ServletModelAttributeMethodProcessor(ModelAttributeMethodProcessor).constructAttribute(Constructor<?>, String, MethodParameter, WebDataBinderFactory, NativeWebRequest) line: 320
ServletModelAttributeMethodProcessor(ModelAttributeMethodProcessor).createAttribute(String, MethodParameter, WebDataBinderFactory, NativeWebRequest) line: 224
ServletModelAttributeMethodProcessor.createAttribute(String, MethodParameter, WebDataBinderFactory, NativeWebRequest) line: 85
ServletModelAttributeMethodProcessor(ModelAttributeMethodProcessor).resolveArgument(MethodParameter, ModelAndViewContainer, NativeWebRequest, WebDataBinderFactory) line: 139
HandlerMethodArgumentResolverComposite.resolveArgument(MethodParameter, ModelAndViewContainer, NativeWebRequest, WebDataBinderFactory) line: 121
ServletInvocableHandlerMethod(InvocableHandlerMethod).getMethodArgumentValues(NativeWebRequest, ModelAndViewContainer, Object...) line: 167
ServletInvocableHandlerMethod(InvocableHandlerMethod).invokeForRequest(NativeWebRequest, ModelAndViewContainer, Object...) line: 134
ServletInvocableHandlerMethod.invokeAndHandle(ServletWebRequest, ModelAndViewContainer, Object...) line: 105
RequestMappingHandlerAdapter.invokeHandlerMethod(HttpServletRequest, HttpServletResponse, HandlerMethod) line: 878
RequestMappingHandlerAdapter.handleInternal(HttpServletRequest, HttpServletResponse, HandlerMethod) line: 792
RequestMappingHandlerAdapter(AbstractHandlerMethodAdapter).handle(HttpServletRequest, HttpServletResponse, Object) line: 87
TestDispatcherServlet(DispatcherServlet).doDispatch(HttpServletRequest, HttpServletResponse) line: 1040
TestDispatcherServlet(DispatcherServlet).doService(HttpServletRequest, HttpServletResponse) line: 943
TestDispatcherServlet(FrameworkServlet).processRequest(HttpServletRequest, HttpServletResponse) line: 1006
TestDispatcherServlet(FrameworkServlet).doGet(HttpServletRequest, HttpServletResponse) line: 898
TestDispatcherServlet(HttpServlet).service(HttpServletRequest, HttpServletResponse) line: 626
TestDispatcherServlet(FrameworkServlet).service(HttpServletRequest, HttpServletResponse) line: 883
TestDispatcherServlet.service(HttpServletRequest, HttpServletResponse) line: 72
TestDispatcherServlet(HttpServlet).service(ServletRequest, ServletResponse) line: 733
MockFilterChain$ServletFilterProxy.doFilter(ServletRequest, ServletResponse, FilterChain) line: 167
MockFilterChain.doFilter(ServletRequest, ServletResponse) line: 134
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 320
FilterSecurityInterceptor.invoke(FilterInvocation) line: 126
FilterSecurityInterceptor.doFilter(ServletRequest, ServletResponse, FilterChain) line: 90
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334
ExceptionTranslationFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 118
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334
SessionManagementFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 137
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334
AnonymousAuthenticationFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 111
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334
SecurityContextHolderAwareRequestFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 158
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334
RequestCacheAwareFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 63
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334
BearerTokenAuthenticationFilter.doFilterInternal(HttpServletRequest, HttpServletResponse, FilterChain) line: 114
BearerTokenAuthenticationFilter(OncePerRequestFilter).doFilter(ServletRequest, ServletResponse, FilterChain) line: 119
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334
LogoutFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 116
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334
CsrfFilter(OncePerRequestFilter).doFilter(ServletRequest, ServletResponse, FilterChain) line: 103
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334
HeaderWriterFilter.doHeadersAfter(HttpServletRequest, HttpServletResponse, FilterChain) line: 92
HeaderWriterFilter.doFilterInternal(HttpServletRequest, HttpServletResponse, FilterChain) line: 77
HeaderWriterFilter(OncePerRequestFilter).doFilter(ServletRequest, ServletResponse, FilterChain) line: 119
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334
SecurityContextPersistenceFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 105
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334
WebAsyncManagerIntegrationFilter.doFilterInternal(HttpServletRequest, HttpServletResponse, FilterChain) line: 56
WebAsyncManagerIntegrationFilter(OncePerRequestFilter).doFilter(ServletRequest, ServletResponse, FilterChain) line: 119
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334
FilterChainProxy.doFilterInternal(ServletRequest, ServletResponse, FilterChain) line: 215
FilterChainProxy.doFilter(ServletRequest, ServletResponse, FilterChain) line: 178
SecurityMockMvcConfigurer$DelegateFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 132
MockFilterChain.doFilter(ServletRequest, ServletResponse) line: 134
MockMvc.perform(RequestBuilder) line: 183
Tried to pass jwt constructor parameters as request parameter.
Map<String, Object> attributeMap = new HashMap<>();
attributeMap.put(null, "");
//Code
final Map<String, String> headerMap = new HashMap<>();
headerMap.put("alg", "none");
final var mapper = new ObjectMapper();
final var jsonMap = mapper.writeValueAsString(headerMap);
final var result = mvc
.perform(get("/api" + requestFields)
.with(jwt().authorities(new OAuth2UserAuthority("ROLE_my_roll", attributeMap)))
.param("tokenValue", "tokenValue")
.param("!headers", jsonMap)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andReturn();
But it throws different error
Cannot convert value of type 'java.lang.String' to required type 'java.util.Map': no matching editors or conversion strategy found
Tried adding converter. But no luck
@Component
public class StringToMapConverter implements Converter<String, Map<String, Object>> {
@Override
public Map<String, Object> convert(final String source) {
try {
return new ObjectMapper().readValue(source,
new TypeReference<Map<String, Object>>() {});
} catch (final IOException e) {
throw new RuntimeException(e.getMessage());
}
}
@Override
public JavaType getInputType(final TypeFactory typeFactory) {
return typeFactory.constructFromCanonical(String.class.getName());
}
@Override
public JavaType getOutputType(final TypeFactory typeFactory) {
return typeFactory.constructFromCanonical(Map.class.getName());
}
}
I am able to solve this by extending WebMvcConfigurationSupport
and proving argumentResolver
//Test configuration
@ContextConfiguration
@EnableWebSecurity
@TestConfiguration
public class ProductApiConfiguration extends WebMvcConfigurationSupport {
@Override
protected void addArgumentResolvers(
final List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(getArgumentResolver());
}
private HandlerMethodArgumentResolver getArgumentResolver() {
return new HandlerMethodArgumentResolver() {
@Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.getParameterType().isAssignableFrom(Jwt.class);
}
@Override
public Object resolveArgument(final MethodParameter parameter,
final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory)
throws Exception {
final var roles = new JSONArray();
roles.add("my_role");
final Map<String, JSONArray> jsonMap = new HashMap<>();
jsonMap.put("roles", roles);
final var claimObject = new JSONObject(jsonMap);
final var jwt = Jwt.withTokenValue("token")
.header("alg", "none")
.claim("realm_access", claimObject)
.build();
final Collection<GrantedAuthority> authorities =
AuthorityUtils.createAuthorityList("ROLE_my_roll");
final var token = new JwtAuthenticationToken(jwt, authorities);
return token.getToken();
}
};
}
}
Since this is not per test i can't change the jwt token for each test and verify different scenarios.
Is there a simple way to mock the method argument in the controller class when an API is being tested by webmvc?
Upvotes: 2
Views: 2085
Reputation: 3147
We were able to solve this issue by setting SecurityContextHolder
@Autowired
private MockMvc mvc;
@Autowired
private WebApplicationContext context;
@MockBean
SecurityContext securityContext;
@Test
public void test() {
//some code
given(securityContext.getAuthentication())
.willReturn(Utils
.getMockJwtToken("my_role", "testSubject"));
SecurityContextHolder.setContext(securityContext);
final var result = this.mvc
.perform(get("/api" + requestFields)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andReturn();
}
And Test util class
public class Utils {
private Utils() {
}
public static JwtAuthenticationToken getMockJwtToken(String role, String subject){
final var roles = new JSONArray();
roles.add(role);
final Map<String, JSONArray> jsonMap = new HashMap<>();
jsonMap.put("roles", roles);
final var claimObject = new JSONObject(jsonMap);
final var jwt = Jwt.withTokenValue("token")
.header("alg", "none")
.claim("realm_access", claimObject)
.subject(subject)
.build();
final Collection<GrantedAuthority> authorities =
AuthorityUtils.createAuthorityList("ROLE_my_role));
return new JwtAuthenticationToken(jwt, authorities);
}
}
Upvotes: 3