diff --git a/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java b/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java index 5eb5aeff35..dee7b18b78 100644 --- a/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java +++ b/apiml-security-common/src/testFixtures/java/org/zowe/apiml/security/common/util/JWTTestUtils.java @@ -22,6 +22,7 @@ import java.security.KeyStore; import java.security.PrivateKey; import java.security.interfaces.RSAPublicKey; +import java.time.Duration; import java.time.Instant; import java.util.Date; import java.util.List; @@ -34,20 +35,26 @@ public static String createZoweJwtToken(String username, String domain, String l return createToken(username, domain, ltpaToken, config, "APIML"); } + public static String createExpiredZoweJwtToken(String username, String domain, String ltpaToken, HttpsConfig config) { + return createToken(username, domain, ltpaToken, System.currentTimeMillis() - Duration.ofDays(1).toMillis(), config, "APIML"); + } + public static String createZosmfJwtToken(String username, String domain, String ltpaToken, HttpsConfig config) { return createToken(username, domain, ltpaToken, config, "zOSMF"); } public static String createToken(String username, String domain, String ltpaToken, HttpsConfig config, String issuer) { - long now = System.currentTimeMillis(); - long expiration = now + 100_000L; + return createToken(username, domain, ltpaToken, System.currentTimeMillis() + 100_000L, config, issuer); + } + + public static String createToken(String username, String domain, String ltpaToken, long expiration, HttpsConfig config, String issuer) { Key jwtSecret = SecurityUtils.loadKey(config); return Jwts.builder() .subject(username) .claim("dom", domain) .claim("ltpa", ltpaToken) - .issuedAt(new Date(now)) + .issuedAt(new Date(System.currentTimeMillis())) .expiration(new Date(expiration)) .issuer(issuer) .id(UUID.randomUUID().toString()) diff --git a/apiml/build.gradle b/apiml/build.gradle index df18bbf8d7..3e1e30df0e 100644 --- a/apiml/build.gradle +++ b/apiml/build.gradle @@ -79,6 +79,7 @@ dependencies { testImplementation(testFixtures(project(":apiml-common"))) testImplementation(testFixtures(project(":apiml-security-common"))) testImplementation(testFixtures(project(":gateway-service"))) + testImplementation(testFixtures(project(":apiml-security-common"))) testImplementation libs.spring.boot.starter.test testImplementation libs.spring.mock.mvc testImplementation libs.modulith.test diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/ValidationJwtCacheRoutingTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/ValidationJwtCacheRoutingTest.java new file mode 100644 index 0000000000..527f7dac68 --- /dev/null +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/ValidationJwtCacheRoutingTest.java @@ -0,0 +1,122 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.acceptance; + +import lombok.SneakyThrows; +import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.zowe.apiml.auth.AuthenticationScheme; +import org.zowe.apiml.gateway.MockService; +import org.zowe.apiml.security.common.token.TokenAuthentication; +import org.zowe.apiml.security.common.util.JWTTestUtils; +import org.zowe.apiml.util.config.SslContext; +import org.zowe.apiml.util.config.SslContextConfigurer; + +import java.util.stream.Stream; + +import static io.restassured.RestAssured.given; +import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.junit.jupiter.api.Assertions.*; + +@AcceptanceTest +class ValidationJwtCacheRoutingTest extends AcceptanceTestWithMockServices { + + private static final String COOKIE = "apimlAuthenticationToken"; + + @Autowired + CacheManager cacheManager; + + @LocalServerPort + private int port; + + @Value("${apiml.service.hostname:localhost}") + private String hostname; + + private String expiredJwtToken; + + private Cache validationJwtTokenCache; + + @Value("${server.ssl.keyPassword}") + char[] password; + @Value("${server.ssl.keyStore}") + String client_cert_keystore; + @Value("${server.ssl.keyStore}") + String keystore; + + @BeforeAll + void initMockServices() { + getSchemes().forEach(scheme -> + mockService("%s-service".formatted(scheme.toLowerCase())).scope(MockService.Scope.CLASS) + .authenticationScheme(AuthenticationScheme.fromString(scheme)) + .applid("dummy") + .addEndpoint("/%s-service/foo".formatted(scheme.toLowerCase())) + .assertion(exchange -> assertFalse(exchange.getRequestHeaders().containsKey("Authorization"))) + .assertion(exchange -> assertFalse(exchange.getRequestHeaders().containsKey("X-SAF-Token"))) + .assertion(exchange -> assertTrue(exchange.getRequestHeaders().containsKey("X-zowe-auth-failure"))) + .responseCode(200) + .and() + .start() + ); + } + + @BeforeEach + @SneakyThrows + void setUp() { + mockZosmfSuccess(); + SslContextConfigurer configurer = new SslContextConfigurer(password, client_cert_keystore, keystore); + SslContext.prepareSslAuthentication(configurer); + expiredJwtToken = JWTTestUtils.createExpiredZoweJwtToken("user", "z/OS", "Ltpa", httpConfig.getHttpsConfig()); + validationJwtTokenCache = cacheManager.getCache("validatedJwtTokens"); + validationJwtTokenCache.put(expiredJwtToken, new TokenAuthentication(expiredJwtToken)); + } + + @AfterAll + void stop() { + SslContext.reset(); + } + + Stream getSchemes() { + return Stream.of("httpBasicPassTicket", "zosmf", "zoweJwt", "safIdt"); + } + + @ParameterizedTest + @MethodSource("getSchemes") + void whenExpiredTokenInValidationCacheRouting_thenAuthorizationFails(String scheme) { + //@formatter:off + var response = given() + .cookie(COOKIE, expiredJwtToken) + .when() + .get("https://%s:%d/%s-service/api/v1/foo".formatted(hostname, port, scheme.toLowerCase())); + //@formatter:on + + assertEquals(SC_OK, response.getStatusCode()); + assertNotNull(response.getHeader("X-zowe-auth-failure")); + } + + @Test + void whenExpiredTokenInValidationCacheQuery_thenAuthorizationFails() { + //@formatter:off + given() + .cookie(COOKIE, expiredJwtToken) + .when() + .get("https://%s:%d/gateway/api/v1/auth/query".formatted(hostname, port)) + .then() + .statusCode(SC_UNAUTHORIZED); + //@formatter:on + } +} diff --git a/zaas-service/src/test/java/org/zowe/apiml/acceptance/JwtValidationCacheTest.java b/zaas-service/src/test/java/org/zowe/apiml/acceptance/JwtValidationCacheTest.java new file mode 100644 index 0000000000..7884da3de7 --- /dev/null +++ b/zaas-service/src/test/java/org/zowe/apiml/acceptance/JwtValidationCacheTest.java @@ -0,0 +1,113 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.acceptance; + +import io.restassured.config.SSLConfig; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.zowe.apiml.product.web.HttpConfig; +import org.zowe.apiml.security.common.token.TokenAuthentication; +import org.zowe.apiml.security.common.util.JWTTestUtils; +import org.zowe.apiml.util.config.SslContext; +import org.zowe.apiml.util.config.SslContextConfigurer; +import org.zowe.apiml.zaas.ZaasApplication; + +import static io.restassured.RestAssured.config; +import static io.restassured.RestAssured.given; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(classes = ZaasApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class JwtValidationCacheTest { + + private static final String COOKIE = "apimlAuthenticationToken"; + + @Autowired + private HttpConfig httpConfig; + + @Autowired + private CacheManager cacheManager; + + @LocalServerPort + private int port; + + @Value("${apiml.service.hostname:localhost}") + private String hostname; + + private String expiredJwtToken; + + private Cache validationJwtTokenCache; + + @Value("${server.ssl.keyPassword}") + private char[] password; + + @Value("${server.ssl.keyStore}") + private String clientCertKeystore; + + @Value("${server.ssl.keyStore}") + private String keystore; + + @BeforeEach + void setUp() throws Exception { + SslContextConfigurer configurer = new SslContextConfigurer(password, clientCertKeystore, keystore); + SslContext.prepareSslAuthentication(configurer); + expiredJwtToken = JWTTestUtils.createExpiredZoweJwtToken("user", "z/OS", "Ltpa", httpConfig.getHttpsConfig()); + validationJwtTokenCache = cacheManager.getCache("validatedJwtTokens"); + validationJwtTokenCache.put(expiredJwtToken, new TokenAuthentication(expiredJwtToken)); + } + + @Nested + class ExpiredJwtTokenInValidationCache { + @ParameterizedTest + @ValueSource(strings = {"ticket", "zosmf","zoweJwt","safIdt"}) + void whenZoweJwtSchemeCalled_thenUnauthorized(String scheme) { + //@formatter:off + given().config(config().sslConfig(new SSLConfig().sslSocketFactory( + new SSLSocketFactory(httpConfig.getSecureSslContextWithoutKeystore(), ALLOW_ALL_HOSTNAME_VERIFIER))) + ) + .cookie(COOKIE, expiredJwtToken) + .when() + .post(String.format("https://%s:%d/zaas/scheme/%s", hostname, port, scheme)) + .then() + .statusCode(SC_UNAUTHORIZED); + //@formatter:on + } + } + + @Test + void whenQueryCalledWithExpiredJwtToken_thenUnauthorized() { + //@formatter:off + var response = given().config(config().sslConfig(new SSLConfig().sslSocketFactory( + new SSLSocketFactory(httpConfig.getSecureSslContextWithoutKeystore(), ALLOW_ALL_HOSTNAME_VERIFIER))) + ) + .cookie(COOKIE, expiredJwtToken) + .when() + .get(String.format("https://%s:%d/zaas/api/v1/auth/query", hostname, port)) + .then() + .statusCode(SC_UNAUTHORIZED) + .extract().body().asString(); + //@formatter:on + + assertTrue(response.contains("The validity of the token is expired.")); + } + +}