web-dev-qa-db-fra.com

Tests unitaires de démarrage de printemps avec sécurité des jetons JWT

Je crée un backend à l'aide de Spring Boot et je viens d'y ajouter la sécurité JWT.

J'ai fait quelques tests à l'aide d'un client REST et la sécurité JWT fonctionne bien, mais tous mes tests unitaires renvoient maintenant un code d'erreur 403.

J'ai ajouté le @WithMockUser annotation pour eux, mais ils ne fonctionnent toujours pas:

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

Y a-t-il une autre configuration qui me manque ici?

Voici la configuration de sécurité:

@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");
      }
}

Et la sécurité de la méthode:

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

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

}
14
Felipe

Je crois que j'ai résolu le problème (et j'espère que je ne fais pas une mauvaise pratique ou que je ne crée pas de vulnérabilité de sécurité sur mon backend).

J'ai suivi les conseils de @ punkrocker27ka et j'ai regardé cette réponse . Ils y disent qu'ils génèrent un jeton Oauth manuellement pour les tests, j'ai donc décidé de faire la même chose pour mon jeton JWT.

J'ai donc mis à jour ma classe qui génère les jetons JWT et les valide comme suit:

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;
    }
}

Et puis j'ai créé un nouveau test pour cela:

@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());
    }

}

Ensuite, j'ai exécuté les tests et ils ont réussi, donc le jeton a été accepté sans avoir besoin du @WithMockUser annotation. Je vais l'ajouter à mes autres classes de tests.

PS: le point final de test est ci-dessous.

/**
 * 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!";
    }
}
14
Felipe

Une chose que vous devez savoir lorsque vous testez à l'aide de cette méthode createToken () est que vos tests ne peuvent pas tester pour un utilisateur inexistant. En effet, createToken () crée uniquement un jeton JWT à partir de la chaîne que vous y mettez. Si vous voulez vous assurer que les utilisateurs inexistants ne peuvent pas accéder, je recommande de rendre votre méthode createToken () privée et d'utiliser à la place des requêtes pour obtenir le jeton, comme ceci:

@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());
}

De la même manière, vous pouvez montrer qu'un utilisateur inexistant ne pourra pas obtenir ce résultat:

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

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

    mvc.perform(MockMvcRequestBuilders.post("/v2/token")
            .content(body))
            .andExpect(status().isForbidden()).andReturn();
}
4
Eric Hendrickson