Протестируйте JwtDecoder в @WebMvcTest с помощью Spring Security - PullRequest
1 голос
/ 08 ноября 2019

Я использую Spring Boot 2.2.1 с spring-security-oauth2-resource-server:5.2.0.RELEASE. Я хочу написать интеграционный тест для проверки безопасности в порядке.

У меня есть это WebSecurityConfigurerAdapter, определенное в моем приложении:

import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final OAuth2ResourceServerProperties properties;
    private final SecuritySettings securitySettings;

    public WebSecurityConfiguration(OAuth2ResourceServerProperties properties, SecuritySettings securitySettings) {
        this.properties = properties;
        this.securitySettings = securitySettings;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/api/**")
            .authenticated()
            .and()
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        NimbusJwtDecoder result = NimbusJwtDecoder.withJwkSetUri(properties.getJwt().getJwkSetUri())
                                                  .build();

        OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(
                JwtValidators.createDefault(),
                new AudienceValidator(securitySettings.getApplicationId()));

        result.setJwtValidator(validator);
        return result;
    }

    private static class AudienceValidator implements OAuth2TokenValidator<Jwt> {

        private final String applicationId;

        public AudienceValidator(String applicationId) {
            this.applicationId = applicationId;
        }

        @Override
        public OAuth2TokenValidatorResult validate(Jwt token) {
            if (token.getAudience().contains(applicationId)) {
                return OAuth2TokenValidatorResult.success();
            } else {
                return OAuth2TokenValidatorResult.failure(
                        new OAuth2Error("invalid_token", "The audience is not as expected, got " + token.getAudience(),
                                        null));
            }
        }
    }
}

У него есть собственный валидатор для проверки аудитории (aud) утверждение в токене.

У меня в настоящее время есть этот тест, который работает, но он вообще не проверяет утверждение аудитории:

@WebMvcTest(UserController.class)
@EnableConfigurationProperties({SecuritySettings.class, OAuth2ResourceServerProperties.class})
@ActiveProfiles("controller-test")
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void testOwnUserDetails() throws Exception {
        mockMvc.perform(get("/api/users/me")
                                .with(jwt(createJwtToken())))
               .andExpect(status().isOk())
               .andExpect(jsonPath("userId").value("AZURE-ID-OF-USER"))
               .andExpect(jsonPath("name").value("John Doe"));
    }

    @Test
    void testOwnUserDetailsWhenNotLoggedOn() throws Exception {
        mockMvc.perform(get("/api/users/me"))
               .andExpect(status().isUnauthorized());
    }

    @NotNull
    private Jwt createJwtToken() {
        String userId = "AZURE-ID-OF-USER";
        String userName = "John Doe";
        String applicationId = "AZURE-APP-ID";

        return Jwt.withTokenValue("fake-token")
                  .header("typ", "JWT")
                  .header("alg", "none")
                  .claim("iss",
                         "https://b2ctestorg.b2clogin.com/80880907-bc3a-469a-82d1-b88ffad655df/v2.0/")
                  .claim("idp", "LocalAccount")
                  .claim("oid", userId)
                  .claim("scope", "user_impersonation")
                  .claim("name", userName)
                  .claim("azp", applicationId)
                  .claim("ver", "1.0")
                  .subject(userId)
                  .audience(Set.of(applicationId))
                  .build();
    }
}

У меня также есть файл свойствдля профиля controller-test, который содержит идентификатор приложения и jwt-set-uri:

security-settings.application-id=FAKE_ID
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://b2ctestorg.b2clogin.com/b2ctestorg.onmicrosoft.com/discovery/v2.0/keys?p=b2c_1_ropc_flow

Может быть, JwtDecoder не используется, поскольку Jwt создается вручную? Как я могу убедиться, что JwtDecoder вызывается в тесте?

Ответы [ 3 ]

0 голосов
/ 08 ноября 2019

Используя постпроцессор JWT .with(jwt(createJwtToken()))), вы можете обойти JwtDecoder.

Рассмотрим, что произойдет, если JwtDecoder не будет обойдено.
В цепочке фильтров ваш запрос достигнет точки, где JwtDecoder анализирует значение JWT.
В этом случаезначение равно "fake-token", что приведет к исключению, поскольку оно не является допустимым JWT.
Это означает, что код даже не достигнет точки, где вызывается AudienceValidator.

Вы можете думать о значении, переданном в SecurityMockMvcRequestPostProcessors.jwt(Jwt jwt), как об ответе, который будет возвращен из JwtDecoder.decode(String token).
Затем тесты, использующие SecurityMockMvcRequestPostProcessors.jwt(Jwt jwt), проверят поведение, когда действительный токен JWT
Вы можете добавить дополнительные тесты для AudienceValidator, чтобы убедиться, что он работает правильно.

0 голосов
/ 08 ноября 2019

Чтобы уточнить ответ от Eleftheria Stein-Kousathana, я изменил это, чтобы сделать возможным:

1) Создайте класс JwtDecoderFactoryBean, чтобы иметь возможность провести модульное тестирование JwtDecoder инастроенные валидаторы:

@Component
public class JwtDecoderFactoryBean implements FactoryBean<JwtDecoder> {

    private final OAuth2ResourceServerProperties properties;
    private final SecuritySettings securitySettings;
    private final Clock clock;

    public JwtDecoderFactoryBean(OAuth2ResourceServerProperties properties,
                                 SecuritySettings securitySettings,
                                 Clock clock) {
        this.properties = properties;
        this.securitySettings = securitySettings;
        this.clock = clock;
    }


    @Override
    public JwtDecoder getObject() {
        JwtTimestampValidator timestampValidator = new JwtTimestampValidator();
        timestampValidator.setClock(clock);
        JwtIssuerValidator issuerValidator = new JwtIssuerValidator(securitySettings.getJwtIssuer());
        JwtAudienceValidator audienceValidator = new JwtAudienceValidator(securitySettings.getJwtApplicationId());
        OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(
                timestampValidator,
                issuerValidator,
                audienceValidator);

        NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(properties.getJwt().getJwkSetUri())
                                                   .build();

        decoder.setJwtValidator(validator);
        return decoder;
    }

    @Override
    public Class<?> getObjectType() {
        return JwtDecoder.class;
    }
}

Я также извлек AudienceValidator из исходного кода во внешний класс и переименовал его в JwtAudienceValidator.

2) Удалите JwtDecoder @Bean метод из конфигурации безопасности, поэтому он выглядит следующим образом:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/api/**")
            .authenticated()
            .and()
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
    }
}

3) Создайте компонент Clock в некотором @Configuration классе:

    @Bean
    public Clock clock() {
        return Clock.systemDefaultZone();
    }

(Это необходимо длямодульное тестирование истечения времени действия токенов)

С помощью этой настройки теперь можно написать модульный тест для настройки JwtDecoder, которая является фактической настройкой, используемой приложением:


   // actual @Test methods ommitted, but they can use this private method
   // to setup a JwtDecoder and test some valid/invalid JWT tokens.

@NotNull
    private JwtDecoder createDecoder(String currentTime, String issuer, String audience) {
        OAuth2ResourceServerProperties properties = new OAuth2ResourceServerProperties();
        properties.getJwt().setJwkSetUri(
                "https://mycompb2ctestorg.b2clogin.com/mycompb2ctestorg.onmicrosoft.com/discovery/v2.0/keys?p=b2c_1_ropc_flow");

        JwtDecoderFactoryBean factoryBean = new JwtDecoderFactoryBean(properties,
                                                                      new SecuritySettings(audience, issuer),
                                                                      Clock.fixed(Instant.parse(currentTime),
                                                                                  ZoneId.systemDefault()));
        //noinspection ConstantConditions - getObject never returns null in this case
        return factoryBean.getObject();
    }

Наконец, @WebMvcTest должен иметь макет JwtDecoder, поскольку реальный не запускается больше с тестовым срезом @WebMvcTest (из-за использования фабричного компонента). Это хороший IMO, так как в противном случае мне нужно было определить свойства для реального JwtDecoder, который так или иначе не использовался. Как следствие, мне больше не нужен профиль controller-test в тесте.

Поэтому просто объявите поле наподобие этого:

@MockBean
private JwtDecoder jwtDecoder;

или создайте вложенный класс конфигурации теста:

 @TestConfiguration
    static class TestConfig {
        @Bean
        public JwtDecoder jwtDecoder() {
            return mock(JwtDecoder.class);
        }
    }
0 голосов
/ 08 ноября 2019

Я предполагаю, что либо mockMvc не был настроен на учет аспекта безопасности (1), либо тестовый фрагмент @WebMvcTest не выполняет автоматическую настройку всех необходимых bean-компонентов (2).

1: не могли бы вы добавить добавление @AutoConfigureMockMvc в класс или настроить mockMvc вручную, используя


@Autowired
private WebApplicationContext context; 

private MockMvc mockMvc;

@Before
public void setup() {
mockMvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity())
                .build();
}

2: если оно связано с тестовым слайсом @WebMvcTest,рассмотрите возможность добавления @Import(WebSecurityConfig.class) в тестовый класс. В противном случае, используйте @SpringBootTest вместе с @AutoConfigureMockMvc в классе тестирования вместо @WebMvcTest для настройки Spring Boot Test.

...