Dheeraj Joshi
Dheeraj Joshi

Reputation: 3147

Mock JWT AuthenticationPrincipal while testing REST end points

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

Answers (1)

Dheeraj Joshi
Dheeraj Joshi

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

Related Questions