web-dev-qa-db-ja.com

JWTトークンセキュリティを使用したSpring Boot Unitテスト

Spring Bootを使用してバックエンドを作成していますが、JWTセキュリティを追加したばかりです。

REST Clientを使用していくつかのテストを実行し、JWTセキュリティは正常に機能していますが、ユニットテストはすべて403エラーコードを返しています。

@WithMockUserアノテーションを追加しましたが、まだ機能していません。

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

ここに欠けている他の構成はありますか?

セキュリティ構成は次のとおりです。

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

メソッドのセキュリティ:

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

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

}
14
Felipe

私は問題を解決したと信じています(そして、バックエンドで悪い習慣をしたり、セキュリティの脆弱性を作ったりしないことを望みます)。

@ punkrocker27kaのアドバイスに従い、 この回答 を見ました。その中で、彼らはテストのためにOauthトークンを手動で生成しているので、私はJWTトークンに対して同じことをすることにしました。

そこで、JWTトークンを生成し、それらを次のように検証するクラスを更新しました。

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

そして、そのための新しいテストを作成しました。

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

}

その後、テストを実行して合格したので、@WithMockUserアノテーションを必要とせずにトークンが受け入れられました。これを他のテストクラスに追加します。

PS:テストエンドポイントは以下です。

/**
 * 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

このcreateToken()メソッドを使用してテストするときに注意する必要があることの1つは、存在しないユーザーに対してテストをテストできないことです。これは、createToken()が、入力した文字列に基づいてJWTトークンのみを作成するためです。存在しないユーザーがアクセスできないようにするには、次のようにcreateToken()メソッドをプライベートにし、代わりにリクエストを使用してトークンを取得することをお勧めします。

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

同様の方法で、存在しないユーザーがこの結果を取得できないことを示すことができます。

@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