37

I am creating a backend using Spring Boot and I have just added JWT security to it.

I have done some tests using a REST Client and the JWT security is working fine, however all of my unit tests are now returning a 403 error code.

I've added the @WithMockUser annotation to them, but they are still not working:

@Test
@WithMockUser
public void shouldRedirectToInstaAuthPage() throws Exception {
    mvc.perform(MockMvcRequestBuilders.get("/instaAuth")).andExpect(status().is3xxRedirection());
}

Is there some other configuration that I am missing here?

Here is the security configuration:

@Configuration
@EnableWebSecurity
public class ServerSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
      protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers(HttpMethod.POST, "/login").permitAll()
            .anyRequest().authenticated()
            .and()
            // We filter the api/login requests
            .addFilterBefore(new JWTLoginFilter("/login", authenticationManager()),
                    UsernamePasswordAuthenticationFilter.class)
            // And filter other requests to check the presence of JWT in header
            .addFilterBefore(new JWTAuthenticationFilter(),
                    UsernamePasswordAuthenticationFilter.class);
      }

      @Override
      protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // Create a default account
        auth.inMemoryAuthentication()
            .withUser("john")
            .password("123")
            .roles("ADMIN");
      }
}

And Method security:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return new OAuth2MethodSecurityExpressionHandler();
    }

}
Felipe
  • 6,312
  • 11
  • 52
  • 70
  • 1
    Take a look at this previous [answer](https://stackoverflow.com/a/44502136/1078565) to a similar question. – Kyle Anderson Jul 21 '17 at 16:43
  • Hi @punkrocker27ka, thanks for the reply. In that post, the problem is happening with an Oauth2 authentication configuration, but I am using JWT, so I don't have a Resource Server. – Felipe Jul 21 '17 at 18:32
  • 1
    They aren't mutually exclusive. If possible, could you share a minimal example project on GitHub? That'll make it easier to troubleshoot. – Kyle Anderson Jul 21 '17 at 20:59
  • 1
    Ok, let me try to do what they suggested on that topic. I will also try to upload something on GitHub. – Felipe Jul 21 '17 at 22:17
  • 1
    Hi, I generated the JWT token myself and added to the tests and everything is working now. I hope I am not doing a bad practice here. I will post an answer with the details. – Felipe Jul 21 '17 at 22:33

2 Answers2

40

I believe that I solved the problem (and I hope I am not doing a bad practice or creating a security vulnerability on my backend).

I followed @punkrocker27ka's advice and looked at this answer. In it they say that they are generating an Oauth token manually for the tests, so I decided to do the same thing for my JWT token.

So I updated my class that generates the JWT tokens and validates them to be like this:

public class TokenAuthenticationService {

    static final long EXPIRATIONTIME = 864_000_000; // 10 days
    static final String SECRET = "ThisIsASecret";
    static final String TOKEN_PREFIX = "Bearer";
    static final String HEADER_STRING = "Authorization";

    public static void addAuthentication(HttpServletResponse res, String username) {

        String jwt = createToken(username);

        res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + jwt);
    }

    public static Authentication getAuthentication(HttpServletRequest request) {
        String token = request.getHeader(HEADER_STRING);
        if (token != null) {
            // parse the token.
            String user = Jwts.parser()
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                    .getBody()
                    .getSubject();

            return user != null ?
                    new UsernamePasswordAuthenticationToken(user, null, Collections.emptyList()) :
                        null;
        }
        return null;
    }

    public static String createToken(String username) {
        String jwt = Jwts.builder()
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();

        return jwt;
    }
}

And then I created a new test for it:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class TokenAuthenticationServiceTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void shouldNotAllowAccessToUnauthenticatedUsers() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/test")).andExpect(status().isForbidden());
    }

    @Test
    public void shouldGenerateAuthToken() throws Exception {
        String token = TokenAuthenticationService.createToken("john");

        assertNotNull(token);
        mvc.perform(MockMvcRequestBuilders.get("/test").header("Authorization", token)).andExpect(status().isOk());
    }

}

Then I ran the tests and they passed, so the token was accepted without the need for the @WithMockUser annotation. I will add this to my other tests classes.

PS: The test endpoint is below.

/**
 * This controller is used only for testing purposes.
 * Especially to check if the JWT authentication is ok.
 */
@RestController
public class TestController {

    @RequestMapping(path = "/test", method = RequestMethod.GET)
    public String testEndpoint() {
        return "Hello World!";
    }
}
Felipe
  • 6,312
  • 11
  • 52
  • 70
  • 1
    In your answer, you are giving a clue that "John" named user exists in your actual database. Maybe before calling "mvc.perform" function, you should mock your user repository. For example, you can call this " given(userRepository.findUserById("john")).willReturn(new User(("john"));". In the above example, I am assuming you have a user repository that has findUserById function. Doing that, you might not give any clue to the one who is reading your code. – burakim Dec 06 '18 at 06:15
  • 1
    Nice answer @Felipe. For a missing JWT, however, APIs should return `401 Unauthorized`, not `403 Forbidden`. Hence `mvc.perform(MockMvcRequestBuilders.get("/test")).andExpect(status().isUnauthorized());` would be more appropriate. – Andrea Ligios Nov 25 '19 at 11:40
  • 1
    Thank you @Filipe, it is a nice and clear solution! – Vaclav Vlcek May 03 '23 at 04:30
13

One thing you need to be aware of when testing using this createToken() method is that your tests cannot test for a nonexistent user.
This is because createToken() only makes a JWT token based off of the string you put into it.
If you want to make sure nonexistent users cannot gain access, I recommend making your createToken() method private and instead use requests to gain the token, like this:

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

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

    MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/v2/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("/test")
        .header("Authorization", "Bearer " + token))
        .andExpect(status().isOk());
}

In a similar way, you can show that a nonexistent user will not be able to get this result:

@Test
public void nonexistentUserCannotGetToken() throws Exception {
    String username = "nonexistentuser";
    String password = "password";

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

    mvc.perform(MockMvcRequestBuilders.post("/v2/token")
            .content(body))
            .andExpect(status().isForbidden()).andReturn();
}
Austin
  • 2,982
  • 2
  • 28
  • 36