From 5c38127b133aed49db0f5c021a8e4f6574b023d1 Mon Sep 17 00:00:00 2001 From: Richard Salac Date: Fri, 21 Feb 2025 09:50:11 +0100 Subject: [PATCH 1/6] fix: authentication header for routed requests Signed-off-by: Richard Salac --- .../gateway/filters/AbstractTokenFilterFactory.java | 3 +++ .../apiml/gateway/acceptance/TokenSchemeTest.java | 13 ++++++------- .../filters/AbstractTokenFilterFactoryTest.java | 2 ++ .../authentication/schemes/ZoweJwtSchemeTest.java | 4 ++++ 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactory.java index 6121c7ee7b..f14588202b 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactory.java @@ -30,6 +30,8 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; +import static org.zowe.apiml.constants.ApimlConstants.BEARER_AUTHENTICATION_PREFIX; + public abstract class AbstractTokenFilterFactory extends AbstractAuthSchemeFactory { protected AbstractTokenFilterFactory(Class configClazz, WebClient webClient, InstanceInfoService instanceInfoService, MessageService messageService) { @@ -90,6 +92,7 @@ protected Mono processResponse(ServerWebExchange exchange, GatewayFilterCh response.get().getToken() ); headers.set(HttpHeaders.COOKIE, cookieHeader); + headers.set(HttpHeaders.AUTHORIZATION, BEARER_AUTHENTICATION_PREFIX + " " + response.get().getToken()); }).build(); exchange = exchange.mutate().request(request).build(); } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/TokenSchemeTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/TokenSchemeTest.java index 5ba2e460b3..1caa8470c0 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/TokenSchemeTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/TokenSchemeTest.java @@ -12,10 +12,7 @@ import com.sun.net.httpserver.HttpExchange; import org.apache.commons.lang3.StringUtils; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.*; import org.springframework.http.HttpHeaders; import org.zowe.apiml.auth.AuthenticationScheme; import org.zowe.apiml.gateway.acceptance.common.AcceptanceTest; @@ -262,7 +259,7 @@ class ZaasCommunication extends AcceptanceTestWithMockServices { private MockService service; @BeforeAll - void initService() throws IOException { + void initService() { service = createService(); } @@ -303,8 +300,9 @@ MockService createService() { .authenticationScheme(getAuthenticationScheme()) .addEndpoint("/service/test/success") .assertion(he -> assertEquals(JWT, getCookie(he, COOKIE_NAME))) + .assertion(he -> assertEquals(1, he.getRequestHeaders().get(HttpHeaders.AUTHORIZATION).size())) + .assertion(he -> assertEquals("Bearer " + JWT, he.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION))) .assertion(he -> assertNull(he.getRequestHeaders().getFirst("x-service-id"))) .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-SAF-Token"))) .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-Public"))) @@ -323,7 +321,8 @@ MockService createService() { .addEndpoint("/service/test/fail") .assertion(he -> assertNull(getCookie(he, COOKIE_NAME))) - .assertion(he -> assertNotNull(he.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION))) + .assertion(he -> assertEquals(1, he.getRequestHeaders().get(HttpHeaders.AUTHORIZATION).size())) + .assertion(he -> assertNotEquals("Bearer " + JWT, he.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION))) .assertion(he -> assertNull(he.getRequestHeaders().getFirst("x-service-id"))) .assertion(he -> assertNotNull(he.getRequestHeaders().getFirst("X-SAF-Token"))) .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-Public"))) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java index 9de932972e..9b49c33c81 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/AbstractTokenFilterFactoryTest.java @@ -17,6 +17,7 @@ import org.mockito.ArgumentCaptor; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; @@ -79,6 +80,7 @@ void givenCookieResponse_whenHandling_thenUpdateTheRequest() { .build() )); assertEquals("cookieName=cookieValue", request.getHeaders().getFirst("cookie")); + assertEquals("Bearer cookieValue" , request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION)); } } diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java index e700b3c05a..13114f1edf 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java @@ -56,6 +56,7 @@ void givenCorrectClientCertificateInRequest() { .get(URL) .then() .body("headers.cookie", startsWith("apimlAuthenticationToken")) + .body("headers.authorization", startsWith("Bearer")) .statusCode(200); } @@ -100,6 +101,7 @@ void forwardJWTToService() { .get(URL) .then() .body("headers.cookie", is("apimlAuthenticationToken=" + jwt)) + .body("headers.authorization", is("Bearer " + jwt)) .statusCode(200); } @@ -117,6 +119,7 @@ void preserveCookies() { .then() .body("cookies.apimlAuthenticationToken", is(jwt)) .body("cookies.XSRF-TOKEN", is("another-token-in-cookies")) + .body("headers.authorization", is("Bearer " + jwt)) .statusCode(200); } @@ -151,6 +154,7 @@ void translateIntoJWTAndSendToService() { .get(URL) .then() .body("headers.cookie", startsWith("apimlAuthenticationToken=")) + .body("headers.authorization", startsWith("Bearer ")) .statusCode(200); } } From bd701610bc099c7a5ab06194c41f342a1bbb82bc Mon Sep 17 00:00:00 2001 From: Richard Salac Date: Fri, 21 Feb 2025 14:38:30 +0100 Subject: [PATCH 2/6] fix zosmf mock to support Bearer auth header Signed-off-by: Richard Salac --- .../client/services/apars/FunctionalApar.java | 14 ++++++++++++++ .../zowe/apiml/client/services/apars/PHBase.java | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/mock-services/src/main/java/org/zowe/apiml/client/services/apars/FunctionalApar.java b/mock-services/src/main/java/org/zowe/apiml/client/services/apars/FunctionalApar.java index dbe995efe3..87592292e5 100644 --- a/mock-services/src/main/java/org/zowe/apiml/client/services/apars/FunctionalApar.java +++ b/mock-services/src/main/java/org/zowe/apiml/client/services/apars/FunctionalApar.java @@ -13,6 +13,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; import org.zowe.apiml.client.model.LoginBody; import org.zowe.apiml.client.services.JwtTokenService; @@ -218,6 +219,19 @@ protected boolean isValidJwtCookie(Map headers) { } + protected boolean isValidAuthHeader(String authHeader) { + if (!StringUtils.hasText(authHeader)) { + return false; + } + + if (authHeader.startsWith("Bearer")) { + var jwtToken = authHeader.length() > 8 ? authHeader.substring(7) : ""; + return jwtTokenService.validateJwtToken(jwtToken); + } + + return true; + } + private String getAuthCookie(Map headers) { return headers.get(COOKIE_HEADER) != null ? headers.get(COOKIE_HEADER) : headers.get(HttpHeaders.COOKIE); } diff --git a/mock-services/src/main/java/org/zowe/apiml/client/services/apars/PHBase.java b/mock-services/src/main/java/org/zowe/apiml/client/services/apars/PHBase.java index 38dcad6ee9..592ba7b5a8 100644 --- a/mock-services/src/main/java/org/zowe/apiml/client/services/apars/PHBase.java +++ b/mock-services/src/main/java/org/zowe/apiml/client/services/apars/PHBase.java @@ -70,7 +70,7 @@ protected ResponseEntity handleFiles(Map headers) { String authorization = headers.get(AUTHORIZATION_HEADER); if (authorization != null) { - if (authorization.startsWith("Bearer")) { + if (!isValidAuthHeader(authorization)) { return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); } } else { From f831f7a860c74868e6f45bc44303b25f463ed9cc Mon Sep 17 00:00:00 2001 From: Richard Salac Date: Fri, 21 Feb 2025 15:28:10 +0100 Subject: [PATCH 3/6] fix zosmf mock service support for ltpa tokens in auth header Signed-off-by: Richard Salac --- .../integration/authentication/pat/PATWithAllSchemesTest.java | 2 +- .../integration/authentication/schemes/GatewayAuthTest.java | 4 ++-- .../java/org/zowe/apiml/client/services/apars/PHBase.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/pat/PATWithAllSchemesTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/pat/PATWithAllSchemesTest.java index 2840f84a0e..036ce77203 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/pat/PATWithAllSchemesTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/pat/PATWithAllSchemesTest.java @@ -62,7 +62,7 @@ static Stream schemas() { var schemasTest = new ArrayList(); schemasTest.add(Arguments.of("zowejwt", HttpRequestUtils.getUriFromGateway(ZOWE_JWT_REQUEST), (Consumer) r -> { assertEquals(HttpStatus.SC_OK, r.getStatusCode()); - assertNull(r.getBody().path("headers.authorization")); + assertNotNull(r.getBody().path("headers.authorization")); assertThat(r.getBody().path("headers.cookie"), containsString(COOKIE_NAME)); String jwt = r.getBody().path("headers.cookie").toString(); try { diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/GatewayAuthTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/GatewayAuthTest.java index ad159eafbd..b87fad4256 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/GatewayAuthTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/GatewayAuthTest.java @@ -49,12 +49,12 @@ static Stream validToBeTransformed() { List arguments = new ArrayList<>(Arrays.asList( Arguments.of("Zowe auth scheme", ZOWE_JWT_REQUEST, (Consumer) response -> { assertNotNull(response.jsonPath().getString("cookies.apimlAuthenticationToken"), "Expected not null apimlAuthenticationToken. Response was: " + response.asPrettyString()); - assertNull(response.jsonPath().getString("headers.authorization"), "Expected null Authorization header. Response was: " + response.asPrettyString()); + assertNotNull(response.jsonPath().getString("headers.authorization"), "Expected not null Authorization header. Response was: " + response.asPrettyString()); assertTrue(CollectionUtils.isEmpty(response.jsonPath().getList("certs")), "Expected empty certs list. Response was: " + response.asPrettyString()); }), Arguments.of("z/OSMF auth scheme", ZOSMF_REQUEST, (Consumer) response -> { assertNotNull(response.jsonPath().getString("cookies.jwtToken"), "Expected not null jwtToken cookie. Response was: " + response.asPrettyString()); - assertNull(response.jsonPath().getString("headers.authorization"), "Expected null Authorization header. Response was: " + response.asPrettyString()); + assertNotNull(response.jsonPath().getString("headers.authorization"), "Expected not null Authorization header. Response was: " + response.asPrettyString()); assertTrue(CollectionUtils.isEmpty(response.jsonPath().getList("certs")), "Expected empty certs list. Response was: " + response.asPrettyString()); }), Arguments.of("PassTicket auth scheme", REQUEST_INFO_ENDPOINT, (Consumer) response -> { diff --git a/mock-services/src/main/java/org/zowe/apiml/client/services/apars/PHBase.java b/mock-services/src/main/java/org/zowe/apiml/client/services/apars/PHBase.java index 592ba7b5a8..45f868f19e 100644 --- a/mock-services/src/main/java/org/zowe/apiml/client/services/apars/PHBase.java +++ b/mock-services/src/main/java/org/zowe/apiml/client/services/apars/PHBase.java @@ -70,7 +70,7 @@ protected ResponseEntity handleFiles(Map headers) { String authorization = headers.get(AUTHORIZATION_HEADER); if (authorization != null) { - if (!isValidAuthHeader(authorization)) { + if (!isValidAuthHeader(authorization) && !ltpaIsPresent(headers)) { return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); } } else { From bdc41dff53ae706e6593ebfb5774fef540f9bfb1 Mon Sep 17 00:00:00 2001 From: Richard Salac Date: Mon, 24 Feb 2025 14:15:54 +0100 Subject: [PATCH 4/6] wip: try it with mock service returning false by default for auth headers Signed-off-by: Richard Salac --- .../org/zowe/apiml/client/services/apars/FunctionalApar.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mock-services/src/main/java/org/zowe/apiml/client/services/apars/FunctionalApar.java b/mock-services/src/main/java/org/zowe/apiml/client/services/apars/FunctionalApar.java index 87592292e5..9c7e6d24d6 100644 --- a/mock-services/src/main/java/org/zowe/apiml/client/services/apars/FunctionalApar.java +++ b/mock-services/src/main/java/org/zowe/apiml/client/services/apars/FunctionalApar.java @@ -229,7 +229,7 @@ protected boolean isValidAuthHeader(String authHeader) { return jwtTokenService.validateJwtToken(jwtToken); } - return true; + return false; } private String getAuthCookie(Map headers) { From 4072bc1542b778512d2a16e67621cf149b8eed59 Mon Sep 17 00:00:00 2001 From: Richard Salac Date: Mon, 24 Feb 2025 16:59:15 +0100 Subject: [PATCH 5/6] revert wip: try it with mock service returning false by default for auth headers Signed-off-by: Richard Salac --- .../org/zowe/apiml/client/services/apars/FunctionalApar.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mock-services/src/main/java/org/zowe/apiml/client/services/apars/FunctionalApar.java b/mock-services/src/main/java/org/zowe/apiml/client/services/apars/FunctionalApar.java index 9c7e6d24d6..87592292e5 100644 --- a/mock-services/src/main/java/org/zowe/apiml/client/services/apars/FunctionalApar.java +++ b/mock-services/src/main/java/org/zowe/apiml/client/services/apars/FunctionalApar.java @@ -229,7 +229,7 @@ protected boolean isValidAuthHeader(String authHeader) { return jwtTokenService.validateJwtToken(jwtToken); } - return false; + return true; } private String getAuthCookie(Map headers) { From 4f65e6b73fe783a724c4f995fb384915a0068dd9 Mon Sep 17 00:00:00 2001 From: Richard Salac Date: Mon, 24 Feb 2025 17:00:04 +0100 Subject: [PATCH 6/6] add Basic auth test Signed-off-by: Richard Salac --- .../schemes/ZoweJwtSchemeTest.java | 18 ++++++++++++++++++ ...erviceProtectedEndpointIntegrationTest.java | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java index 13114f1edf..ea76eca353 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/ZoweJwtSchemeTest.java @@ -21,6 +21,7 @@ import org.zowe.apiml.util.categories.DiscoverableClientDependentTest; import org.zowe.apiml.util.categories.InfinispanStorageTest; import org.zowe.apiml.util.categories.zOSMFAuthTest; +import org.zowe.apiml.util.config.ConfigReader; import org.zowe.apiml.util.config.ItSslConfigFactory; import org.zowe.apiml.util.config.SslContext; import org.zowe.apiml.util.http.HttpRequestUtils; @@ -41,6 +42,9 @@ class ZoweJwtSchemeTest implements TestWithStartedInstances { private static URI URL; + private static final String PASSWORD = ConfigReader.environmentConfiguration().getCredentials().getPassword(); + private static final String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); + @BeforeAll static void init() throws Exception { @@ -72,6 +76,20 @@ void givenInvalidClientCertificateInRequest() { .statusCode(200); } + @Test + void givenBasicAuthHeader_authenticationIsPassedUnchanged() { + given() + .config(SslContext.tlsWithoutCert) + .auth().preemptive().basic(USERNAME, PASSWORD) + .when() + .get(URL) + .then() + .log().all() + .body("headers.cookie", nullValue()) + .body("headers.authorization", startsWith("Basic")) + .statusCode(200); + } + @Nested class GivenCustomAuthHeader { @Test diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/services/ServiceProtectedEndpointIntegrationTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/services/ServiceProtectedEndpointIntegrationTest.java index 1efaa7cf8b..e9435eb410 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/services/ServiceProtectedEndpointIntegrationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/services/ServiceProtectedEndpointIntegrationTest.java @@ -112,7 +112,7 @@ void viaBasicAuthHeader() { URI uri = HttpRequestUtils.getUriFromGateway(ZOSMF_ENDPOINT, ARGUMENT); given() - .auth().preemptive().basic(USERNAME, new String(PASSWORD)) + .auth().preemptive().basic(USERNAME, PASSWORD) .header("X-CSRF-ZOSMF-HEADER", "zosmf") .when() .get(uri)