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

9

29

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
}
Udela answered 29/4, 2020 at 11:19 Comment(4)
One way is to disable security in case of test profile. So your SecurityConfig bean should not be initialized in case of test profile.Bespectacled
@Bespectacled can you please provide a code example?Udela
You need to pass JWT token as additional HTTP header, Jhipster's sample application has such unit test: github.com/jhipster/jhipster-sample-app/blob/master/src/test/…Bestrew
You should consider changing your accepted answer which is not in line with the manualGyn
C
28

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);
    }
Circumfluent answered 14/5, 2020 at 6:17 Comment(6)
I think you are missing the @Bean on the JwtDecoder in the TestSecurityConfig. At least I got it working by adding it plus @MissingOnConditionalBean on the real JwtDecoder bean definition.Deferential
How do you use request builder then? Because that is used more or less in @WebMvcTest which you cannot use with @SpringBootTest.Humidify
I am getting tokenValue cannot be empty error. any idea ?Diamagnet
The answer below using a @MockBean JwtDecoder and setting the test security context .with(SecurityMockMvcRequestPostProcessors.jwt()) is a way better optionGyn
I can't see that this is best practice, Spring Security allows you to mock JWT tokens docs.spring.io/spring-security/site/docs/5.2.0.RELEASE/…Handset
I have got the similar issue. I cannot fix it. I hope you can help me. Here is the link : #77078157Elysha
O
13

For me, I made it pretty simple.

I don't want to actually check for the JWT token, this can also be mocked.

Have a look at this security config.

@Override
    public void configure(HttpSecurity http) throws Exception {

        //@formatter:off
        http
            .cors()
            .and()
            
            .authorizeRequests()
                .antMatchers("/api/v1/orders/**")
                .authenticated()
            .and()
            .authorizeRequests()
                .anyRequest()
                .denyAll()
            .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            
            .and()
            .oauth2ResourceServer()
            .jwt();

Then in my test, I make use of two thing

  • Provide a mock bean for the jwtDecoder
  • Use the SecurityMockMvcRequestPostProcessors to mock the JWT in the request. This is available in the following dependency
         <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>

And Here is how it's done.

@SpringBootTest
@AutoConfigureMockMvc
public class OrderApiControllerIT {

    @Autowired
    protected MockMvc mockMvc;

    @MockBean
    private JwtDecoder jwtDecoder;
 
    @Test
    void testEndpoint() {

     MvcResult mvcResult = mockMvc.perform(post("/api/v1/orders")
                .with(SecurityMockMvcRequestPostProcessors.jwt())
                .content(jsonString)
                .contentType(MediaType.APPLICATION_JSON)
            )
            .andDo(print())
            .andExpect(status().is2xxSuccessful())
            .andReturn();

}

That's it and it should work.

Og answered 19/1, 2022 at 12:57 Comment(3)
Thanks! The @MockBean annotation is super useful!Gibbet
This actually is the best answer. An alternative is @WithMockJwtAuth from github.com/ch4mpy/spring-addonsGyn
For my tests, I don't need to mock jwtDecoder. SecurityMockMvcRequestPostProcessors.jwt() is enough.Supranational
J
4

For others like me, who after gathering information from what seems like a gazillion StackOverlow answers on how to do this, here is the summary of what ultimately worked for me (using Kotlin syntax, but it is applicable to Java as well):

Step 1 - Define a custom JWT decoder to be used in tests

Notice the JwtClaimNames.SUB entry - this is the user name which will ultimately be accessible via authentication.getName() field.

val jwtDecoder = JwtDecoder {
        Jwt(
                "token",
                Instant.now(),
                Instant.MAX,
                mapOf(
                        "alg" to "none"
                ),
                mapOf(
                        JwtClaimNames.SUB to "testUser"
                )
        )
}

Step 2 - Define a TestConfiguration

This class goes in your test folder. We do this to replace real implementation with a stub one which always treats the user as authenticated.

Note that we are not done yet, check Step 3 as well.

@TestConfiguration
class TestAppConfiguration {

    @Bean // important
    fun jwtDecoder() {
        // Initialize JWT decoder as described in step 1
        // ...

        return jwtDecoder
    }

}

Step 3 - Update your primary configuration to avoid bean conflict

Without this change your test and production beans would clash, resulting in a conflict. Adding this line delays the resolution of the bean and lets Spring prioritise test bean over production one.

There is a caveat, however, as this change effectively removes bean conflict protection in production builds for JwtDecoder instances.

@Configuration
class AppConfiguration {

    @Bean
    @ConditionalOnMissingBean // important
    fun jwtDecoder() {
        // Provide decoder as you would usually do
    }

}

Step 4 - Import TestAppConfiguration in your test

This makes sure that your test actually takes TestConfiguration into account.

@SpringBootTest
@Import(TestAppConfiguration::class)
class MyTest {

    // Your tests

}

Step 5 - Add @WithMockUser annotation to your test

You do not really need to provide any arguments to the annotation.

@Test
@WithMockUser
fun myTest() {
    // Test body
}

Step 6 - Provide Authentication header during the test

mockMvc
    .perform(
        post("/endpointUnderTest")
            .header(HttpHeaders.AUTHORIZATION, "Bearer token") // important
    )
    .andExpect(status().isOk)
Jerome answered 18/6, 2021 at 21:2 Comment(1)
You should not try to build valid JWTs or hack the decoder. Instead, keep your security config and have the test security context be set either manually or with one of the tools designed for that (see my answer or the one from Amrut Prabhu)Gyn
P
3

I am using the JwtAuthenticationToken from the Security Context. The @WithMockUser annotation is creating a Username-based Authentication Token.

I wrote my own implementation of @WithMockJwt:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@WithSecurityContext(factory = WithMockJwtSecurityContextFactory.class)
public @interface WithMockJwt {

    long value() default 1L;

    String[] roles() default {};

    String email() default "[email protected]";

}

And the related factory:

public class WithMockJwtSecurityContextFactory implements WithSecurityContextFactory<WithMockJwt> {
    @Override
    public SecurityContext createSecurityContext(WithMockJwt annotation) {
        val jwt = Jwt.withTokenValue("token")
                .header("alg", "none")
                .claim("sub", annotation.value())
                .claim("user", Map.of("email", annotation.email()))
                .build();

        val authorities = AuthorityUtils.createAuthorityList(annotation.roles());
        val token = new JwtAuthenticationToken(jwt, authorities);

        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(token);
        return context;

    }
}

And now I can annotate test with:

    @Test
    @WithMockJwt
    void test() {

     ...omissis...
Peer answered 26/1, 2023 at 15:27 Comment(1)
I provide with quite more advanced annotations in this lib. See my answer and or my github repo for detailsGyn
G
1

First, @SpringBootTest is for integration testing. Controllers should be unit tested with @WebMvcTest (in servlets) or @WebFluxTest (in reactive apps), with a @MockBean for each autowired dependency.

Second, you should neither deactivate security (as done in one of the answers) nor try to build valid JWTs or hack the decoder (as done in the accepted answer). Instead, keep your security config as it is and manually set the test security context with an Authentication instance of your choice (SecurityContextHolder.getContext().setAuthentication(auth);) or use some existing tools to do it for you.

spring-security-test comes with SecurityMockMvcRequestPostProcessors.jwt() and SecurityMockServerConfigurers.mockJwt() since version 5.2 which was released the 30th of september 2019. Usage as follow:

@Test
void givenUserAuthenticated_whenGetGreet_thenOk() throws Exception {
    api.perform(get("/greet").with(SecurityMockMvcRequestPostProcessors.jwt()
                .authorities(new SimpleGrantedAuthority("NICE"), new SimpleGrantedAuthority("AUTHOR"))))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.body").value("Hi user! You are granted with: [NICE, AUTHOR]."));
}
@Test
void givenUserHasNiceMutator_whenGetRestricted_thenOk() throws Exception {
    api.mutateWith(SecurityMockServerConfigurers.mockJwt()
            .authorities(new SimpleGrantedAuthority("NICE"), new SimpleGrantedAuthority("AUTHOR")))
        .get().uri("/restricted").exchange()
        .expectStatus().isOk()
        .expectBody(MessageDto.class).isEqualTo(new MessageDto("You are so nice!"));
}

I also wrote test annotations for OAuth2 in spring-addons-oauth2-test. Two might be of most interest in the case of a resource server with JWT decoder:

  • @WithMockAuthentication to be used in cases where mocking authorities is enough (and optionally username or actual Authentication implementation type). This annotation builds an Authentication mock, pre-configured with what you provide as annotation arguments.
  • @WithJwt when you want full control on JWT claims and use the actual authentication converter to setup the test security context. This one is a bit more advanced: it uses the JSON file or String passed as argument to build a org.springframework.security.oauth2.jwt.Jwt instance (not an actual base64 encoded JWT string, but what is built after JWT decoding and validation) and then provide it as input to the Converter<Jwt, ? extends AbstractAuthenticationToken> picked from the security configuration. This means that the actual Authentication implementation (JwtAuthenticationToken by default), as well as username, authorities and claims will be the exact same as at runtime for the same JWT payload.

When using @WithJwt, extract the claims from tokens for a few representative users and dump the content as JSON files in test resources. Using a tool like https://jwt.io and real tokens it is rather simple. You could also write the JSON yourself, starting with the sample below.

@Test
@WithMockAuthentication("NICE")
void givenMockAuthenticationWithNice_whenGetRestricted_thenOk() throws Exception {
    api.perform(get("/restricted"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.body").value("You are so nice!"));
}

@Test
@WithJwt("tonton-pirate.json")
void givenJwtWithNice_whenGetRestricted_thenOk() throws Exception {
    api.perform(get("/restricted"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.body").value("You are so nice!"));
}

With src/test/resources/tonton-pirate.json being a plain JSON file containing the claim-set (user authorities in the private claim as expected by the authentication converter). Sample for Keycloak and realm roles:

{
  "preferred_username": "tonton-pirate",
  "scope": "profile email",
  "email": "[email protected]",
  "email_verified": true,
  "realm_access": {
    "roles": [
      "NICE",
      "AUTHOR"
    ]
  }
}

More samples on my Github repo for spring-addons.

Gyn answered 31/7, 2023 at 3:4 Comment(4)
What if I'm really doing integration testing with @SpringBootTest?Zaporozhye
@AutoConfigureMockMvc and use MockMvc as exposed here. Follow the link to "my repo" or refer to this Baeldung article.Gyn
MockMvc is not the integration testing, so that may be the answer to OP but not for integration testing.Zaporozhye
Why couldn't MockMvc be used in integration-testing? Integration testing is when you test more than one component at a time, like when injecting real @Bean instances from an application context built by Spring in @SpringBootTest (as opposed to injecting mocks, like done when unit-testing using @WebMvcTest).Gyn
B
0

SecurityConfig bean can be loaded conditionally as,

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  @Profile("!test")
  public WebSecurityConfigurerAdapter securityEnabled() {

    return new WebSecurityConfigurerAdapter() {

      @Override
      protected void configure(HttpSecurity http) throws Exception {
        // your code goes here
      }

    };
  }

  @Bean
  @Profile("test")
  public WebSecurityConfigurerAdapter securityDisabled() {

    return new WebSecurityConfigurerAdapter() {

      @Override
      protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().permitAll();
      }
    };
  }

}

So this bean won't be initialized in case of test profile. It means now security is disabled and all endpoints are accessible without any authorization header.

Now "test" profile needs to be active in case of running the tests, this can be done as,

@RunWith(SpringRunner.class)
@ActiveProfiles("test")
@WebMvcTest(UserRoundsController.class)
public class UserRoundsControllerTest extends AbstractUnitTests {

// your code goes here

}

Now this test is going to run with profile "test". Further if you want to have any properties related to this test, that can be put under src/test/resources/application-test.properties.

Hope this helps! please let me know otherwise.

Update: Basic idea is to disable security for test profile. In previous code, even after having profile specific bean, default security was getting enabled.

Bespectacled answered 29/4, 2020 at 15:22 Comment(4)
Sorry this does not seem to work - I have added the code as above and still get the same issue with getting 401 unauthorised. I have added my Abstract unit test class to my question to help..Udela
I am now getting the following error when I try this: @Order on WebSecurityConfigurers must be unique. Order of 100 was already used on com.ryd.pokerstats.pokerstats.auth.SecurityConfig$$EnhancerBySpringCGLIB$$f1a72b2@baa9ce4, so it cannot be used on com.ryd.pokerstats.pokerstats.auth.SecurityConfig$1@5b332439 too.Udela
This just disables authentication, and is not a solution. Ignoring the fact that this is adding a big risk of getting false positive test passes, it also does not cover scenarios where code depends on "current user". I have seen solutions like this ending up with developers sending "current user" as a request param which is a big security issue.Dabney
Do not deactivate security during tests. Set the test security context instead, or better, have tools do it for you. See my answer for details.Gyn
C
0

You can get the Bearer token and pass it on as a HTTP Header. Below is a sample snippet of the Test Method for your reference,

@Test
public void existentUserCanGetTokenAndAuthentication() throws Exception {
   String username = "existentuser";
   String password = "password";

   String body = "{\"username\":\"" + username + "\", \"password\":\" 
              + password + "\"}";

   MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/token")
          .content(body))
          .andExpect(status().isOk()).andReturn();

   String response = result.getResponse().getContentAsString();
   response = response.replace("{\"access_token\": \"", "");
   String token = response.replace("\"}", "");

   mvc.perform(MockMvcRequestBuilders.get("/users/" + userId + "/rounds")
      .header("Authorization", "Bearer " + token))
      .andExpect(status().isOk());
}
Convivial answered 11/5, 2020 at 18:52 Comment(1)
This requires a valid token and 1) tokens expire (test will start failing some day) and 2) requires an actual JWT decoder which will need the authorization server to be reachable to fetch public key (this not a unit test any more). Instead, use a mocked JWT decoder and set the test security context, or better, have tool do it for you. See my answer for details.Gyn
B
0

For Spring WebFlux application you might also use mockJwt() with WebTestClient, like here (example in Kotlin):

client.mutateWith(mockJwt()).get().uri("/endpoint").exchange()

It lets you also configure authorities and claims, for example:

.mutateWith(
    mockJwt()
        .authorities(SimpleGrantedAuthority("my-authority"))
        .jwt {
            it.claim("sub", "my-claim")
        }
)

For me it worked out of the box, without the need of doing additional configuration or including some beans.

More info can be found in the Spring docs.

Borchert answered 26/1 at 19:53 Comment(0)
M
-1

create application.properties in test/resources (it will override main but for test stage only)

turnoff security by specifyig:

security.ignored=/**
security.basic.enable= false
spring.autoconfigure.exclude= org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
Macassar answered 9/5, 2020 at 22:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.