From c9dd4aa8eb396de41281b681e49db1f0f38bff69 Mon Sep 17 00:00:00 2001 From: achmelo <37397715+achmelo@users.noreply.github.com> Date: Wed, 24 Apr 2024 12:53:15 +0200 Subject: [PATCH] chore: v2 cherry pick (#3516) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: allow key exchange port configuration (#3453) * allow key exchange port configuration Signed-off-by: achmelo * explain different defaults for the port Signed-off-by: achmelo * use the same default port number Signed-off-by: achmelo --------- Signed-off-by: achmelo (cherry picked from commit d82322ee37540695d20516b44d46ad9188adf5b0) * chore: Change log levels for WS and Identity Mapper and add service info (#3344) * add info about the southbound service request for authsource Signed-off-by: at670475 * add debug msg for websocket routing Signed-off-by: at670475 * address pr comments Signed-off-by: at670475 --------- Signed-off-by: at670475 (cherry picked from commit 0a888f8f663e01781068e63702139056372ee550) * fix: Respect configuration enabling JWT Token Refresh Functionality #3468 (#3474) * Respect JWT Refresh Configuration from zowe.yaml Signed-off-by: Jakub Balhar * Fix the default in shell. Signed-off-by: Jakub Balhar --------- Signed-off-by: Jakub Balhar (cherry picked from commit b4146bec755ed8ce953623874617384cf2d480b5) * feat: include OIDC JWKSet in the gateway JWKs (#3499) * use the same JWK format, include OIDC keys in the response Signed-off-by: achmelo * cleanup, update tests Signed-off-by: achmelo * integration test for local validation Signed-off-by: achmelo * set default ssl factory Signed-off-by: achmelo * change debug message Signed-off-by: achmelo * test coverage Signed-off-by: achmelo --------- Signed-off-by: achmelo (cherry picked from commit a588a8f7a36e69cfc4d1edc356e3f1fb2d4a5abf) Signed-off-by: achmelo * feat: forward valid OIDC token to southbound service in case of distributed ID is not mapped (#3497) * forward token and message in case of missing mapping Signed-off-by: at670475 * fix test Signed-off-by: at670475 * add unit test Signed-off-by: at670475 * small refactoring Signed-off-by: at670475 * updating integration tests Signed-off-by: at670475 * add test Signed-off-by: at670475 * add exception to the error handler to return correct response code Signed-off-by: at670475 * fix styles Signed-off-by: achmelo --------- Signed-off-by: at670475 Signed-off-by: achmelo Co-authored-by: achmelo <37397715+achmelo@users.noreply.github.com> Co-authored-by: achmelo (cherry picked from commit 60777c1592024ad3fb5eea5b9a13484541121fe9) * fix: check for nullpointer exception when jwk key can't be retrieved (#3503) * check for nullpointer ex when jwk key can't be retrieved Signed-off-by: at670475 * add test Signed-off-by: at670475 * address comment Signed-off-by: at670475 --------- Signed-off-by: at670475 (cherry picked from commit 7c00dba79c40ce5f3aaec994b6c4fe2d15817c53) * revert Signed-off-by: achmelo * use current methods Signed-off-by: achmelo * feat: Move OIDC access token from cookie to special header (#3513) * POC Signed-off-by: Pavel Jares * fix Signed-off-by: Pavel Jares * replace old constructors Signed-off-by: achmelo * update IT Signed-off-by: achmelo * fix Signed-off-by: Pavel Jares * update IT Signed-off-by: achmelo * fix IT Signed-off-by: Pavel Jares * exception handler for no MF ID, unit test Signed-off-by: achmelo * unit tests for request modification Signed-off-by: Pavel Jares * license Signed-off-by: achmelo * minor changes Signed-off-by: Pavel Jares * lowercase header Signed-off-by: achmelo * remove import Signed-off-by: achmelo * remove authorization header from httpservletrequest Signed-off-by: achmelo * test no ID and invalid token Signed-off-by: achmelo * ignore cookies if auth cookie only remains Signed-off-by: achmelo * expect no cookie in request Signed-off-by: achmelo * fix sonar Signed-off-by: Pavel Jares --------- Signed-off-by: Pavel Jares Signed-off-by: achmelo Co-authored-by: achmelo (cherry picked from commit 6248308569b59387c421113da3d4ccbc804822a9) * url without default Signed-off-by: achmelo * use the same jwk uri Signed-off-by: achmelo * attempt to fix IT Signed-off-by: Pavel Jares * Revert "attempt to fix IT" This reverts commit cf35400537f7b6c2c492748a38b9accff903dd1a. * use keyLocator for JWK set Signed-off-by: achmelo --------- Signed-off-by: achmelo Signed-off-by: Pavel Jares Co-authored-by: Andrea Tabone <39694626+taban03@users.noreply.github.com> Co-authored-by: Jakub Balhar Co-authored-by: Pavel Jareš <58428711+pj892031@users.noreply.github.com> Co-authored-by: Pavel Jares --- .github/workflows/integration-tests.yml | 4 +- .../common/error/AuthExceptionHandler.java | 8 + .../security/common/error/ErrorType.java | 3 +- .../token/NoMainframeIdentityException.java | 35 +++ .../error/AuthExceptionHandlerTest.java | 19 +- .../NoMainframeIdentityExceptionTest.java | 29 +++ .../src/main/resources/bin/start.sh | 1 + .../infinispan/config/InfinispanConfig.java | 3 + .../src/main/resources/infinispan.xml | 2 +- .../filters/TokenFilterFactory.java | 19 +- .../acceptance/TokenSchemeTest.java | 100 ++++---- .../filters/TokenFilterFactoryTest.java | 99 ++++++++ .../zowe/apiml/constants/ApimlConstants.java | 1 + .../zowe/apiml/zaas/ZaasTokenResponse.java | 3 + .../src/main/resources/bin/start.sh | 2 + .../gateway/controllers/AuthController.java | 34 ++- .../pre/ServiceAuthenticationFilter.java | 9 + .../security/mapping/ExternalMapper.java | 22 +- .../security/service/schema/OidcCommand.java | 56 +++++ .../source/DefaultAuthSourceService.java | 5 +- .../schema/source/OIDCAuthSourceService.java | 4 +- .../service/token/OIDCTokenProvider.java | 107 ++++---- .../security/service/zosmf/ZosmfService.java | 32 ++- .../ws/WebSocketProxyClientHandler.java | 2 +- .../ws/WebSocketProxyServerHandler.java | 1 + .../apiml/gateway/zaas/ZaasController.java | 32 ++- .../main/resources/gateway-log-messages.yml | 21 ++ .../controllers/AuthControllerTest.java | 69 +++-- .../pre/ServiceAuthenticationFilterTest.java | 51 +++- .../service/schema/OidcCommandTest.java | 61 +++++ .../source/OIDCAuthSourceServiceTest.java | 7 +- .../service/token/OIDCTokenProviderTest.java | 235 ++++++++++-------- .../service/zosmf/ZosmfServiceTest.java | 75 ++++-- .../gateway/zaas/ZaasControllerTest.java | 42 +++- integration-tests/build.gradle | 1 + .../authentication/oauth2/OktaOauth2Test.java | 71 +++++- .../org/zowe/apiml/util/SecurityUtils.java | 2 +- .../zowe/apiml/util/requests/Endpoints.java | 1 + keystore/localhost/localhost.truststore.p12 | Bin 10850 -> 12314 bytes .../apiml/client/services/apars/JwtKeys.java | 2 +- .../client/handler/RestResponseHandler.java | 3 + .../handler/RestResponseHandlerTest.java | 7 + 42 files changed, 949 insertions(+), 331 deletions(-) create mode 100644 apiml-security-common/src/main/java/org/zowe/apiml/security/common/token/NoMainframeIdentityException.java create mode 100644 apiml-security-common/src/test/java/org/zowe/apiml/security/common/token/NoMainframeIdentityExceptionTest.java create mode 100644 cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/filters/TokenFilterFactoryTest.java create mode 100644 gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/OidcCommand.java create mode 100644 gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/OidcCommandTest.java diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 8afcb46a77..2bc7cd7791 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -104,7 +104,7 @@ jobs: APIML_SECURITY_OIDC_CLIENTSECRET: ${{ secrets.OKTA_CLIENT_PASSWORD }} APIML_SECURITY_OIDC_ENABLED: true APIML_SECURITY_OIDC_REGISTRY: zowe.okta.com - APIML_SECURITY_OIDC_JWKS_URI: ${{ secrets.OKTA_JWK_URI }} + APIML_SECURITY_OIDC_JWKS_URI: ${{ secrets.OKTA_JWKSET_URI }} APIML_SECURITY_OIDC_IDENTITYMAPPERUSER: APIMTST APIML_SECURITY_OIDC_IDENTITYMAPPERURL: https://gateway-service:10010/zss/api/v1/certificate/dn discovery-service: @@ -391,7 +391,7 @@ jobs: APIML_SECURITY_OIDC_CLIENTSECRET: ${{ secrets.OKTA_CLIENT_PASSWORD }} APIML_SECURITY_OIDC_ENABLED: true APIML_SECURITY_OIDC_REGISTRY: zowe.okta.com - APIML_SECURITY_OIDC_JWKS_URI: ${{ secrets.OKTA_JWK_URI }} + APIML_SECURITY_OIDC_JWKS_URI: ${{ secrets.OKTA_JWKSET_URI }} APIML_SECURITY_OIDC_IDENTITYMAPPERUSER: APIMTST APIML_SECURITY_OIDC_IDENTITYMAPPERURL: https://gateway-service:10010/zss/api/v1/certificate/dn mock-services: diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AuthExceptionHandler.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AuthExceptionHandler.java index 22f6d70ceb..abb58db4be 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AuthExceptionHandler.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AuthExceptionHandler.java @@ -21,6 +21,7 @@ import org.zowe.apiml.message.api.ApiMessageView; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.security.common.token.InvalidTokenTypeException; +import org.zowe.apiml.security.common.token.NoMainframeIdentityException; import org.zowe.apiml.security.common.token.TokenExpireException; import org.zowe.apiml.security.common.token.TokenFormatNotValidException; import org.zowe.apiml.security.common.token.TokenNotProvidedException; @@ -64,6 +65,8 @@ public void handleException(HttpServletRequest request, HttpServletResponse resp handleAuthMethodNotSupported(request, response, ex); } else if (ex instanceof TokenNotValidException) { handleTokenNotValid(request, response, ex); + } else if (ex instanceof NoMainframeIdentityException) { + handleNoMainframeIdentity(request, response, ex); } else if (ex instanceof TokenNotProvidedException) { handleTokenNotProvided(request, response, ex); } else if (ex instanceof TokenExpireException) { @@ -120,6 +123,11 @@ private void handleTokenNotValid(HttpServletRequest request, HttpServletResponse writeErrorResponse(ErrorType.TOKEN_NOT_VALID.getErrorMessageKey(), HttpStatus.UNAUTHORIZED, request, response); } + private void handleNoMainframeIdentity(HttpServletRequest request, HttpServletResponse response, RuntimeException ex) throws ServletException { + log.debug(MESSAGE_FORMAT, HttpStatus.UNAUTHORIZED.value(), ex.getMessage()); + writeErrorResponse(ErrorType.IDENTITY_MAPPING_FAILED.getErrorMessageKey(), HttpStatus.UNAUTHORIZED, request, response); + } + private void handleTokenNotProvided(HttpServletRequest request, HttpServletResponse response, RuntimeException ex) throws ServletException { log.debug(MESSAGE_FORMAT, HttpStatus.UNAUTHORIZED.value(), ex.getMessage()); writeErrorResponse(ErrorType.TOKEN_NOT_PROVIDED.getErrorMessageKey(), HttpStatus.UNAUTHORIZED, request, response); diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/ErrorType.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/ErrorType.java index 48c7a5c68c..e2ac0ba5d5 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/ErrorType.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/ErrorType.java @@ -31,7 +31,8 @@ public enum ErrorType { INVALID_TOKEN_TYPE("org.zowe.apiml.security.login.invalidTokenType", "Invalid token type in response from Authentication service.", "Review your APIML authentication provider configuration and ensure your Authentication service is working."), USER_SUSPENDED("org.zowe.apiml.security.platform.errno.EMVSSAFEXTRERR","Account Suspended", "Contact your security administrator to unsuspend your account."), NEW_PASSWORD_INVALID("org.zowe.apiml.security.platform.errno.EMVSPASSWORD", "The new password is not valid", "Provide valid password."), - PASSWORD_EXPIRED("org.zowe.apiml.security.platform.errno.EMVSEXPIRE", "Password has expired", "Contact your security administrator to reset your password."); + PASSWORD_EXPIRED("org.zowe.apiml.security.platform.errno.EMVSEXPIRE", "Password has expired", "Contact your security administrator to reset your password."), + IDENTITY_MAPPING_FAILED("org.zowe.apiml.gateway.security.schema.x509.mappingFailed", "No user was found", "Ask your security administrator to connect your token or client certificate with your mainframe user."); private final String errorMessageKey; private final String defaultMessage; diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/token/NoMainframeIdentityException.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/token/NoMainframeIdentityException.java new file mode 100644 index 0000000000..999e1f9da4 --- /dev/null +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/token/NoMainframeIdentityException.java @@ -0,0 +1,35 @@ +/* + * 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.security.common.token; + +import lombok.Getter; +import org.springframework.security.core.AuthenticationException; + +/** + * This exception is thrown in case the OIDC token is valid but distributed ID is not mapped to the mainframe ID. + */ +@Getter +public class NoMainframeIdentityException extends AuthenticationException { + + private final boolean validToken; + private final String token; + + public NoMainframeIdentityException(String msg) { + this(msg, null, false); + } + + public NoMainframeIdentityException(String msg, String token, boolean validToken) { + super(msg); + this.token = token; + this.validToken = validToken; + } + +} diff --git a/apiml-security-common/src/test/java/org/zowe/apiml/security/common/error/AuthExceptionHandlerTest.java b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/error/AuthExceptionHandlerTest.java index 0ed98dd975..38702ee7de 100644 --- a/apiml-security-common/src/test/java/org/zowe/apiml/security/common/error/AuthExceptionHandlerTest.java +++ b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/error/AuthExceptionHandlerTest.java @@ -31,10 +31,7 @@ import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.yaml.YamlMessageService; import org.zowe.apiml.security.common.auth.saf.PlatformReturned; -import org.zowe.apiml.security.common.token.InvalidTokenTypeException; -import org.zowe.apiml.security.common.token.TokenFormatNotValidException; -import org.zowe.apiml.security.common.token.TokenNotProvidedException; -import org.zowe.apiml.security.common.token.TokenNotValidException; +import org.zowe.apiml.security.common.token.*; import jakarta.servlet.ServletException; @@ -135,6 +132,20 @@ void testAuthenticationFailure_whenExceptionIsTokenNotValidException() throws IO verify(objectMapper).writeValue(httpServletResponse.getWriter(), message.mapToView()); } + @Test + void testAuthenticationFailure_whenExceptionIsNoMainframeIdException() throws IOException, ServletException { + authExceptionHandler.handleException( + httpServletRequest, + httpServletResponse, + new NoMainframeIdentityException("ERROR")); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), httpServletResponse.getStatus()); + assertEquals(MediaType.APPLICATION_JSON_VALUE, httpServletResponse.getContentType()); + + Message message = messageService.createMessage(ErrorType.IDENTITY_MAPPING_FAILED.getErrorMessageKey(), httpServletRequest.getRequestURI()); + verify(objectMapper).writeValue(httpServletResponse.getWriter(), message.mapToView()); + } + @Test void testAuthenticationFailure_whenExceptionIsTokenNotProvidedException() throws IOException, ServletException { authExceptionHandler.handleException( diff --git a/apiml-security-common/src/test/java/org/zowe/apiml/security/common/token/NoMainframeIdentityExceptionTest.java b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/token/NoMainframeIdentityExceptionTest.java new file mode 100644 index 0000000000..77d900b189 --- /dev/null +++ b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/token/NoMainframeIdentityExceptionTest.java @@ -0,0 +1,29 @@ +/* + * 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.security.common.token; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class NoMainframeIdentityExceptionTest { + @Nested + class GivenExceptionMessage { + + @Test + void thenReturnMessage() { + String message = "This is an error message"; + NoMainframeIdentityException exception = new NoMainframeIdentityException(message); + assertEquals(message, exception.getMessage()); + } + } +} diff --git a/caching-service-package/src/main/resources/bin/start.sh b/caching-service-package/src/main/resources/bin/start.sh index e98a60243f..6aac7002ea 100755 --- a/caching-service-package/src/main/resources/bin/start.sh +++ b/caching-service-package/src/main/resources/bin/start.sh @@ -178,6 +178,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${CACHING_CODE} java \ -Dcaching.storage.vsam.name=${VSAM_FILE_NAME} \ -Djgroups.bind.address=${ZWE_haInstance_hostname:-localhost} \ -Djgroups.bind.port=${ZWE_configs_storage_infinispan_jgroups_port:-7098} \ + -Djgroups.keyExchange.port=${ZWE_configs_storage_infinispan_jgroups_keyExchange_port:-7118} \ -Dcaching.storage.infinispan.persistence.dataLocation=${ZWE_configs_storage_infinispan_persistence_dataLocation:-data} \ -Dcaching.storage.infinispan.persistence.indexLocation=${ZWE_configs_storage_infinispan_persistence_indexLocation:-index} \ -Dcaching.storage.infinispan.initialHosts=${ZWE_configs_storage_infinispan_initialHosts:-localhost[7098]} \ diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/config/InfinispanConfig.java b/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/config/InfinispanConfig.java index e67110bba7..747005de30 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/config/InfinispanConfig.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/config/InfinispanConfig.java @@ -60,6 +60,8 @@ public class InfinispanConfig { private String port; @Value("${jgroups.bind.address}") private String address; + @Value("${jgroups.keyExchange.port:7118}") + private String keyExchangePort; @PostConstruct void updateKeyring() { @@ -74,6 +76,7 @@ DefaultCacheManager cacheManager(ResourceLoader resourceLoader) { System.setProperty("jgroups.tcpping.initial_hosts", initialHosts); System.setProperty("jgroups.bind.port", port); System.setProperty("jgroups.bind.address", address); + System.setProperty("jgroups.keyExchange.port", keyExchangePort); System.setProperty("server.ssl.keyStoreType", keyStoreType); System.setProperty("server.ssl.keyStore", keyStore); System.setProperty("server.ssl.keyStorePassword", keyStorePass); diff --git a/caching-service/src/main/resources/infinispan.xml b/caching-service/src/main/resources/infinispan.xml index efe8eddff9..3b83eeb35f 100644 --- a/caching-service/src/main/resources/infinispan.xml +++ b/caching-service/src/main/resources/infinispan.xml @@ -23,9 +23,9 @@ keystore_type="${server.ssl.keyStoreType}" keystore_password="${server.ssl.keyStorePassword}" secret_key_algorithm="RSA" + port="${jgroups.keyExchange.port}" /> - createRequest(ServiceInstance instance @Override @SuppressWarnings("squid:S2092") // the internal API cannot define generic more specifically protected Mono processResponse(ServerWebExchange exchange, GatewayFilterChain chain, ZaasTokenResponse response) { - ServerHttpRequest request; + ServerHttpRequest request = null; if (response.getToken() != null) { - request = exchange.getRequest().mutate().headers(headers -> - headers.add(HttpHeaders.COOKIE, new HttpCookie(response.getCookieName(), response.getToken()).toString()) - ).build(); - } else { + if (!StringUtils.isEmpty(response.getCookieName())) { + request = exchange.getRequest().mutate().headers(headers -> + headers.add(HttpHeaders.COOKIE, new HttpCookie(response.getCookieName(), response.getToken()).toString()) + ).build(); + } + if (!StringUtils.isEmpty(response.getHeaderName())) { + request = exchange.getRequest().mutate().headers(headers -> + headers.add(response.getHeaderName(), response.getToken()) + ).build(); + } + } + if (request == null) { request = updateHeadersForError(exchange, "Invalid or missing authentication"); } diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/acceptance/TokenSchemeTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/acceptance/TokenSchemeTest.java index eb2503d92c..9cb4af06fe 100644 --- a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/acceptance/TokenSchemeTest.java +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/acceptance/TokenSchemeTest.java @@ -41,7 +41,7 @@ public abstract class TokenSchemeTest { private static final String COOKIE_NAME = "token_cookie"; private static final String JWT = "jwt"; - private static final ZaasTokenResponse OK_RESPONSE = new ZaasTokenResponse(COOKIE_NAME, JWT); + private static final ZaasTokenResponse OK_RESPONSE = ZaasTokenResponse.builder().cookieName(COOKIE_NAME).token(JWT).build(); public abstract String getTokenEndpoint(); @@ -83,23 +83,23 @@ void createAllZaasServices() throws IOException { // on the beginning prepare all as zombie, each test will decide zaasError = mockService("gateway").scope(MockService.Scope.CLASS) .addEndpoint(getTokenEndpoint()) - .responseCode(500) + .responseCode(500) .and().build(); zaasZombie = mockService("gateway").scope(MockService.Scope.CLASS) .addEndpoint(getTokenEndpoint()) - .bodyJson(new ZaasTokenResponse()) + .bodyJson(new ZaasTokenResponse()) .and().build(); zaasOk = mockService("gateway").scope(MockService.Scope.CLASS) .addEndpoint(getTokenEndpoint()) - .bodyJson(OK_RESPONSE) - .assertion(he -> assertEquals("service", he.getRequestHeaders().getFirst("x-service-id"))) + .bodyJson(OK_RESPONSE) + .assertion(he -> assertEquals("service", he.getRequestHeaders().getFirst("x-service-id"))) .and().build(); // south-bound service - alive for all tests service = mockService("service").scope(MockService.Scope.CLASS) .authenticationScheme(getAuthenticationScheme()) .addEndpoint("/service/test") - .assertion(he -> assertEquals(JWT, getCookie(he, COOKIE_NAME))) + .assertion(he -> assertEquals(JWT, getCookie(he, COOKIE_NAME))) .and().start(); } @@ -188,13 +188,13 @@ private String getServiceUrl() { void init() throws IOException { zaas = mockService("gateway").scope(MockService.Scope.CLASS) .addEndpoint(getTokenEndpoint()) - .responseCode(401) + .responseCode(401) .and().start(); service = mockService("service").scope(MockService.Scope.CLASS) .authenticationScheme(getAuthenticationScheme()) .addEndpoint("/service/test") - .assertion(he -> assertNull(getCookie(he, COOKIE_NAME))) + .assertion(he -> assertNull(getCookie(he, COOKIE_NAME))) .and().start(); } @@ -229,15 +229,15 @@ private String getServiceUrl() { void init() throws IOException { zaas = mockService("gateway").scope(MockService.Scope.CLASS) .addEndpoint(getTokenEndpoint()) - .responseCode(200) - .bodyJson(new ZaasTokenResponse()) + .responseCode(200) + .bodyJson(new ZaasTokenResponse()) .and().start(); service = mockService("service").scope(MockService.Scope.CLASS) .authenticationScheme(getAuthenticationScheme()) - .addEndpoint("/service/test") - .assertion(he -> assertNull(getCookie(he, COOKIE_NAME))) + .addEndpoint("/service/test") + .assertion(he -> assertNull(getCookie(he, COOKIE_NAME))) .and().start(); } @@ -258,9 +258,9 @@ void givenNoCredentials_whenCallingAService_thenDontPropagateCredentials() { void givenInvalidCredentials_whenCallingAService_thenDontPropagateCredentials() { given() .header(HttpHeaders.AUTHORIZATION, "Baerer nonSense") - .when() + .when() .get(getServiceUrl()) - .then() + .then() .statusCode(200); assertEquals(1, zaas.getCounter()); assertEquals(1, service.getCounter()); @@ -285,24 +285,24 @@ void init() throws IOException { MockService createZaas() throws IOException { return mockService("gateway").scope(MockService.Scope.CLASS) .addEndpoint(getTokenEndpoint()) - .bodyJson(OK_RESPONSE) - .assertion(he -> assertEquals("Bearer userJwt", he.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION))) - - .assertion(he -> assertEquals("service", he.getRequestHeaders().getFirst("x-service-id"))) - - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("myheader"))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-SAF-Token"))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-Public"))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-DistinguishedName"))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-CommonName"))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("Client-Cert"))) - - .assertion(he -> assertNull(getCookie(he, "mycookie"))) - .assertion(he -> assertEquals("pat", getCookie(he, "personalAccessToken"))) - .assertion(he -> assertEquals("jwt1", getCookie(he, "apimlAuthenticationToken"))) - .assertion(he -> assertEquals("jwt2", getCookie(he, "apimlAuthenticationToken.2"))) - .assertion(he -> assertNull(getCookie(he, "jwtToken"))) - .assertion(he -> assertNull(getCookie(he, "LtpaToken2"))) + .bodyJson(OK_RESPONSE) + .assertion(he -> assertEquals("Bearer userJwt", he.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION))) + + .assertion(he -> assertEquals("service", he.getRequestHeaders().getFirst("x-service-id"))) + + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("myheader"))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-SAF-Token"))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-Public"))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-DistinguishedName"))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-CommonName"))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("Client-Cert"))) + + .assertion(he -> assertNull(getCookie(he, "mycookie"))) + .assertion(he -> assertEquals("pat", getCookie(he, "personalAccessToken"))) + .assertion(he -> assertEquals("jwt1", getCookie(he, "apimlAuthenticationToken"))) + .assertion(he -> assertEquals("jwt2", getCookie(he, "apimlAuthenticationToken.2"))) + .assertion(he -> assertNull(getCookie(he, "jwtToken"))) + .assertion(he -> assertNull(getCookie(he, "LtpaToken2"))) .and().start(); } @@ -310,22 +310,22 @@ MockService createService() throws IOException { return mockService("service").scope(MockService.Scope.CLASS) .authenticationScheme(getAuthenticationScheme()) .addEndpoint("/service/test") - .assertion(he -> assertEquals(JWT, getCookie(he, COOKIE_NAME))) - - .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"))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-DistinguishedName"))) - .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-CommonName"))) - .assertion(he -> assertEquals("myvalue", he.getRequestHeaders().getFirst("myheader"))) - - .assertion(he -> assertNull(getCookie(he, "personalAccessToken"))) - .assertion(he -> assertNull(getCookie(he, "apimlAuthenticationToken"))) - .assertion(he -> assertNull(getCookie(he, "apimlAuthenticationToken.2"))) - .assertion(he -> assertNull(getCookie(he, "jwtToken"))) - .assertion(he -> assertNull(getCookie(he, "LtpaToken2"))) - .assertion(he -> assertEquals("mycookievalue", getCookie(he, "mycookie"))) + .assertion(he -> assertEquals(JWT, getCookie(he, COOKIE_NAME))) + + .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"))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-DistinguishedName"))) + .assertion(he -> assertNull(he.getRequestHeaders().getFirst("X-Certificate-CommonName"))) + .assertion(he -> assertEquals("myvalue", he.getRequestHeaders().getFirst("myheader"))) + + .assertion(he -> assertNull(getCookie(he, "personalAccessToken"))) + .assertion(he -> assertNull(getCookie(he, "apimlAuthenticationToken"))) + .assertion(he -> assertNull(getCookie(he, "apimlAuthenticationToken.2"))) + .assertion(he -> assertNull(getCookie(he, "jwtToken"))) + .assertion(he -> assertNull(getCookie(he, "LtpaToken2"))) + .assertion(he -> assertEquals("mycookievalue", getCookie(he, "mycookie"))) .and().start(); } @@ -352,9 +352,9 @@ void givenMultipleHeaders_whenCallingAService_thenTheyAreResend() { .cookie("apimlAuthenticationToken.2", "jwt2") .cookie("jwtToken", "jwtToken") .cookie("LtpaToken2", "LtpaToken2") - .when() + .when() .get(getServiceUrl()) - .then() + .then() .statusCode(200); assertEquals(1, zaas.getCounter()); diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/filters/TokenFilterFactoryTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/filters/TokenFilterFactoryTest.java new file mode 100644 index 0000000000..5ab2a2a357 --- /dev/null +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/filters/TokenFilterFactoryTest.java @@ -0,0 +1,99 @@ +/* + * 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.cloudgatewayservice.filters; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.zowe.apiml.constants.ApimlConstants; +import org.zowe.apiml.zaas.ZaasTokenResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +class TokenFilterFactoryTest { + + @Nested + class RequestUpdate { + + private MockServerHttpRequest testRequestMutation(ZaasTokenResponse response) { + MockServerHttpRequest request = MockServerHttpRequest.get("/url").build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + + new TokenFilterFactory(null, null, null) { + @Override + public String getEndpointUrl(ServiceInstance instance) { + return null; + } + }.processResponse(exchange, mock(GatewayFilterChain.class), response); + + return request; + } + + @Nested + class ValidResponse { + + @Test + void givenHeaderResponse_whenHandling_thenUpdateTheRequest() { + MockServerHttpRequest request = testRequestMutation(ZaasTokenResponse.builder() + .headerName("headerName") + .token("headerValue") + .build() + ); + assertEquals("headerValue", request.getHeaders().getFirst("headerName")); + } + + @Test + void givenCookieResponse_whenHandling_thenUpdateTheRequest() { + MockServerHttpRequest request = testRequestMutation(ZaasTokenResponse.builder() + .cookieName("cookieName") + .token("cookieValue") + .build() + ); + assertEquals("cookieName=\"cookieValue\"", request.getHeaders().getFirst("cookie")); + } + + } + + @Nested + class InvalidResponse { + + @Test + void givenEmptyResponse_whenHandling_thenNoUpdate() { + MockServerHttpRequest request = testRequestMutation(ZaasTokenResponse.builder() + .token("jwt") + .build() + ); + assertEquals(1, request.getHeaders().size()); + assertTrue(request.getHeaders().containsKey(ApimlConstants.AUTH_FAIL_HEADER)); + } + + @Test + void givenCookieAndHeaderInResponse_whenHandling_thenSetBoth() { + MockServerHttpRequest request = testRequestMutation(ZaasTokenResponse.builder() + .cookieName("cookie") + .headerName("header") + .token("jwt") + .build() + ); + assertEquals("jwt", request.getHeaders().getFirst("header")); + assertEquals("cookie=\"jwt\"", request.getHeaders().getFirst("cookie")); + } + + } + + } + +} \ No newline at end of file diff --git a/common-service-core/src/main/java/org/zowe/apiml/constants/ApimlConstants.java b/common-service-core/src/main/java/org/zowe/apiml/constants/ApimlConstants.java index 2e76dbeb59..c1112dcf5b 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/constants/ApimlConstants.java +++ b/common-service-core/src/main/java/org/zowe/apiml/constants/ApimlConstants.java @@ -26,5 +26,6 @@ private ApimlConstants() { public static final String AUTH_FAIL_HEADER = "X-Zowe-Auth-Failure"; public static final String HTTP_CLIENT_USE_CLIENT_CERTIFICATE = "apiml.useClientCert"; public static final String SAF_TOKEN_HEADER = "X-SAF-Token"; + public static final String HEADER_OIDC_TOKEN = "OIDC-token"; } diff --git a/common-service-core/src/main/java/org/zowe/apiml/zaas/ZaasTokenResponse.java b/common-service-core/src/main/java/org/zowe/apiml/zaas/ZaasTokenResponse.java index 21dd40ce65..f4ebf59a9e 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/zaas/ZaasTokenResponse.java +++ b/common-service-core/src/main/java/org/zowe/apiml/zaas/ZaasTokenResponse.java @@ -11,15 +11,18 @@ package org.zowe.apiml.zaas; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor +@Builder public class ZaasTokenResponse { private String cookieName; + private String headerName; private String token; } diff --git a/gateway-package/src/main/resources/bin/start.sh b/gateway-package/src/main/resources/bin/start.sh index 6312bfa71f..ce1bb0261e 100755 --- a/gateway-package/src/main/resources/bin/start.sh +++ b/gateway-package/src/main/resources/bin/start.sh @@ -32,6 +32,7 @@ # - ZWE_configs_apiml_catalog_serviceId # - ZWE_configs_apiml_gateway_timeoutMillis # - ZWE_configs_apiml_security_auth_provider +# - ZWE_configs_apiml_security_allowtokenrefresh # - ZWE_configs_apiml_security_auth_zosmf_jwtAutoconfiguration # - ZWE_configs_apiml_security_auth_zosmf_serviceId # - ZWE_configs_apiml_security_authorization_endpoint_enabled @@ -300,6 +301,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${GATEWAY_CODE} java \ -Dapiml.security.oidc.identityMapperUser=${ZWE_configs_apiml_security_oidc_identityMapperUser:-${ZWE_zowe_setup_security_users_zowe:-ZWESVUSR}} \ -Dapiml.security.oidc.jwks.uri=${ZWE_configs_apiml_security_oidc_jwks_uri} \ -Dapiml.security.oidc.jwks.refreshInternalHours=${ZWE_configs_apiml_security_oidc_jwks_refreshInternalHours:-1} \ + -Dapiml.security.allowTokenRefresh=${ZWE_configs_apiml_security_allowtokenrefresh:-false} \ -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ -Dloader.path=${GATEWAY_LOADER_PATH} \ -Djava.library.path=${LIBPATH} \ diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/AuthController.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/AuthController.java index e7609219a5..0fe1eab3a6 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/AuthController.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/AuthController.java @@ -29,16 +29,23 @@ import org.springframework.lang.Nullable; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; import org.zowe.apiml.gateway.security.service.AuthenticationService; import org.zowe.apiml.gateway.security.service.JwtSecurity; +import org.zowe.apiml.gateway.security.service.token.OIDCTokenProvider; import org.zowe.apiml.gateway.security.service.zosmf.ZosmfService; import org.zowe.apiml.gateway.security.webfinger.WebFingerProvider; import org.zowe.apiml.gateway.security.webfinger.WebFingerResponse; import org.zowe.apiml.message.api.ApiMessageView; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.security.common.token.AccessTokenProvider; -import org.zowe.apiml.security.common.token.OIDCProvider; import org.zowe.apiml.security.common.token.TokenNotValidException; import javax.servlet.http.HttpServletRequest; @@ -46,9 +53,15 @@ import java.io.IOException; import java.io.StringWriter; import java.security.PublicKey; -import java.util.*; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; -import static org.apache.http.HttpStatus.*; +import static org.apache.http.HttpStatus.SC_NO_CONTENT; +import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; /** * Controller offer method to control security. It can contains method for user and also method for calling services @@ -69,7 +82,7 @@ public class AuthController { private final AccessTokenProvider tokenProvider; @Nullable - private final OIDCProvider oidcProvider; + private final OIDCTokenProvider oidcProvider; private final WebFingerProvider webFingerProvider; private static final String TOKEN_KEY = "token"; @@ -210,9 +223,18 @@ public void distributeInvalidate(HttpServletRequest request, HttpServletResponse @ResponseBody @HystrixCommand public Map getAllPublicKeys() { - final List keys = new LinkedList<>(zosmfService.getPublicKeys().getKeys()); + List keys; + if (jwtSecurity.actualJwtProducer() == JwtSecurity.JwtProducer.ZOSMF) { + keys = new LinkedList<>(zosmfService.getPublicKeys().getKeys()); + } else { + keys = new LinkedList<>(); + } Optional key = jwtSecurity.getJwkPublicKey(); key.ifPresent(keys::add); + JWKSet oidcSet = oidcProvider.getJwkSet(); + if (oidcSet != null) { + keys.addAll(oidcSet.getKeys()); + } return new JWKSet(keys).toJSONObject(true); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/pre/ServiceAuthenticationFilter.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/pre/ServiceAuthenticationFilter.java index 73c003b151..5011479a51 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/pre/ServiceAuthenticationFilter.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/pre/ServiceAuthenticationFilter.java @@ -20,6 +20,7 @@ import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.gateway.security.service.ServiceAuthenticationServiceImpl; import org.zowe.apiml.gateway.security.service.schema.AuthenticationCommand; +import org.zowe.apiml.gateway.security.service.schema.OidcCommand; import org.zowe.apiml.gateway.security.service.schema.source.AuthSchemeException; import org.zowe.apiml.gateway.security.service.schema.source.AuthSource; import org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService; @@ -27,6 +28,7 @@ import org.zowe.apiml.message.core.MessageType; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; +import org.zowe.apiml.security.common.token.NoMainframeIdentityException; import org.zowe.apiml.security.common.token.TokenExpireException; import org.zowe.apiml.security.common.token.TokenNotValidException; @@ -82,6 +84,13 @@ public Object run() { if (authSource.isPresent() && !isSourceValidForCommand(authSource.get(), cmd)) { throw new AuthSchemeException("org.zowe.apiml.gateway.security.invalidAuthentication"); } + } catch (NoMainframeIdentityException noIdentityException) { + String error = this.messageService.createMessage("org.zowe.apiml.gateway.security.schema.x509.mappingFailed").mapToLogMessage(); + sendErrorMessage(error, context); + if (!noIdentityException.isValidToken()) { + return null; + } + cmd = new OidcCommand(noIdentityException.getToken()); } catch (TokenExpireException tee) { String error = this.messageService.createMessage("org.zowe.apiml.gateway.security.expiredToken").mapToLogMessage(); sendErrorMessage(error, context); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/mapping/ExternalMapper.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/mapping/ExternalMapper.java index fb5421c4cc..e8dce4a856 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/mapping/ExternalMapper.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/mapping/ExternalMapper.java @@ -16,7 +16,6 @@ import org.apache.commons.lang.StringUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.message.BasicHeader; @@ -25,6 +24,8 @@ import org.springframework.http.MediaType; import org.zowe.apiml.gateway.security.mapping.model.MapperResponse; import org.zowe.apiml.gateway.security.service.TokenCreationService; +import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import javax.validation.constraints.NotNull; @@ -45,9 +46,11 @@ public abstract class ExternalMapper { private final CloseableHttpClient httpClientProxy; private final TokenCreationService tokenCreationService; private final AuthConfigurationProperties authConfigurationProperties; - protected static final ObjectMapper objectMapper = new ObjectMapper(); + @InjectApimlLogger + protected ApimlLogger apimlLog = ApimlLogger.empty(); + MapperResponse callExternalMapper(@NotNull HttpEntity payload) { if (StringUtils.isBlank(mapperUrl)) { log.warn("Configuration error: External identity mapper URL is not set."); @@ -73,8 +76,15 @@ MapperResponse callExternalMapper(@NotNull HttpEntity payload) { if (httpResponse.getEntity() != null) { response = EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8); } - if (statusCode < HttpStatus.SC_OK || statusCode >= HttpStatus.SC_MULTIPLE_CHOICES) { - log.warn("Unexpected response from the external identity mapper. Status: {} body: {}", statusCode, response); + if (statusCode == 0) { + return null; + } + if (!org.springframework.http.HttpStatus.valueOf(statusCode).is2xxSuccessful()) { + if (org.springframework.http.HttpStatus.valueOf(statusCode).is5xxServerError()) { + apimlLog.log("org.zowe.apiml.gateway.security.unexpectedMappingResponse", statusCode, response); + } else { + log.debug("Unexpected response from the external identity mapper. Status: {} body: {}", statusCode, response); + } return null; } log.debug("External identity mapper API returned: {}", response); @@ -82,9 +92,9 @@ MapperResponse callExternalMapper(@NotNull HttpEntity payload) { return objectMapper.readValue(response, MapperResponse.class); } } catch (IOException e) { - log.warn("Error occurred while communicating with external identity mapper", e); + apimlLog.log("org.zowe.apiml.gateway.security.InvalidMappingResponse", e); } catch (URISyntaxException e) { - log.warn("Configuration error: Failed to construct the external identity mapper URI.", e); + apimlLog.log("org.zowe.apiml.gateway.security.InvalidMapperUrl", e); } return null; diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/OidcCommand.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/OidcCommand.java new file mode 100644 index 0000000000..5f1718968a --- /dev/null +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/OidcCommand.java @@ -0,0 +1,56 @@ +/* + * 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.gateway.security.service.schema; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.zuul.context.RequestContext; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpHeaders; +import org.zowe.apiml.constants.ApimlConstants; +import org.zowe.apiml.util.CookieUtil; + +import java.util.HashSet; +import java.util.Set; + +import static org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper.IGNORED_HEADERS; +import static org.zowe.apiml.gateway.security.service.schema.JwtCommand.COOKIE_HEADER; +import static org.zowe.apiml.security.SecurityUtils.COOKIE_AUTH_NAME; + +@RequiredArgsConstructor +public class OidcCommand extends AuthenticationCommand { + + + private final String token; + + @Override + public void apply(InstanceInfo instanceInfo) { + RequestContext context = RequestContext.getCurrentContext(); + + String cookie = context.getRequest().getHeader(COOKIE_HEADER); + if (!StringUtils.isEmpty(cookie)) { + cookie = CookieUtil.removeCookie(cookie, COOKIE_AUTH_NAME); + if (StringUtils.isEmpty(cookie)) { + removeCookieFromRequest(context, COOKIE_HEADER); + } else { + context.addZuulRequestHeader(COOKIE_HEADER, cookie); + } + } + removeCookieFromRequest(context, HttpHeaders.AUTHORIZATION); + + context.addZuulRequestHeader(ApimlConstants.HEADER_OIDC_TOKEN, token); + } + + void removeCookieFromRequest(RequestContext context, String headerName) { + ((Set) context.computeIfAbsent(IGNORED_HEADERS, k -> new HashSet<>())).add(headerName.toLowerCase()); // NOSONAR + } + +} diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/DefaultAuthSourceService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/DefaultAuthSourceService.java index 96c88ead21..7756622f3c 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/DefaultAuthSourceService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/DefaultAuthSourceService.java @@ -10,6 +10,7 @@ package org.zowe.apiml.gateway.security.service.schema.source; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; @@ -40,13 +41,14 @@ @Primary @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) @EnableAspectJAutoProxy(proxyTargetClass = true) +@Slf4j public class DefaultAuthSourceService implements AuthSourceService { private final Map map = new EnumMap<>(AuthSourceType.class); private final boolean isX509Enabled; private final boolean isPATEnabled; private final boolean isOIDCEnabled; - + private static final String LOG_MESSAGE = "Authentication request towards the southbound service {} using the auth source {}"; /** * Build the map of the specific implementations of {@link AuthSourceService} for processing of different type of authentications * @@ -106,6 +108,7 @@ public Optional getAuthSourceFromRequest(HttpServletRequest request) service = getService(AuthSourceType.CLIENT_CERT); authSource = service.getAuthSourceFromRequest(request); } + authSource.ifPresent(source -> log.debug(LOG_MESSAGE, request.getRequestURI(), source.getType())); return authSource; } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/OIDCAuthSourceService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/OIDCAuthSourceService.java index 5895d430d0..3a898c2d13 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/OIDCAuthSourceService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/OIDCAuthSourceService.java @@ -22,9 +22,9 @@ import org.zowe.apiml.message.core.MessageType; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; +import org.zowe.apiml.security.common.token.NoMainframeIdentityException; import org.zowe.apiml.security.common.token.OIDCProvider; import org.zowe.apiml.security.common.token.QueryResponse; -import org.zowe.apiml.security.common.token.TokenNotValidException; import javax.servlet.http.HttpServletRequest; import java.util.Optional; @@ -109,7 +109,7 @@ private AuthSource.Parsed parseOIDCToken(OIDCAuthSource oidcAuthSource, Authenti String mappedUser = mapper.mapToMainframeUserId(oidcAuthSource); if (StringUtils.isEmpty(mappedUser)) { logger.log(MessageType.DEBUG, "No mainframe user id retrieved. Cancel parsing of OIDC token."); - throw new TokenNotValidException("No mainframe identity found."); + throw new NoMainframeIdentityException("No mainframe identity found.", token, true); } logger.log(MessageType.DEBUG, "Parsing OIDC token."); QueryResponse response = authenticationService.parseJwtToken(token); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/token/OIDCTokenProvider.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/token/OIDCTokenProvider.java index 2402a7da8b..6ee30e6213 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/token/OIDCTokenProvider.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/token/OIDCTokenProvider.java @@ -11,22 +11,19 @@ package org.zowe.apiml.gateway.security.service.token; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.KeyType; +import com.nimbusds.jose.jwk.KeyUse; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Clock; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.io.Decoders; -import lombok.NonNull; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpEntity; -import org.apache.http.HttpStatus; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.util.EntityUtils; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -40,13 +37,9 @@ import javax.annotation.PostConstruct; import java.io.IOException; -import java.math.BigInteger; -import java.nio.charset.StandardCharsets; +import java.net.URL; import java.security.Key; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.RSAPublicKeySpec; +import java.text.ParseException; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @@ -78,72 +71,55 @@ public class OIDCTokenProvider implements OIDCProvider { @Value("${apiml.security.oidc.jwks.refreshInternalHours:1}") private int jwkRefreshInterval; - @Qualifier("secureHttpClientWithoutKeystore") - @NonNull - private final CloseableHttpClient httpClient; - @Qualifier("oidcJwtClock") private final Clock clock; - @Qualifier("oidcJwkMapper") - private final ObjectMapper mapper; - - private Map jwks = new ConcurrentHashMap<>(); + @Getter + private Map publicKeys = new ConcurrentHashMap<>(); + @Getter + private JWKSet jwkSet; @PostConstruct public void afterPropertiesSet() { - this.fetchJwksUrls(); + this.fetchJWKSet(); Executors.newSingleThreadScheduledExecutor(r -> new Thread("OIDC JWK Refresh")) - .scheduleAtFixedRate(this::fetchJwksUrls , jwkRefreshInterval, jwkRefreshInterval, TimeUnit.HOURS); + .scheduleAtFixedRate(this::fetchJWKSet, jwkRefreshInterval, jwkRefreshInterval, TimeUnit.HOURS); } @Retryable - void fetchJwksUrls() { + void fetchJWKSet() { if (StringUtils.isBlank(jwksUri)) { log.debug("OIDC JWK URI not provided, JWK refresh not performed"); return; } log.debug("Refreshing JWK endpoints {}", jwksUri); - HttpGet getRequest = new HttpGet(jwksUri); + try { - CloseableHttpResponse response = httpClient.execute(getRequest); - final int statusCode = response.getStatusLine() != null ? response.getStatusLine().getStatusCode() : 0; - final HttpEntity responseEntity = response.getEntity(); - String responseBody = ""; - if (responseEntity != null) { - responseBody = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8); - } - if (statusCode == HttpStatus.SC_OK && !responseBody.isEmpty()) { - jwks.clear(); - JwkKeys jwkKeys = mapper.readValue(responseBody, JwkKeys.class); - jwks.putAll(processKeys(jwkKeys)); - } else { - log.error("Failed to obtain JWKs from URI {}. Unexpected response: {}, response text: {}", jwksUri, statusCode, responseBody); - } - } catch (IOException | IllegalStateException e) { - log.error("Error processing response from URI {}", jwksUri, e.getMessage()); + publicKeys.clear(); + jwkSet = null; + jwkSet = JWKSet.load(new URL(jwksUri)); + publicKeys.putAll(processKeys(jwkSet)); + } catch (IOException | ParseException | IllegalStateException e) { + log.error("Error processing response from URI {} message: {}", jwksUri, e.getMessage()); } } - private Map processKeys(JwkKeys jwkKeys) { + + private Map processKeys(JWKSet jwkKeys) { return jwkKeys.getKeys().stream() - .filter(jwkKey -> "sig".equals(jwkKey.getUse())) - .filter(jwkKey -> "RSA".equals(jwkKey.getKty())) - .collect(Collectors.toMap(JwkKeys.Key::getKid, jwkKey -> { - BigInteger modulus = base64ToBigInteger(jwkKey.getN()); - BigInteger exponent = base64ToBigInteger(jwkKey.getE()); - RSAPublicKeySpec rsaPublicKeySpec = new RSAPublicKeySpec(modulus, exponent); + .filter(jwkKey -> { + KeyUse keyUse = jwkKey.getKeyUse(); + KeyType keyType = jwkKey.getKeyType(); + return keyUse != null && keyType != null && "sig".equals(keyUse.getValue()) && "RSA".equals(keyType.getValue()); + }) + .collect(Collectors.toMap(JWK::getKeyID, jwkKey -> { try { - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - return keyFactory.generatePublic(rsaPublicKeySpec); - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - throw new IllegalStateException("Failed to parse public key"); + return jwkKey.toRSAKey().toRSAPublicKey(); + } catch (JOSEException e) { + log.debug("Problem with getting RSA Public key from JWK. ", e.getCause()); + throw new IllegalStateException("Failed to parse public key", e); } - })); - } - - private BigInteger base64ToBigInteger(String value) { - return new BigInteger(1, Decoders.BASE64URL.decode(value)); + })); } @Override @@ -154,7 +130,7 @@ public boolean isValid(String token) { } String kid = getKeyId(token); logger.log(MessageType.DEBUG, "Token signed by key {}", kid); - return Optional.ofNullable(jwks.get(kid)) + return Optional.ofNullable(publicKeys.get(kid)) .map(key -> validate(token, key)) .map(claims -> claims != null && !claims.isEmpty()) .orElse(false); @@ -163,11 +139,11 @@ public boolean isValid(String token) { private String getKeyId(String token) { try { return String.valueOf(Jwts.parserBuilder() - .setClock(clock) - .build() - .parseClaimsJwt(token.substring(0, token.lastIndexOf('.') + 1)) - .getHeader() - .get("kid")); + .setClock(clock) + .build() + .parseClaimsJwt(token.substring(0, token.lastIndexOf('.') + 1)) + .getHeader() + .get("kid")); } catch (JwtException e) { log.error("OIDC Token is not valid: {}", e.getMessage()); return ""; @@ -187,4 +163,5 @@ private Claims validate(String token, Key key) { return null; // NOSONAR } } + } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java index 47e11a77ee..210135a6ad 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java @@ -28,7 +28,12 @@ import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; -import org.springframework.http.*; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.security.authentication.AuthenticationServiceException; @@ -229,19 +234,19 @@ public String getZosmfRealm(String infoURIEndpoint) { public ZaasTokenResponse exchangeAuthenticationForZosmfToken(String token, AuthSource.Parsed authSource) throws ServiceNotFoundException { switch (authSource.getOrigin()) { case ZOSMF: - return new ZaasTokenResponse(ZosmfService.TokenType.JWT.getCookieName(), token); + return ZaasTokenResponse.builder().cookieName(JWT.getCookieName()).token(token).build(); case ZOWE: String ltpaToken = authenticationService.getLtpaToken(token); if (ltpaToken != null) { - return new ZaasTokenResponse(ZosmfService.TokenType.LTPA.getCookieName(), ltpaToken); + return ZaasTokenResponse.builder().cookieName(LTPA.getCookieName()).token(ltpaToken).build(); } default: Map zosmfTokens = tokenCreationService.createZosmfTokensWithoutCredentials(authSource.getUserId()); if (zosmfTokens.containsKey(JWT)) { - return new ZaasTokenResponse(ZosmfService.TokenType.JWT.getCookieName(), zosmfTokens.get(JWT)); + return ZaasTokenResponse.builder().cookieName(JWT.getCookieName()).token(zosmfTokens.get(JWT)).build(); } else if (zosmfTokens.containsKey(LTPA)) { - return new ZaasTokenResponse(ZosmfService.TokenType.LTPA.getCookieName(), zosmfTokens.get(LTPA)); + return ZaasTokenResponse.builder().cookieName(LTPA.getCookieName()).token(zosmfTokens.get(LTPA)).build(); } } @@ -271,12 +276,12 @@ public boolean isAccessible() { log.debug("Verifying z/OSMF accessibility on info endpoint: {}", infoURIEndpoint); try { final ResponseEntity info = restTemplateWithoutKeystore - .exchange( - infoURIEndpoint, - HttpMethod.GET, - new HttpEntity<>(headers), - ZosmfInfo.class - ); + .exchange( + infoURIEndpoint, + HttpMethod.GET, + new HttpEntity<>(headers), + ZosmfInfo.class + ); if (info.getStatusCode() != HttpStatus.OK) { log.error("Unexpected status code {} from z/OSMF accessing URI {}\n" @@ -557,12 +562,13 @@ public JWKSet getPublicKeys() { final String url = getURI(getZosmfServiceId(), authConfigurationProperties.getZosmf().getJwtEndpoint()); try { - final String json = restTemplateWithoutKeystore.getForObject(url, String.class); - return JWKSet.parse(json); + return JWKSet.load(new URL(url)); } catch (ParseException pe) { log.debug("Invalid format of public keys from z/OSMF", pe); } catch (HttpClientErrorException.NotFound nf) { log.debug("Cannot get public keys from z/OSMF", nf); + } catch (IOException me) { + log.debug("Can't read JWK due to the exception " + me.getMessage(), me.getCause()); } return new JWKSet(); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/ws/WebSocketProxyClientHandler.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/ws/WebSocketProxyClientHandler.java index 3cab801dae..df810ee827 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/ws/WebSocketProxyClientHandler.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/ws/WebSocketProxyClientHandler.java @@ -80,7 +80,7 @@ static CloseStatus getCloseStatusByError(Throwable exception) { @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { - log.warn("WebSocket transport error in session {}: {}", session.getId(), exception.getMessage()); + log.debug("WebSocket transport error in session {}: {}", session.getId(), exception.getMessage()); if (webSocketServerSession.isOpen()) { webSocketServerSession.close(getCloseStatusByError(exception)); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/ws/WebSocketProxyServerHandler.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/ws/WebSocketProxyServerHandler.java index b768ae3b9b..f175f83eef 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/ws/WebSocketProxyServerHandler.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/ws/WebSocketProxyServerHandler.java @@ -137,6 +137,7 @@ private void routeToService(WebSocketSession webSocketSession, String serviceId, } try { + log.debug("Trying to open a WebSocket connection and route to the {} service", serviceId); meAsProxy.openConn(serviceId, service, webSocketSession, path); } catch (WebSocketProxyError e) { log.debug("Error opening WebSocket connection to: {}, {}", service.getServiceUrl(), e.getMessage()); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/zaas/ZaasController.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/zaas/ZaasController.java index 15eae10852..d67151c139 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/zaas/ZaasController.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/zaas/ZaasController.java @@ -16,7 +16,13 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.gateway.security.service.TokenCreationService; import org.zowe.apiml.gateway.security.service.schema.source.AuthSource; import org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService; @@ -24,6 +30,7 @@ import org.zowe.apiml.gateway.security.ticket.ApplicationNameNotFoundException; import org.zowe.apiml.passticket.IRRPassTicketGenerationException; import org.zowe.apiml.passticket.PassTicketService; +import org.zowe.apiml.security.common.token.NoMainframeIdentityException; import org.zowe.apiml.ticket.TicketRequest; import org.zowe.apiml.ticket.TicketResponse; import org.zowe.apiml.zaas.ZaasTokenResponse; @@ -83,7 +90,26 @@ public ResponseEntity getZoweJwt(@RequestAttribute(AUTH_SOURC return ResponseEntity .status(HttpStatus.OK) - .body(new ZaasTokenResponse(COOKIE_AUTH_NAME, token)); + .body(ZaasTokenResponse.builder().cookieName(COOKIE_AUTH_NAME).token(token).build()); + + } + + /** + * Controller level exception handler for cases when NO mapping with mainframe ID exists. + * + * @param authSource credentials that will be used for authentication translation + * @param nmie exception thrown in case of missing user mapping + * @return status code OK, header name and value if OIDC token is valid, otherwise status code UNAUTHORIZED + */ + @ExceptionHandler(NoMainframeIdentityException.class) + public ResponseEntity handleNoMainframeIdException(@RequestAttribute(AUTH_SOURCE_ATTR) AuthSource authSource, NoMainframeIdentityException nmie) { + if (nmie.isValidToken() && authSource.getType() == AuthSource.AuthSourceType.OIDC) { + return ResponseEntity + .status(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON) + .body(ZaasTokenResponse.builder().headerName(ApimlConstants.HEADER_OIDC_TOKEN).token(String.valueOf(authSource.getRawSource())).build()); + } + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } @PostMapping(path = "safIdt", produces = MediaType.APPLICATION_JSON_VALUE) @@ -99,7 +125,7 @@ public ResponseEntity getSafIdToken(@RequestBody TicketReques String safIdToken = tokenCreationService.createSafIdTokenWithoutCredentials(authSourceParsed.getUserId(), applicationName); return ResponseEntity .status(HttpStatus.OK) - .body(new ZaasTokenResponse("", safIdToken)); + .body(ZaasTokenResponse.builder().token(safIdToken).build()); } } diff --git a/gateway-service/src/main/resources/gateway-log-messages.yml b/gateway-service/src/main/resources/gateway-log-messages.yml index b4aa34e9e3..17ff3a21c3 100644 --- a/gateway-service/src/main/resources/gateway-log-messages.yml +++ b/gateway-service/src/main/resources/gateway-log-messages.yml @@ -370,6 +370,27 @@ messages: reason: "The JWT token or client certificate is not valid" action: "Configure your client to provide valid authentication." + - key: org.zowe.apiml.gateway.security.unexpectedMappingResponse + number: ZWEAG169 + type: ERROR + text: "Unexpected response from the external identity mapper. Status: %s body: %s" + reason: "The external identity mapper request failed with Internal Error" + action: "Verify that ZSS is responding." + + - key: org.zowe.apiml.gateway.security.InvalidMappingResponse + number: ZWEAG170 + type: ERROR + text: "Error occurred while trying to parse the response from the external identity mapper. Reason: %s" + reason: "The external identity mapper failed when trying to parse the response" + action: "Verify that the response is valid." + + - key: org.zowe.apiml.gateway.security.InvalidMapperUrl + number: ZWEAG171 + type: ERROR + text: "Configuration error. Failed to construct the external identity mapper URI. Reason: %s" + reason: "Failed to construct the external identity mapper URI" + action: "Verify that the external identity mapper URL specified in the configuration is valid." + # Revoke personal access token - key: org.zowe.apiml.security.query.invalidRevokeRequestBody number: ZWEAT607 diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/controllers/AuthControllerTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/controllers/AuthControllerTest.java index 43116385c1..e258505676 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/controllers/AuthControllerTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/controllers/AuthControllerTest.java @@ -32,13 +32,13 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.zowe.apiml.gateway.security.service.AuthenticationService; import org.zowe.apiml.gateway.security.service.JwtSecurity; +import org.zowe.apiml.gateway.security.service.token.OIDCTokenProvider; import org.zowe.apiml.gateway.security.service.zosmf.ZosmfService; import org.zowe.apiml.gateway.security.webfinger.WebFingerProvider; import org.zowe.apiml.gateway.security.webfinger.WebFingerResponse; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.yaml.YamlMessageService; import org.zowe.apiml.security.common.token.AccessTokenProvider; -import org.zowe.apiml.security.common.token.OIDCProvider; import org.zowe.apiml.security.common.token.TokenAuthentication; import java.io.IOException; @@ -48,12 +48,25 @@ import java.util.List; import java.util.Optional; -import static org.apache.http.HttpStatus.*; +import static org.apache.http.HttpStatus.SC_BAD_REQUEST; +import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; +import static org.apache.http.HttpStatus.SC_NOT_FOUND; +import static org.apache.http.HttpStatus.SC_NO_CONTENT; +import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; import static org.hamcrest.CoreMatchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) class AuthControllerTest { @@ -74,14 +87,14 @@ class AuthControllerTest { private AccessTokenProvider tokenProvider; @Mock - private OIDCProvider oidcProvider; + private OIDCTokenProvider oidcProvider; @Mock private WebFingerProvider webFingerProvider; private MessageService messageService; - private JWK jwk1, jwk2; + private JWK zosmfJwk, apimlJwk; private JSONObject body; @BeforeEach @@ -93,8 +106,8 @@ void setUp() throws ParseException, JSONException { .put("token", "token") .put("serviceId", "service"); - jwk1 = getJwk(1); - jwk2 = getJwk(2); + zosmfJwk = getJwk(1); + apimlJwk = getJwk(2); } @Test @@ -132,17 +145,43 @@ private JWK getJwk(int i) throws ParseException { private void initPublicKeys() { JWKSet zosmf = mock(JWKSet.class); when(zosmf.getKeys()).thenReturn( - Arrays.asList(jwk1) + Collections.singletonList(zosmfJwk) ); + when(zosmfService.getPublicKeys()).thenReturn(zosmf); - when(jwtSecurity.getPublicKeyInSet()).thenReturn(new JWKSet(Arrays.asList(jwk2))); - when(jwtSecurity.getJwkPublicKey()).thenReturn(Optional.of(jwk2)); + when(jwtSecurity.getPublicKeyInSet()).thenReturn(new JWKSet(Collections.singletonList(apimlJwk))); + when(jwtSecurity.getJwkPublicKey()).thenReturn(Optional.of(apimlJwk)); } @Test void testGetAllPublicKeys() throws Exception { initPublicKeys(); - JWKSet jwkSet = new JWKSet(Arrays.asList(jwk1, jwk2)); + when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.ZOSMF); + JWKSet jwkSet = new JWKSet(Arrays.asList(zosmfJwk, apimlJwk)); + this.mockMvc.perform(get("/gateway/auth/keys/public/all")) + .andExpect(status().is(SC_OK)) + .andExpect(content().json(jwkSet.toString())); + } + + @Test + void givenAPIMLJWTProducer_whenGetAllPublicKeys_thenReturnsOnlyAPIMLKeys() throws Exception { + initPublicKeys(); + when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.APIML); + JWKSet jwkSet = new JWKSet(Collections.singletonList(apimlJwk)); + this.mockMvc.perform(get("/gateway/auth/keys/public/all")) + .andExpect(status().is(SC_OK)) + .andExpect(content().json(jwkSet.toString())); + } + + @Test + void givenOIDCJWKSet_whenGetAllPublicKeys_thenIncludeOIDCInResult() throws Exception { + initPublicKeys(); + JWKSet mockedJwkSet = mock(JWKSet.class); + JWK oidcJwk = getJwk(3); + when(oidcProvider.getJwkSet()).thenReturn(mockedJwkSet); + when(mockedJwkSet.getKeys()).thenReturn(Collections.singletonList(oidcJwk)); + + JWKSet jwkSet = new JWKSet(Arrays.asList(apimlJwk, oidcJwk)); this.mockMvc.perform(get("/gateway/auth/keys/public/all")) .andExpect(status().is(SC_OK)) .andExpect(content().json(jwkSet.toString())); @@ -154,7 +193,7 @@ class WhenGettingActiveKey { void useZoweJwt() throws Exception { initPublicKeys(); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.APIML); - JWKSet jwkSet = new JWKSet(Collections.singletonList(jwk2)); + JWKSet jwkSet = new JWKSet(Collections.singletonList(apimlJwk)); mockMvc.perform(get("/gateway/auth/keys/public/current")) .andExpect(status().is(SC_OK)) .andExpect(content().json(jwkSet.toString())); @@ -173,7 +212,7 @@ void returnEmptyWhenUnknown() throws Exception { @Test void useZosmf() throws Exception { initPublicKeys(); - JWKSet jwkSet = new JWKSet(Collections.singletonList(jwk1)); + JWKSet jwkSet = new JWKSet(Collections.singletonList(zosmfJwk)); when(jwtSecurity.actualJwtProducer()).thenReturn(JwtSecurity.JwtProducer.ZOSMF); mockMvc.perform(get("/gateway/auth/keys/public/current")) .andExpect(status().is(SC_OK)) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/pre/ServiceAuthenticationFilterTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/pre/ServiceAuthenticationFilterTest.java index 7c952b870b..67fb2f7372 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/pre/ServiceAuthenticationFilterTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/pre/ServiceAuthenticationFilterTest.java @@ -37,6 +37,7 @@ import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.core.MessageType; import org.zowe.apiml.message.template.MessageTemplate; +import org.zowe.apiml.security.common.token.NoMainframeIdentityException; import org.zowe.apiml.security.common.token.TokenExpireException; import javax.servlet.http.HttpServletRequest; @@ -44,7 +45,11 @@ import java.util.Optional; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.*; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY; @@ -142,7 +147,7 @@ private AuthenticationCommand createValidationCommand(AuthSource authSource) { @ParameterizedTest @MethodSource("provideAuthSources") - void givenInvalidAuthSource_whenAuthSourceRequired_thenCallThrought(AuthSource authSource) { + void givenInvalidAuthSource_whenAuthSourceRequired_thenCallThrough(AuthSource authSource) { MessageTemplate messageTemplate = new MessageTemplate("key", "number", MessageType.ERROR, "text"); Message message = Message.of("requestedKey", messageTemplate, new Object[0]); doReturn(message).when(messageService).createMessage(anyString(), (Object) any()); @@ -195,7 +200,7 @@ public void increment(String name) { @ParameterizedTest @MethodSource("provideAuthSources") - void givenExpiredJwt_thenCallThrought(AuthSource authSource) { + void givenExpiredJwt_thenCallThrough(AuthSource authSource) { MessageTemplate messageTemplate = new MessageTemplate("key", "number", MessageType.ERROR, "text"); Message message = Message.of("requestedKey", messageTemplate, new Object[0]); doReturn(message).when(messageService).createMessage(anyString(), (Object) any()); @@ -210,6 +215,46 @@ void givenExpiredJwt_thenCallThrought(AuthSource authSource) { verify(cmd, never()).apply(any()); } + @Test + void givenNoMappedDistributedId_thenCallThrough() { + MessageTemplate messageTemplate = new MessageTemplate("key", "number", MessageType.ERROR, "text"); + Message message = Message.of("requestedKey", messageTemplate, new Object[0]); + doReturn(message).when(messageService).createMessage(anyString(), (Object) any()); + + RequestContext requestContext = mock(RequestContext.class); + when(requestContext.get(SERVICE_ID_KEY)).thenReturn("service"); + RequestContext.testSetCurrentContext(requestContext); + + AuthSource authSource = new JwtAuthSource("token"); + Authentication authentication = new Authentication(AuthenticationScheme.ZOSMF, ""); + doReturn(authentication).when(serviceAuthenticationService).getAuthentication("service"); + doReturn(Optional.of(authSource)).when(serviceAuthenticationService).getAuthSourceByAuthentication(authentication); + doThrow(new NoMainframeIdentityException("User not found.")) + .when(serviceAuthenticationService) + .getAuthenticationCommand(eq("service"), any(Authentication.class), any(AuthSource.class)); + + serviceAuthenticationFilter.run(); + + verify(RequestContext.getCurrentContext(), never()).setSendZuulResponse(false); + verify(RequestContext.getCurrentContext(), never()).setResponseStatusCode(401); + } + + @Test + void givenNoMappedDistributedIdAndInvalidToken_thenCallThrough() { + MessageTemplate messageTemplate = new MessageTemplate("key", "number", MessageType.ERROR, "text"); + Message message = Message.of("requestedKey", messageTemplate, new Object[0]); + doReturn(message).when(messageService).createMessage(anyString(), (Object) any()); + RequestContext.testSetCurrentContext(new RequestContext()); + doThrow(new NoMainframeIdentityException("User not found.", null, false)) + .when(serviceAuthenticationService) + .getAuthentication((String) null); + + assertNull(serviceAuthenticationFilter.run()); + + assertNotNull(RequestContext.getCurrentContext().getZuulRequestHeaders().get(ApimlConstants.AUTH_FAIL_HEADER.toLowerCase())); + assertEquals(ApimlConstants.AUTH_FAIL_HEADER, RequestContext.getCurrentContext().getZuulResponseHeaders().get(0).first()); + } + @ParameterizedTest @MethodSource("provideAuthSources") void givenInvalidAuthSource_whenAuthenticationException_thenReject(AuthSource authSource) { diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/OidcCommandTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/OidcCommandTest.java new file mode 100644 index 0000000000..724c613a8d --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/OidcCommandTest.java @@ -0,0 +1,61 @@ +/* + * 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.gateway.security.service.schema; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.zuul.context.RequestContext; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.web.MockHttpServletRequest; +import org.zowe.apiml.constants.ApimlConstants; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper.IGNORED_HEADERS; + +class OidcCommandTest { + + private static final String tokenValue = "tokenValue"; + + @Test + void givenTokenExistsAndMultipleCookies_thenSetAsHeaderAndRemoveAuthorization() { + RequestContext mockContext = new RequestContext(); + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + RequestContext.testSetCurrentContext(mockContext); + mockContext.setRequest(mockRequest); + + mockRequest.addHeader(HttpHeaders.COOKIE, "cookieName=cookieValue; apimlAuthenticationToken=xyz"); + new OidcCommand(tokenValue).apply(mock(InstanceInfo.class)); + + assertEquals("cookieName=cookieValue", mockContext.getZuulRequestHeaders().get(HttpHeaders.COOKIE.toLowerCase())); + assertEquals(tokenValue, mockContext.getZuulRequestHeaders().get(ApimlConstants.HEADER_OIDC_TOKEN.toLowerCase())); + assertTrue(((Set) mockContext.get(IGNORED_HEADERS)).contains(HttpHeaders.AUTHORIZATION.toLowerCase())); // NOSONAR + } + + @Test + void givenTokenExists_thenSetAsHeaderAndRemoveAuthorization() { + RequestContext mockContext = new RequestContext(); + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + RequestContext.testSetCurrentContext(mockContext); + mockContext.setRequest(mockRequest); + + mockRequest.addHeader(HttpHeaders.COOKIE, "apimlAuthenticationToken=xyz"); + new OidcCommand(tokenValue).apply(mock(InstanceInfo.class)); + + assertEquals(tokenValue, mockContext.getZuulRequestHeaders().get(ApimlConstants.HEADER_OIDC_TOKEN.toLowerCase())); + assertTrue(((Set) mockContext.get(IGNORED_HEADERS)).contains(HttpHeaders.AUTHORIZATION.toLowerCase())); // NOSONAR + assertTrue(((Set) mockContext.get(IGNORED_HEADERS)).contains(HttpHeaders.COOKIE.toLowerCase())); // NOSONAR + } + +} diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/source/OIDCAuthSourceServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/source/OIDCAuthSourceServiceTest.java index 08fa436edb..3a33060553 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/source/OIDCAuthSourceServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/source/OIDCAuthSourceServiceTest.java @@ -21,10 +21,7 @@ import org.zowe.apiml.gateway.security.mapping.AuthenticationMapper; import org.zowe.apiml.gateway.security.service.AuthenticationService; import org.zowe.apiml.gateway.security.service.TokenCreationService; -import org.zowe.apiml.security.common.token.OIDCProvider; -import org.zowe.apiml.security.common.token.QueryResponse; -import org.zowe.apiml.security.common.token.TokenExpireException; -import org.zowe.apiml.security.common.token.TokenNotValidException; +import org.zowe.apiml.security.common.token.*; import javax.servlet.http.HttpServletRequest; import java.util.Collections; @@ -115,7 +112,7 @@ void givenNoMapping_whenParse_thenThrowException() { OIDCAuthSource authSource = mockValidAuthSource(); when(mapper.mapToMainframeUserId(authSource)).thenReturn(null); - assertThrows(TokenNotValidException.class, () -> { + assertThrows(NoMainframeIdentityException.class, () -> { service.parse(authSource); }); } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/token/OIDCTokenProviderTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/token/OIDCTokenProviderTest.java index a8a015da72..3f1f57b611 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/token/OIDCTokenProviderTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/token/OIDCTokenProviderTest.java @@ -10,15 +10,11 @@ package org.zowe.apiml.gateway.security.service.token; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.Requirement; +import com.nimbusds.jose.jwk.*; import io.jsonwebtoken.impl.DefaultClock; import io.jsonwebtoken.impl.FixedClock; -import org.apache.commons.io.IOUtils; -import org.apache.http.HttpStatus; -import org.apache.http.StatusLine; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.entity.BasicHttpEntity; -import org.apache.http.impl.client.CloseableHttpClient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -26,66 +22,40 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EmptySource; import org.junit.jupiter.params.provider.NullSource; -import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import org.zowe.apiml.gateway.cache.CachingServiceClientException; import java.io.IOException; -import java.nio.charset.StandardCharsets; +import java.net.URL; import java.security.Key; +import java.text.ParseException; import java.time.Instant; -import java.util.Date; -import java.util.Map; +import java.util.*; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class OIDCTokenProviderTest { private static final String OKTA_JWKS_RESOURCE = "/test_samples/okta_jwks.json"; - private static final String JWKS_KEYS_BODY_INVALID = "\n" - + "{\n" - + " \"keys\": [\n" - + " {\n" - + " \"kty\": \"RSA\",\n" - + " \"alg\": \"RS256\",\n" - + " \"kid\": \"Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4\",\n" - + " \"use\": \"sig\",\n" - + " \"e\": \"AQAB\",\n" - + " \"n\": \"invalid\"\n" - + " }\n" - + " ]\n" - + "}"; private static final String EXPIRED_TOKEN = "eyJraWQiOiJMY3hja2tvcjk0cWtydW54SFA3VGtpYjU0N3J6bWtYdnNZVi1uYzZVLU40IiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULlExakp2UkZ0dUhFUFpGTXNmM3A0enQ5aHBRRHZrSU1CQ3RneU9IcTdlaEkiLCJpc3MiOiJodHRwczovL2Rldi05NTcyNzY4Ni5va3RhLmNvbS9vYXV0aDIvZGVmYXVsdCIsImF1ZCI6ImFwaTovL2RlZmF1bHQiLCJpYXQiOjE2OTcwNjA3NzMsImV4cCI6MTY5NzA2NDM3MywiY2lkIjoiMG9hNmE0OG1uaVhBcUVNcng1ZDciLCJ1aWQiOiIwMHU5OTExOGgxNmtQT1dBbTVkNyIsInNjcCI6WyJvcGVuaWQiXSwiYXV0aF90aW1lIjoxNjk3MDYwMDY0LCJzdWIiOiJzajg5NTA5MkBicm9hZGNvbS5uZXQiLCJncm91cHMiOlsiRXZlcnlvbmUiXX0.Cuf1JVq_NnfBxaCwiLsR5O6DBmVV1fj9utAfKWIF1hlek2hCJsDLQM4ii_ucQ0MM1V3nVE1ZatPB-W7ImWPlGz7NeNBv7jEV9DkX70hchCjPHyYpaUhAieTG75obdufiFpI55bz3qH5cPRvsKv0OKKI9T8D7GjEWsOhv6CevJJZZvgCFLGFfnacKLOY5fEBN82bdmCulNfPVrXF23rOregFjOBJ1cKWfjmB0UGWgI8VBGGemMNm3ACX3OYpTOek2PBfoCIZWOSGnLZumFTYA0F_3DsWYhIJNoFv16_EBBJcp_C0BYE_fiuXzeB0fieNUXASsKp591XJMflDQS_Zt1g"; private static final String TOKEN = "token"; private OIDCTokenProvider oidcTokenProvider; - @Mock - private CloseableHttpClient httpClient; - @Mock - private CloseableHttpResponse response; - private StatusLine responseStatusLine; - private BasicHttpEntity responseEntity; + private JWKSet jwkSet; @BeforeEach - void setup() throws CachingServiceClientException, IOException { - responseStatusLine = mock(StatusLine.class); - responseEntity = new BasicHttpEntity(); - responseEntity.setContent(IOUtils.toInputStream("", StandardCharsets.UTF_8)); - oidcTokenProvider = new OIDCTokenProvider(httpClient, new DefaultClock(), new ObjectMapper()); + void setup() throws CachingServiceClientException { + oidcTokenProvider = new OIDCTokenProvider(new DefaultClock()); ReflectionTestUtils.setField(oidcTokenProvider, "jwkRefreshInterval", 1); ReflectionTestUtils.setField(oidcTokenProvider, "jwksUri", "https://jwksurl"); oidcTokenProvider.clientId = "client_id"; @@ -95,60 +65,48 @@ void setup() throws CachingServiceClientException, IOException { @Nested class GivenInitializationWithJwks { - @BeforeEach - void setup() { - - responseEntity.setContent(getClass().getResourceAsStream(OKTA_JWKS_RESOURCE)); - } - @Test @SuppressWarnings("unchecked") - void initialized_thenJwksFullfilled() throws IOException { - Map jwks = (Map) ReflectionTestUtils.getField(oidcTokenProvider, "jwks"); - when(responseStatusLine.getStatusCode()).thenReturn(HttpStatus.SC_OK); - when(response.getStatusLine()).thenReturn(responseStatusLine); - when(response.getEntity()).thenReturn(responseEntity); - when(httpClient.execute(any())).thenReturn(response); - oidcTokenProvider.afterPropertiesSet(); - assertFalse(jwks.isEmpty()); - assertTrue(jwks.containsKey("Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4")); - assertTrue(jwks.containsKey("-716sp3XBB_v30lGj2mu5MdXkdh8poa9zJQlAwC46n4")); - assertNotNull(jwks.get("Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4")); - assertInstanceOf(Key.class, jwks.get("Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4")); + void initialized_thenJwksFullfilled() throws ParseException, IOException { + jwkSet = JWKSet.load(getClass().getResourceAsStream(OKTA_JWKS_RESOURCE)); + try (MockedStatic mockedStatic = Mockito.mockStatic(JWKSet.class)) { + mockedStatic.when(() -> JWKSet.load(any(URL.class))).thenReturn(jwkSet); + oidcTokenProvider.afterPropertiesSet(); + } + Map publicKeys = oidcTokenProvider.getPublicKeys(); + + assertFalse(publicKeys.isEmpty()); + assertTrue(publicKeys.containsKey("Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4")); + assertTrue(publicKeys.containsKey("-716sp3XBB_v30lGj2mu5MdXkdh8poa9zJQlAwC46n4")); + assertNotNull(publicKeys.get("Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4")); + assertInstanceOf(Key.class, publicKeys.get("Lcxckkor94qkrunxHP7Tkib547rzmkXvsYV-nc6U-N4")); } @Test - @SuppressWarnings("unchecked") - void whenRequestFails_thenNotInitialized() throws IOException { - Map jwks = (Map) ReflectionTestUtils.getField(oidcTokenProvider, "jwks"); - when(responseStatusLine.getStatusCode()).thenReturn(HttpStatus.SC_INTERNAL_SERVER_ERROR); - when(response.getStatusLine()).thenReturn(responseStatusLine); - when(response.getEntity()).thenReturn(responseEntity); - when(httpClient.execute(any())).thenReturn(response); + void whenRequestFails_thenNotInitialized() { + try (MockedStatic mockedStatic = Mockito.mockStatic(JWKSet.class)) { + mockedStatic.when(() -> JWKSet.load(any(URL.class))).thenThrow(IOException.class); + oidcTokenProvider.afterPropertiesSet(); + } oidcTokenProvider.afterPropertiesSet(); - assertTrue(jwks.isEmpty()); + assertTrue(oidcTokenProvider.getPublicKeys().isEmpty()); } @Test - @SuppressWarnings("unchecked") void whenUriNotProvided_thenNotInitialized() { ReflectionTestUtils.setField(oidcTokenProvider, "jwksUri", ""); - Map jwks = (Map) ReflectionTestUtils.getField(oidcTokenProvider, "jwks"); oidcTokenProvider.afterPropertiesSet(); - assertTrue(jwks.isEmpty()); + assertTrue(oidcTokenProvider.getPublicKeys().isEmpty()); } @Test - @SuppressWarnings("unchecked") - void whenInvalidKey_thenNotInitialized() throws IOException { - responseEntity.setContent(IOUtils.toInputStream(JWKS_KEYS_BODY_INVALID, StandardCharsets.UTF_8)); - Map jwks = (Map) ReflectionTestUtils.getField(oidcTokenProvider, "jwks"); - when(responseStatusLine.getStatusCode()).thenReturn(HttpStatus.SC_OK); - when(response.getStatusLine()).thenReturn(responseStatusLine); - when(response.getEntity()).thenReturn(responseEntity); - when(httpClient.execute(any())).thenReturn(response); + void whenInvalidKeyResponse_thenNotInitialized() { + try (MockedStatic mockedStatic = Mockito.mockStatic(JWKSet.class)) { + mockedStatic.when(() -> JWKSet.load(any(URL.class))).thenThrow(ParseException.class); + oidcTokenProvider.afterPropertiesSet(); + } oidcTokenProvider.afterPropertiesSet(); - assertTrue(jwks.isEmpty()); + assertTrue(oidcTokenProvider.getPublicKeys().isEmpty()); } } @@ -156,41 +114,37 @@ void whenInvalidKey_thenNotInitialized() throws IOException { class GivenTokenForValidation { @SuppressWarnings("unchecked") - private void initJwks() throws IOException { - Map jwks = (Map) ReflectionTestUtils.getField(oidcTokenProvider, "jwks"); - responseEntity.setContent(getClass().getResourceAsStream(OKTA_JWKS_RESOURCE)); - when(responseStatusLine.getStatusCode()).thenReturn(HttpStatus.SC_OK); - when(response.getStatusLine()).thenReturn(responseStatusLine); - when(response.getEntity()).thenReturn(responseEntity); - when(httpClient.execute(any())).thenReturn(response); - oidcTokenProvider.afterPropertiesSet(); - assertFalse(jwks.isEmpty()); + private void initPublicKeys() throws IOException, ParseException { + jwkSet = JWKSet.load(getClass().getResourceAsStream(OKTA_JWKS_RESOURCE)); + try (MockedStatic mockedStatic = Mockito.mockStatic(JWKSet.class)) { + mockedStatic.when(() -> JWKSet.load(any(URL.class))).thenReturn(jwkSet); + oidcTokenProvider.afterPropertiesSet(); + } + assertFalse(oidcTokenProvider.getPublicKeys().isEmpty()); } @Test - void whenValidTokenExpired_thenReturnInvalid() throws IOException { - initJwks(); + void whenValidTokenExpired_thenReturnInvalid() throws IOException, ParseException { + initPublicKeys(); assertFalse(oidcTokenProvider.isValid(EXPIRED_TOKEN)); } @Test - void whenValidtoken_thenReturnValid() throws IOException { - initJwks(); + void whenValidToken_thenReturnValid() throws IOException, ParseException { + initPublicKeys(); ReflectionTestUtils.setField(oidcTokenProvider, "clock", new FixedClock(new Date(Instant.ofEpochSecond(1697060773 + 1000L).toEpochMilli()))); assertTrue(oidcTokenProvider.isValid(EXPIRED_TOKEN)); } @Test - void whenInvalidToken_thenReturnInvalid() throws IOException { - initJwks(); + void whenInvalidToken_thenReturnInvalid() throws IOException, ParseException { + initPublicKeys(); assertFalse(oidcTokenProvider.isValid(TOKEN)); } @Test - @SuppressWarnings("unchecked") - void whenNoJwks_thenReturnInvalid() { - Map jwks = (Map) ReflectionTestUtils.getField(oidcTokenProvider, "jwks"); - assumeTrue(jwks.isEmpty()); + void whenNoJwk_thenReturnInvalid() { + assumeTrue(oidcTokenProvider.getPublicKeys().isEmpty()); assertFalse(oidcTokenProvider.isValid(TOKEN)); } @@ -231,24 +185,87 @@ void whenInvalidClientSecret_thenReturnInvalid(String secret) { @Nested class JwksUriLoad { - @Mock - private CloseableHttpClient httpClientMock; - @Mock - CloseableHttpResponse httpResponse; + @BeforeEach public void setUp() { - oidcTokenProvider = new OIDCTokenProvider(httpClientMock, new DefaultClock(), new ObjectMapper()); + oidcTokenProvider = new OIDCTokenProvider(new DefaultClock()); ReflectionTestUtils.setField(oidcTokenProvider, "jwksUri", "https://jwksurl"); } @Test - void shouldNotModifyJwksUri() throws IOException { - when(httpClientMock.execute(any())).thenReturn(httpResponse); + void shouldNotModifyJwksUri() { + + try (MockedStatic mockedStatic = Mockito.mockStatic(JWKSet.class)) { + mockedStatic.when(() -> JWKSet.load(any(URL.class))).thenReturn(new JWKSet()); + + oidcTokenProvider.fetchJWKSet(); + mockedStatic.verify(() -> JWKSet.load(new URL("https://jwksurl")), times(1)); + } + } + + @Test + void shouldHandleNullPointer_whenJWKKeyNull() { + + JWKSet mockedJwtSet = mock(JWKSet.class); + List mockedKeys = new ArrayList<>(); + JWK mockedJwk = mock(JWK.class); + when(mockedJwk.getKeyUse()).thenReturn(null); + RSAKey rsaKey = mock(RSAKey.class); + mockedKeys.add(mockedJwk); + when(mockedJwtSet.getKeys()).thenReturn(mockedKeys); + + try (MockedStatic mockedStatic = Mockito.mockStatic(JWKSet.class)) { + mockedStatic.when(() -> JWKSet.load(any(URL.class))).thenReturn(mockedJwtSet); + + oidcTokenProvider.fetchJWKSet(); + + verify(rsaKey, never()).toRSAPublicKey(); + } catch (JOSEException e) { + fail("Exception thrown: " + e.getMessage()); + } + } + + @Test + void shouldHandleNullPointer_whenJWKTypeNull() { + + JWKSet mockedJwtSet = mock(JWKSet.class); + List mockedKeys = new ArrayList<>(); + JWK mockedJwk = mock(JWK.class); + when(mockedJwk.getKeyType()).thenReturn(null); + RSAKey rsaKey = mock(RSAKey.class); + mockedKeys.add(mockedJwk); + when(mockedJwtSet.getKeys()).thenReturn(mockedKeys); + + try (MockedStatic mockedStatic = Mockito.mockStatic(JWKSet.class)) { + mockedStatic.when(() -> JWKSet.load(any(URL.class))).thenReturn(mockedJwtSet); - oidcTokenProvider.fetchJwksUrls(); + oidcTokenProvider.fetchJWKSet(); - verify(httpClientMock).execute(argThat((getJwksRequest) -> getJwksRequest.getURI().toString().equals("https://jwksurl"))); + verify(rsaKey, never()).toRSAPublicKey(); + } catch (JOSEException e) { + fail("Exception thrown: " + e.getMessage()); + } + } + + @Test + void throwsCorrectException() throws JOSEException { + + try (MockedStatic mockedStatic = Mockito.mockStatic(JWKSet.class)) { + JWKSet jwkSet1 = mock(JWKSet.class); + JWK invalidJwk = mock(JWK.class); + when(invalidJwk.getKeyUse()).thenReturn(new KeyUse("sig")); + when(invalidJwk.getKeyType()).thenReturn(new KeyType("RSA", Requirement.REQUIRED)); + when(invalidJwk.getKeyID()).thenReturn("123"); + RSAKey rsaKey = mock(RSAKey.class); + when(invalidJwk.toRSAKey()).thenReturn(rsaKey); + when(rsaKey.toRSAPublicKey()).thenThrow(new JOSEException()); + List keys = Collections.singletonList(invalidJwk); + when(jwkSet1.getKeys()).thenReturn(keys); + mockedStatic.when(() -> JWKSet.load(any(URL.class))).thenReturn(jwkSet1); + + assertDoesNotThrow(() -> oidcTokenProvider.fetchJWKSet()); + } } } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceTest.java index 40af4c8fb6..a347a10b52 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceTest.java @@ -17,6 +17,7 @@ import ch.qos.logback.core.Appender; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.discovery.DiscoveryClient; +import com.nimbusds.jose.jwk.JWKSet; import org.hamcrest.collection.IsMapContaining; import org.json.JSONException; import org.json.JSONObject; @@ -28,17 +29,28 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.skyscreamer.jsonassert.JSONAssert; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; -import org.springframework.http.*; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.web.client.*; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; import org.zowe.apiml.gateway.security.service.AuthenticationService; import org.zowe.apiml.gateway.security.service.TokenCreationService; import org.zowe.apiml.gateway.security.service.schema.source.AuthSource; @@ -54,14 +66,40 @@ import javax.management.ServiceNotFoundException; import javax.net.ssl.SSLHandshakeException; import java.net.ConnectException; +import java.net.URL; import java.nio.charset.Charset; -import java.util.*; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; import static org.zowe.apiml.gateway.security.service.zosmf.ZosmfService.TokenType.JWT; import static org.zowe.apiml.gateway.security.service.zosmf.ZosmfService.TokenType.LTPA; @@ -358,7 +396,7 @@ void thenChangePasswordWithZosmfValidationError() { HttpMethod.PUT, new HttpEntity<>(new ChangePasswordRequest(loginRequest), requiredHeaders), String.class)) - .thenThrow(new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR, "Internal error", "{\"returnCode\": 8}".getBytes(), Charset.defaultCharset())); + .thenThrow(new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR, "Internal error", "{\"returnCode\": 8}".getBytes(), Charset.defaultCharset())); assertThrows(BadCredentialsException.class, () -> zosmfService.changePassword(authentication)); } @@ -693,16 +731,15 @@ class WhenReadTokenFromCookie { "}"; @Test - void thenSuccess() throws JSONException { + void thenSuccess() throws JSONException, ParseException { String zosmfJwtUrl = "/jwt/ibm/api/zOSMFBuilder/jwk"; when(authConfigurationProperties.getZosmf().getJwtEndpoint()).thenReturn(zosmfJwtUrl); ZosmfService zosmfService = getZosmfServiceSpy(); - when(restTemplate.getForObject( - "http://zosmf:1433" + zosmfJwtUrl, - String.class - )).thenReturn(ZOSMF_PUBLIC_KEY_JSON); - - JSONAssert.assertEquals(ZOSMF_PUBLIC_KEY_JSON, new JSONObject(zosmfService.getPublicKeys().toString()), true); + JWKSet jwkSet = JWKSet.parse(ZOSMF_PUBLIC_KEY_JSON); + try (MockedStatic mockedStatic = Mockito.mockStatic(JWKSet.class)) { + mockedStatic.when(() -> JWKSet.load(any(URL.class))).thenReturn(jwkSet); + JSONAssert.assertEquals(ZOSMF_PUBLIC_KEY_JSON, new JSONObject(zosmfService.getPublicKeys().toString()), true); + } } @Test @@ -721,20 +758,10 @@ void thenReturnNull() { @Nested class WhenGetsPublicKeys { - @Test - void thenThrowException() { - ZosmfService zosmfService = getZosmfServiceSpy(); - when(restTemplate.getForObject(anyString(), any())) - .thenThrow(HttpClientErrorException.create(HttpStatus.NOT_FOUND, - "", new HttpHeaders(), new byte[]{}, null)); - assertTrue(zosmfService.getPublicKeys().getKeys().isEmpty()); - } @Test - void thenReturnInvalidFormat() { + void givenExceptionInTheResponse_thenPublicKeysAreEmpty() { ZosmfService zosmfService = getZosmfServiceSpy(); - when(restTemplate.getForObject(anyString(), any())) - .thenReturn("invalidFormat"); assertTrue(zosmfService.getPublicKeys().getKeys().isEmpty()); } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/zaas/ZaasControllerTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/zaas/ZaasControllerTest.java index 882df9f76a..c8f52d6532 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/zaas/ZaasControllerTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/zaas/ZaasControllerTest.java @@ -21,14 +21,21 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.gateway.security.service.TokenCreationService; import org.zowe.apiml.gateway.security.service.saf.SafIdtException; -import org.zowe.apiml.gateway.security.service.schema.source.*; +import org.zowe.apiml.gateway.security.service.schema.source.AuthSchemeException; +import org.zowe.apiml.gateway.security.service.schema.source.AuthSource; +import org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService; +import org.zowe.apiml.gateway.security.service.schema.source.JwtAuthSource; +import org.zowe.apiml.gateway.security.service.schema.source.OIDCAuthSource; +import org.zowe.apiml.gateway.security.service.schema.source.ParsedTokenAuthSource; import org.zowe.apiml.gateway.security.service.zosmf.ZosmfService; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.yaml.YamlMessageService; import org.zowe.apiml.passticket.IRRPassTicketGenerationException; import org.zowe.apiml.passticket.PassTicketService; +import org.zowe.apiml.security.common.token.NoMainframeIdentityException; import org.zowe.apiml.security.common.token.TokenExpireException; import org.zowe.apiml.security.common.token.TokenNotValidException; import org.zowe.apiml.zaas.ZaasTokenResponse; @@ -36,7 +43,11 @@ import javax.management.ServiceNotFoundException; import java.util.Date; -import static org.apache.http.HttpStatus.*; +import static org.apache.http.HttpStatus.SC_BAD_REQUEST; +import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; +import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.hasSize; @@ -101,7 +112,7 @@ void setUp() { @Test void whenRequestZosmfToken_thenResponseOK() throws Exception { when(zosmfService.exchangeAuthenticationForZosmfToken(JWT_TOKEN, authParsedSource)) - .thenReturn(new ZaasTokenResponse(ZosmfService.TokenType.JWT.getCookieName(), JWT_TOKEN)); + .thenReturn(ZaasTokenResponse.builder().cookieName(ZosmfService.TokenType.JWT.getCookieName()).token(JWT_TOKEN).build()); mockMvc.perform(post(ZOSMF_TOKEN_URL) .requestAttr(AUTH_SOURCE_ATTR, authSource) @@ -234,6 +245,31 @@ void whenRequestingZoweTokensAndTokenExpireException_thenUnauthorized() throws E .andExpect(jsonPath("$.messages[0].messageContent", is("The token has expired"))); } + @Test + void whenRequestingZoweTokensAndUserMissingMapping_thenOkWithTokenInHeader() throws Exception { + authSource = new OIDCAuthSource(JWT_TOKEN); + when(authSourceService.getJWT(authSource)) + .thenThrow(new NoMainframeIdentityException("No user mappring", null, true)); + + mockMvc.perform(post(ZOWE_TOKEN_URL) + .requestAttr(AUTH_SOURCE_ATTR, authSource)) + .andExpect(status().is(SC_OK)) + .andExpect(jsonPath("$.token", is(JWT_TOKEN))) + .andExpect(jsonPath("$.headerName", is(ApimlConstants.HEADER_OIDC_TOKEN))); + + } + + @Test + void whenRequestingZoweTokensAndUserMissingMappingAndTokenIsInvalid_thenUnauthorized() throws Exception { + authSource = new OIDCAuthSource(JWT_TOKEN); + when(authSourceService.getJWT(authSource)) + .thenThrow(new NoMainframeIdentityException("No user mappring", null, false)); + + mockMvc.perform(post(ZOWE_TOKEN_URL) + .requestAttr(AUTH_SOURCE_ATTR, authSource)) + .andExpect(status().is(SC_UNAUTHORIZED)); + } + @Test void whenRequestingZoweTokensAndAuthSchemeException_thenUnauthorized() throws Exception { when(authSourceService.getJWT(authSource)) diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index 3aa1b4cbd0..35ee0068e3 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -31,6 +31,7 @@ dependencies { testImplementation libs.spring.boot.starter.test testImplementation libs.spring.boot.starter.websocket testImplementation libs.spring.webflux + testImplementation libs.jjwt.impl testImplementation libs.bcpkix; testImplementation libs.jackson.dataformat.yaml diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/oauth2/OktaOauth2Test.java b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/oauth2/OktaOauth2Test.java index 349c0b03c0..fdbc2d9cd0 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/oauth2/OktaOauth2Test.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/authentication/oauth2/OktaOauth2Test.java @@ -10,6 +10,13 @@ package org.zowe.apiml.integration.authentication.oauth2; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.JWKSet; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.LocatorAdapter; +import io.jsonwebtoken.ProtectedHeader; +import io.jsonwebtoken.impl.DefaultClock; import io.restassured.RestAssured; import io.restassured.http.ContentType; import org.apache.http.HttpHeaders; @@ -29,19 +36,32 @@ import org.zowe.apiml.util.http.HttpRequestUtils; import org.zowe.apiml.util.requests.Endpoints; +import javax.net.ssl.HttpsURLConnection; +import java.io.IOException; import java.net.URI; +import java.net.URL; +import java.security.Key; +import java.text.ParseException; import java.util.stream.Stream; import static io.restassured.RestAssured.given; -import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.Matchers.hasKey; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.zowe.apiml.util.SecurityUtils.GATEWAY_TOKEN_COOKIE_NAME; -import static org.zowe.apiml.util.requests.Endpoints.*; +import static org.zowe.apiml.util.requests.Endpoints.JWK_ALL; +import static org.zowe.apiml.util.requests.Endpoints.REQUEST_INFO_ENDPOINT; +import static org.zowe.apiml.util.requests.Endpoints.SAF_IDT_REQUEST; +import static org.zowe.apiml.util.requests.Endpoints.ZOSMF_REQUEST; +import static org.zowe.apiml.util.requests.Endpoints.ZOWE_JWT_REQUEST; @Tag("OktaOauth2Test") public class OktaOauth2Test { public static final URI VALIDATE_ENDPOINT = HttpRequestUtils.getUriFromGateway(Endpoints.VALIDATE_OIDC_TOKEN); + public static final URI JWK_ENDPOINT = HttpRequestUtils.getUriFromGateway(JWK_ALL); private static final String VALID_TOKEN_WITH_MAPPING = SecurityUtils.validOktaAccessToken(true); private static final String VALID_TOKEN_NO_MAPPING = SecurityUtils.validOktaAccessToken(false); private static final String EXPIRED_TOKEN = SecurityUtils.expiredOktaAccessToken(); @@ -67,6 +87,30 @@ static void init() { @Nested class GivenValidOktaToken { + + @ParameterizedTest + @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OktaOauth2Test#validTokens") + void thenValidateUsingJWKLocally(String token) throws ParseException, IOException, JOSEException { + HttpsURLConnection.setDefaultSSLSocketFactory(SecurityUtils.getSslContext().getSocketFactory()); + JWKSet jwkSet = JWKSet.load(new URL(JWK_ENDPOINT.toString())); + Claims claims = Jwts.parser() + .keyLocator(new LocatorAdapter() { + @Override + protected Key locate(ProtectedHeader header) { + try { + return jwkSet.getKeyByKeyId(header.getKeyId()).toRSAKey().toPublicKey(); + } catch (JOSEException e) { + throw new RuntimeException(e); + } + } + }) + .clock(new DefaultClock()) + .build() + .parseSignedClaims(token) + .getPayload(); + assertNotNull(claims); + } + @ParameterizedTest @MethodSource("org.zowe.apiml.integration.authentication.oauth2.OktaOauth2Test#validTokens") void thenValidateReturns200(String validToken) { @@ -109,6 +153,8 @@ void whenUserHasNoMapping_thenZoweAuthFailure() { .get(DC_url) .then().statusCode(200) .body("headers", hasKey("x-zowe-auth-failure")) + .body("headers", not(hasKey("authorization"))) + .body("headers", hasKey(ApimlConstants.HEADER_OIDC_TOKEN.toLowerCase())) .body("headers", not(hasKey("cookie"))); } @@ -138,8 +184,8 @@ void whenUserHasNoMapping_thenZoweAuthFailure() { .get(DC_url) .then().statusCode(200) .body("headers", hasKey("x-zowe-auth-failure")) - .body("cookies", hasKey("apimlAuthenticationToken")) - .body("cookies.apimlAuthenticationToken", is(VALID_TOKEN_NO_MAPPING)); + .body("cookies", not(hasKey("apimlAuthenticationToken"))) + .body("cookies.apimlAuthenticationToken", is((String) null)); } } } @@ -150,10 +196,10 @@ class WhenTestingZosmfScheme { @Test void whenUserHasMapping_thenJwtTokenCreated() { - given() + given() .contentType(ContentType.JSON) - .header(HttpHeaders.AUTHORIZATION, - ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + VALID_TOKEN_WITH_MAPPING) + .header(HttpHeaders.AUTHORIZATION, + ApimlConstants.BEARER_AUTHENTICATION_PREFIX + " " + VALID_TOKEN_WITH_MAPPING) .when() .get(DC_url) .then().statusCode(200) @@ -172,6 +218,8 @@ void whenUserHasNoMapping_thenZoweAuthFailure() { .get(DC_url) .then().statusCode(200) .body("headers", hasKey("x-zowe-auth-failure")) + .body("headers", not(hasKey("authorization"))) + .body("headers", hasKey(ApimlConstants.HEADER_OIDC_TOKEN.toLowerCase())) .body("headers", not(hasKey("cookie"))); } @@ -200,7 +248,7 @@ void whenUserHasNoMapping_thenZoweAuthFailure() { .get(DC_url) .then().statusCode(200) .body("headers", hasKey("x-zowe-auth-failure")) - .body("headers", hasKey("cookie")) + .body("headers", not(hasKey("cookie"))) .body("cookies", not(hasKey("jwtToken"))); } } @@ -233,6 +281,8 @@ void whenUserHasNoMapping_thenZoweAuthFailure() { .get(DC_url) .then().statusCode(200) .body("headers", hasKey("x-zowe-auth-failure")) + .body("headers", not(hasKey("authorization"))) + .body("headers", hasKey(ApimlConstants.HEADER_OIDC_TOKEN.toLowerCase())) .body("headers", not(hasKey("x-saf-token"))); } @@ -293,8 +343,8 @@ void whenUserHasNoMapping_thenZoweAuthFailure() { .get(DC_url) .then().statusCode(200) .body("headers", hasKey("x-zowe-auth-failure")) - .body("headers", hasKey("authorization")) - .body("headers.authorization", not(startsWith("Basic"))); + .body("headers", not(hasKey("authorization"))) + .body("headers", hasKey(ApimlConstants.HEADER_OIDC_TOKEN.toLowerCase())); } @Nested @@ -545,6 +595,7 @@ void testZssReturns401() { .body("headers", hasKey("x-zowe-auth-failure")) .body("headers", not(hasKey("cookie"))); } + @Test void testZssReturns404() { setZssResponse(404, ZssResponse.ZssError.MAPPING_OTHER); diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java b/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java index 17e5c76a82..533295a521 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java @@ -431,7 +431,7 @@ public static String validOktaAccessToken(boolean userHasMappingDefined) { .config(RestAssured.config().httpClient(HttpClientConfig.httpClientConfig().setParam("http.connection.timeout", 30 * 1000))) .queryParams(queryParams) .when() - .get(OKTA_HOSTNAME + "/oauth2/default/v1/authorize") + .get(OKTA_HOSTNAME + "/oauth2/v1/authorize") .then() .statusCode(200) .extract().response(); diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/requests/Endpoints.java b/integration-tests/src/test/java/org/zowe/apiml/util/requests/Endpoints.java index f1431ea999..744a8f3eaf 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/requests/Endpoints.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/requests/Endpoints.java @@ -48,6 +48,7 @@ public class Endpoints { public final static String EVICT_ACCESS_TOKEN = "/gateway/auth/access-token/evict"; public final static String VALIDATE_OIDC_TOKEN = "/gateway/api/v1/auth/oidc-token/validate"; + public final static String JWK_ALL = "/gateway/api/v1/auth/keys/public/all"; public final static String DISCOVERY_STATIC_API = "/discovery/api/v1/staticApi"; diff --git a/keystore/localhost/localhost.truststore.p12 b/keystore/localhost/localhost.truststore.p12 index a07ab622235344b64984630fa2e26b9d75b9cff2..3b235e3b0929a91428c71e535c849ca8f0a8e3b5 100644 GIT binary patch literal 12314 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 A^Z)<= literal 10850 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 diff --git a/mock-services/src/main/java/org/zowe/apiml/client/services/apars/JwtKeys.java b/mock-services/src/main/java/org/zowe/apiml/client/services/apars/JwtKeys.java index eb14772be2..425dd83471 100644 --- a/mock-services/src/main/java/org/zowe/apiml/client/services/apars/JwtKeys.java +++ b/mock-services/src/main/java/org/zowe/apiml/client/services/apars/JwtKeys.java @@ -26,7 +26,7 @@ public JwtKeys(List usernames, List passwords) { protected ResponseEntity handleJwtKeys() { try { JWKSet jwkSet = JwtTokenService.getKeySet(); - return new ResponseEntity<>(jwkSet, HttpStatus.OK); + return new ResponseEntity<>(jwkSet.toJSONObject(), HttpStatus.OK); } catch (Exception e) { return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } diff --git a/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/handler/RestResponseHandler.java b/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/handler/RestResponseHandler.java index c115b17d35..a99f07ee33 100644 --- a/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/handler/RestResponseHandler.java +++ b/security-service-client-spring/src/main/java/org/zowe/apiml/security/client/handler/RestResponseHandler.java @@ -23,6 +23,7 @@ import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; import org.zowe.apiml.security.common.error.ZosAuthenticationException; import org.zowe.apiml.security.common.token.InvalidTokenTypeException; +import org.zowe.apiml.security.common.token.NoMainframeIdentityException; import org.zowe.apiml.security.common.token.TokenNotProvidedException; import org.zowe.apiml.security.common.token.TokenNotValidException; @@ -51,6 +52,8 @@ public void handleErrorType(CloseableHttpResponse response, ErrorType errorType, throw new ZosAuthenticationException(PlatformReturned.builder().errno(169).errnoMsg("org.zowe.apiml.security.platform.errno.EMVSPASSWORD").build()); } else if (errorType.equals(ErrorType.PASSWORD_EXPIRED)) { throw new ZosAuthenticationException(PlatformReturned.builder().errno(168).errnoMsg("org.zowe.apiml.security.platform.errno.EMVSEXPIRE").build()); + } else if (errorType.equals(ErrorType.IDENTITY_MAPPING_FAILED)) { + throw new NoMainframeIdentityException(errorType.getDefaultMessage()); } } throw new BadCredentialsException(ErrorType.BAD_CREDENTIALS.getDefaultMessage()); diff --git a/security-service-client-spring/src/test/java/org/zowe/apiml/security/client/handler/RestResponseHandlerTest.java b/security-service-client-spring/src/test/java/org/zowe/apiml/security/client/handler/RestResponseHandlerTest.java index 1821b57558..cc91b53b8d 100644 --- a/security-service-client-spring/src/test/java/org/zowe/apiml/security/client/handler/RestResponseHandlerTest.java +++ b/security-service-client-spring/src/test/java/org/zowe/apiml/security/client/handler/RestResponseHandlerTest.java @@ -23,6 +23,7 @@ import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; import org.zowe.apiml.security.common.error.ZosAuthenticationException; import org.zowe.apiml.security.common.token.InvalidTokenTypeException; +import org.zowe.apiml.security.common.token.NoMainframeIdentityException; import org.zowe.apiml.security.common.token.TokenNotProvidedException; import org.zowe.apiml.security.common.token.TokenNotValidException; @@ -122,6 +123,12 @@ void thenHttpServerError() { when(response.getCode()).thenReturn(500); assertThrows(ServiceNotAccessibleException.class, () -> handler.handleErrorType(response, null, GENERIC_LOG_MESSAGE, LOG_PARAMETERS)); } + + @Test + void thenNoMappingError() { + assertThrows(NoMainframeIdentityException.class, + () -> handler.handleErrorType(response, ErrorType.IDENTITY_MAPPING_FAILED, GENERIC_LOG_MESSAGE, LOG_PARAMETERS)); + } } @Test