javaspringspring-bootunit-testingjwt

How to mock JWT authentication in a Spring Boot Unit Test?


I have added JWT Authentication using Auth0 to my Spring Boot REST API following this example.

Now, as expected, my previously working Controller unit tests give a response code of401 Unauthorized rather than 200 OK as I am not passing any JWT in the tests.

How can I mock the JWT/Authentication part of my REST Controller tests?

Unit test class

@AutoConfigureMockMvc
public class UserRoundsControllerTest extends AbstractUnitTests {

    private static String STUB_USER_ID = "user3";
    private static String STUB_ROUND_ID = "7e3b270222252b2dadd547fb";

    @Autowired
    private MockMvc mockMvc;

    private Round round;

    private ObjectId objectId;

    @BeforeEach
    public void setUp() {
        initMocks(this);
        round = Mocks.roundOne();
        objectId = Mocks.objectId();
    }

    @Test
    public void shouldGetAllRoundsByUserId() throws Exception {

        // setup
        given(userRoundService.getAllRoundsByUserId(STUB_USER_ID)).willReturn(
                Collections.singletonList(round));

        // mock the rounds/userId request
        RequestBuilder requestBuilder = Requests.getAllRoundsByUserId(STUB_USER_ID);

        // perform the requests
        MockHttpServletResponse response = mockMvc.perform(requestBuilder)
                .andReturn()
                .getResponse();

        // asserts
        assertNotNull(response);
        assertEquals(HttpStatus.OK.value(), response.getStatus());
    }

    //other tests
}

Requests class (used above)

public class Requests {

    private Requests() {}

    public static RequestBuilder getAllRoundsByUserId(String userId) {
        return MockMvcRequestBuilders
                .get("/users/" + userId + "/rounds/")
                .accept(MediaType.APPLICATION_JSON)
                .contentType(MediaType.APPLICATION_JSON);
    }
}

Spring Security Config

/**
 * Configures our application with Spring Security to restrict access to our API endpoints.
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Value("${auth0.audience}")
    private String audience;

    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
    private String issuer;

    @Override
    public void configure(HttpSecurity http) throws Exception {
            /*
            This is where we configure the security required for our endpoints and setup our app to serve as
            an OAuth2 Resource Server, using JWT validation.
            */

        http.cors().and().csrf().disable().sessionManagement().
                sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
                .mvcMatchers(HttpMethod.GET, "/users/**").authenticated()
                .mvcMatchers(HttpMethod.POST, "/users/**").authenticated()
                .mvcMatchers(HttpMethod.DELETE, "/users/**").authenticated()
                .mvcMatchers(HttpMethod.PUT, "/users/**").authenticated()
                .and()
                .oauth2ResourceServer().jwt();
    }

    @Bean
    JwtDecoder jwtDecoder() {
            /*
            By default, Spring Security does not validate the "aud" claim of the token, to ensure that this token is
            indeed intended for our app. Adding our own validator is easy to do:
            */

        NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
                JwtDecoders.fromOidcIssuerLocation(issuer);

        OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
        OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer,
                audienceValidator);

        jwtDecoder.setJwtValidator(withAudience);

        return jwtDecoder;
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

Abstract Unit test class

@ExtendWith(SpringExtension.class)
@SpringBootTest(
        classes = PokerStatApplication.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
public abstract class AbstractUnitTests {
    // mock objects etc
}

Solution

  • If I understand correctly your case there is one of the solutions.

    In most cases, JwtDecoder bean performs token parsing and validation if the token exists in the request headers.

    Example from your configuration:

        @Bean
        JwtDecoder jwtDecoder() {
            /*
            By default, Spring Security does not validate the "aud" claim of the token, to ensure that this token is
            indeed intended for our app. Adding our own validator is easy to do:
            */
    
            NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
                JwtDecoders.fromOidcIssuerLocation(issuer);
    
            OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
            OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
            OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
    
            jwtDecoder.setJwtValidator(withAudience);
    
            return jwtDecoder;
        }
    

    So for the tests, you need to add stub of this bean and also for replacing this bean in spring context, you need the test configuration with it.

    It can be some things like this:

    @TestConfiguration
    public class TestSecurityConfig {
    
      static final String AUTH0_TOKEN = "token";
      static final String SUB = "sub";
      static final String AUTH0ID = "sms|12345678";
    
      @Bean
      public JwtDecoder jwtDecoder() {
        // This anonymous class needs for the possibility of using SpyBean in test methods
        // Lambda cannot be a spy with spring @SpyBean annotation
        return new JwtDecoder() {
          @Override
          public Jwt decode(String token) {
            return jwt();
          }
        };
      }
    
      public Jwt jwt() {
    
        // This is a place to add general and maybe custom claims which should be available after parsing token in the live system
        Map<String, Object> claims = Map.of(
            SUB, USER_AUTH0ID
        );
    
        //This is an object that represents contents of jwt token after parsing
        return new Jwt(
            AUTH0_TOKEN,
            Instant.now(),
            Instant.now().plusSeconds(30),
            Map.of("alg", "none"),
            claims
        );
      }
    
    }
    

    For using this configuration in tests just pick up this test security config:

    @SpringBootTest(classes = TestSecurityConfig.class)

    Also in the test request should be authorization header with a token like Bearer .. something.

    Here is an example regarding your configuration:

        public static RequestBuilder getAllRoundsByUserId(String userId) {
    
            return MockMvcRequestBuilders
                .get("/users/" + userId + "/rounds/")
                .accept(MediaType.APPLICATION_JSON)
                .header(HttpHeaders.AUTHORIZATION, "Bearer token"))
                .contentType(MediaType.APPLICATION_JSON);
        }